From 4e39a03d75f27f7c5f94320bd33cd01d67da1bba Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Jan 2025 12:12:30 +0530 Subject: [PATCH 01/33] feat: claimer --- validator-cli/src/ArbToEth/claimer.ts | 81 ++++++++++++ .../src/ArbToEth/transactionHandler.ts | 116 +++++++++++++++++- validator-cli/src/utils/botEvents.ts | 12 +- validator-cli/src/utils/claim.ts | 11 ++ validator-cli/src/utils/cli.ts | 30 +++++ validator-cli/src/utils/ethers.ts | 9 +- validator-cli/src/utils/graphQueries.ts | 65 ++++++++++ validator-cli/src/utils/logger.ts | 50 +++++--- validator-cli/src/watcher.ts | 19 ++- 9 files changed, 370 insertions(+), 23 deletions(-) create mode 100644 validator-cli/src/ArbToEth/claimer.ts create mode 100644 validator-cli/src/utils/cli.ts create mode 100644 validator-cli/src/utils/graphQueries.ts diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts new file mode 100644 index 00000000..87d64add --- /dev/null +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -0,0 +1,81 @@ +import { EventEmitter } from "events"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ethers } from "ethers"; +import { getClaim } from "../utils/claim"; +import { getLastClaimedEpoch } from "../utils/graphQueries"; +import { ArbToEthTransactionHandler } from "./transactionHandler"; +import { BotEvents } from "../utils/botEvents"; +interface checkAndClaimParams { + epochPeriod: number; + epoch: number; + veaInbox: any; + veaInboxProvider: JsonRpcProvider; + veaOutbox: any; + veaOutboxProvider: JsonRpcProvider; + transactionHandler: ArbToEthTransactionHandler | null; + emitter: EventEmitter; +} + +export async function checkAndClaim({ + epoch, + epochPeriod, + veaInbox, + veaInboxProvider, + veaOutbox, + veaOutboxProvider, + transactionHandler, + emitter, +}: checkAndClaimParams) { + let outboxStateRoot = await veaOutbox.stateRoot(); + const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); + const claimAbleEpoch = finalizedOutboxBlock.timestamp / epochPeriod; + const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, finalizedOutboxBlock.number, "finalized"); + if (claim == null && epoch == claimAbleEpoch) { + const savedSnapshot = await veaInbox.snapshots(epoch); + if (savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash) { + const claimData = await getLastClaimedEpoch(); + if (claimData.challenged || claimData.stateroot != savedSnapshot) { + if (!transactionHandler) { + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim + ); + } + await transactionHandler.makeClaim(savedSnapshot); + } + } + return null; + } else if (claim != null) { + if (!transactionHandler) { + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim + ); + } else { + transactionHandler.claim = claim; + } + if (claim.honest == 1) { + await transactionHandler.withdrawClaimDeposit(); + } else if (claim.honest == 0) { + if (claim.timestampVerification == 0) { + await transactionHandler.startVerification(); + } else { + await transactionHandler.verifySnapshot(); + } + } + } else { + emitter.emit(BotEvents.CLAIM_EPOCH_PASSED, epoch); + } + if (transactionHandler) return transactionHandler; + return null; +} diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index 571b3684..dd11c659 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -17,6 +17,10 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; */ type Transactions = { + claimTxn: string | null; + withdrawClaimDepositTxn: string | null; + startVerificationTxn: string | null; + verifySnapshotTxn: string | null; challengeTxn: string | null; withdrawChallengeDepositTxn: string | null; sendSnapshotTxn: string | null; @@ -48,6 +52,10 @@ export class ArbToEthTransactionHandler { public emitter: typeof defaultEmitter; public transactions: Transactions = { + claimTxn: null, + withdrawClaimDepositTxn: null, + startVerificationTxn: null, + verifySnapshotTxn: null, challengeTxn: null, withdrawChallengeDepositTxn: null, sendSnapshotTxn: null, @@ -110,6 +118,112 @@ export class ArbToEthTransactionHandler { return TransactionStatus.NOT_FINAL; } + /** + * Make a claim on the VeaOutbox(ETH). + * + * @param snapshot - The snapshot to be claimed. + */ + public async makeClaim(stateRoot: string) { + this.emitter.emit(BotEvents.CLAIMING, this.epoch); + if ((await this.checkTransactionStatus(this.transactions.claimTxn, ContractType.OUTBOX)) > 0) { + return; + } + const { deposit } = getBridgeConfig(this.chainId); + + const estimateGas = await this.veaOutbox["claim(uint256,bytes32)"].estimateGas(this.epoch, stateRoot, { + value: deposit, + }); + const claimTransaction = await this.veaOutbox.claim(this.epoch, stateRoot, { + value: deposit, + gasLimit: estimateGas, + }); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Claim"); + this.transactions.claimTxn = claimTransaction.hash; + } + + /** + * Start verification for this.epoch in VeaOutbox(ETH). + */ + public async startVerification() { + this.emitter.emit(BotEvents.STARTING_VERIFICATION, this.epoch); + if (this.claim == null) { + throw new ClaimNotSetError(); + } + if ((await this.checkTransactionStatus(this.transactions.startVerificationTxn, ContractType.OUTBOX)) > 0) { + return; + } + const latestBlockTimestamp = (await this.veaOutboxProvider.getBlock("latest")).timestamp; + + const bridgeConfig = getBridgeConfig(this.chainId); + const timeOver = + latestBlockTimestamp - + Number(this.claim.timestampClaimed) - + bridgeConfig.sequencerDelayLimit - + bridgeConfig.epochPeriod; + + if (timeOver < 0) { + this.emitter.emit(BotEvents.VERIFICATION_CANT_START, -1 * timeOver); + return; + } + const estimateGas = await this.veaOutbox[ + "startVerification(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))" + ].estimateGas(this.epoch, this.claim); + const startVerifTrx = await this.veaOutbox.startVerification(this.epoch, this.claim, { gasLimit: estimateGas }); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, startVerifTrx.hash, "Start Verification"); + this.transactions.startVerificationTxn = startVerifTrx.hash; + } + + /** + * Verify snapshot for this.epoch in VeaOutbox(ETH). + */ + public async verifySnapshot() { + this.emitter.emit(BotEvents.VERIFYING_SNAPSHOT, this.epoch); + if (this.claim == null) { + throw new ClaimNotSetError(); + } + if ((await this.checkTransactionStatus(this.transactions.verifySnapshotTxn, ContractType.OUTBOX)) > 0) { + return; + } + const latestBlockTimestamp = (await this.veaOutboxProvider.getBlock("latest")).timestamp; + const bridgeConfig = getBridgeConfig(this.chainId); + + const timeLeft = latestBlockTimestamp - Number(this.claim.timestampClaimed) - bridgeConfig.minChallengePeriod; + + // Claim not resolved yet, check if we can verifySnapshot + if (timeLeft < 0) { + this.emitter.emit(BotEvents.CANT_VERIFY_SNAPSHOT, -1 * timeLeft); + return; + } + // Estimate gas for verifySnapshot + const estimateGas = await this.veaOutbox[ + "verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))" + ].estimateGas(this.epoch, this.claim); + const claimTransaction = await this.veaOutbox.verifySnapshot(this.epoch, this.claim, { + gasLimit: estimateGas, + }); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Verify Snapshot"); + this.transactions.verifySnapshotTxn = claimTransaction.hash; + } + + /** + * Withdraw the claim deposit. + * + */ + public async withdrawClaimDeposit() { + this.emitter.emit(BotEvents.WITHDRAWING_CLAIM_DEPOSIT, this.epoch); + if ((await this.checkTransactionStatus(this.transactions.withdrawClaimDepositTxn, ContractType.OUTBOX)) > 0) { + return; + } + const estimateGas = await this.veaOutbox[ + "withdrawClaimDeposit(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))" + ].estimateGas(this.epoch, this.claim); + const withdrawTxn = await this.veaOutbox.withdrawClaimDeposit(this.epoch, this.claim, { + gasLimit: estimateGas, + }); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, withdrawTxn.hash, "Withdraw Deposit"); + this.transactions.withdrawClaimDepositTxn = withdrawTxn.hash; + } + /** * Challenge claim for this.epoch in VeaOutbox(ETH). * @@ -154,7 +268,7 @@ export class ArbToEthTransactionHandler { * */ public async withdrawChallengeDeposit() { - this.emitter.emit(BotEvents.WITHDRAWING); + this.emitter.emit(BotEvents.WITHDRAWING_CHALLENGE_DEPOSIT); if (!this.claim) { throw new ClaimNotSetError(); } diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index fef5ed5b..ba3cb8b6 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -9,15 +9,21 @@ export enum BotEvents { // Epoch state NO_NEW_MESSAGES = "no_new_messages", NO_SNAPSHOT = "no_snapshot", - EPOCH_PASSED = "epoch_passed", + CLAIM_EPOCH_PASSED = "claim_epoch_passed", // Claim state + CLAIMING = "claiming", + STARTING_VERIFICATION = "starting_verification", + VERIFICATION_CANT_START = "verification_cant_start", + VERIFYING_SNAPSHOT = "verifying_snapshot", + CANT_VERIFY_SNAPSHOT = "cant_verify_snapshot", CHALLENGING = "challenging", CHALLENGER_WON_CLAIM = "challenger_won_claim", SENDING_SNAPSHOT = "sending_snapshot", EXECUTING_SNAPSHOT = "executing_snapshot", - CANT_EXECUTE_SNAPSHOT = "CANT_EXECUTE_SNAPSHOT", - WITHDRAWING = "withdrawing", + CANT_EXECUTE_SNAPSHOT = "cant_execute_snapshot", + WITHDRAWING_CHALLENGE_DEPOSIT = "withdrawing_challenge_deposit", + WITHDRAWING_CLAIM_DEPOSIT = "withdrawing_claim_deposit", WAITING_ARB_TIMEOUT = "waiting_arb_timeout", // Transaction state diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 6b7e2262..b988a7c5 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -80,6 +80,17 @@ type ClaimResolveState = { }; }; +/** + * Fetches the claim resolve state. + * @param veaInbox VeaInbox contract instance + * @param veaInboxProvider VeaInbox provider + * @param veaOutboxProvider VeaOutbox provider + * @param epoch epoch number of the claim to be fetched + * @param fromBlock from block number + * @param toBlock to block number + * @param fetchMessageStatus function to fetch message status + * @returns ClaimResolveState + **/ const getClaimResolveState = async ( veaInbox: any, veaInboxProvider: JsonRpcProvider, diff --git a/validator-cli/src/utils/cli.ts b/validator-cli/src/utils/cli.ts new file mode 100644 index 00000000..83ca4d98 --- /dev/null +++ b/validator-cli/src/utils/cli.ts @@ -0,0 +1,30 @@ +export enum BotPaths { + CLAIMER = 0, // happy path + CHALLENGER = 1, // unhappy path + BOTH = 2, // both happy and unhappy path +} + +/** + * Get the bot path from the command line arguments + * @param defaultPath - default path to use if not specified in the command line arguments + * @returns BotPaths - the bot path (BotPaths) + */ +export function getBotPath(defaultPath: number = BotPaths.BOTH): number { + const args = process.argv.slice(2); + const pathFlag = args.find((arg) => arg.startsWith("--path=")); + + const path = pathFlag ? pathFlag.split("=")[1] : null; + + const pathMapping: Record = { + claimer: BotPaths.CLAIMER, + challenger: BotPaths.CHALLENGER, + both: BotPaths.BOTH, + }; + + if (path && !(path in pathMapping)) { + console.error(`Error: Invalid path '${path}'. Use one of: ${Object.keys(pathMapping).join(", ")}.`); + process.exit(1); + } + + return path ? pathMapping[path] : defaultPath; +} diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 29187eb2..5707a2b8 100644 --- a/validator-cli/src/utils/ethers.ts +++ b/validator-cli/src/utils/ethers.ts @@ -10,6 +10,7 @@ import { IAMB__factory, } from "@kleros/vea-contracts/typechain-types"; import { challengeAndResolveClaim as challengeAndResolveClaimArbToEth } from "../ArbToEth/validator"; +import { checkAndClaim } from "../ArbToEth/claimer"; import { ArbToEthTransactionHandler } from "../ArbToEth/transactionHandler"; import { TransactionHandlerNotDefinedError } from "./errors"; @@ -64,7 +65,12 @@ const getClaimValidator = (chainId: number) => { return challengeAndResolveClaimArbToEth; } }; - +const getClaimer = (chainId: number) => { + switch (chainId) { + case 11155111: + return checkAndClaim; + } +}; const getTransactionHandler = (chainId: number) => { switch (chainId) { case 11155111: @@ -82,6 +88,7 @@ export { getWETH, getAMB, getClaimValidator, + getClaimer, getTransactionHandler, getVeaRouter, }; diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts new file mode 100644 index 00000000..a7b5ca6d --- /dev/null +++ b/validator-cli/src/utils/graphQueries.ts @@ -0,0 +1,65 @@ +import request from "graphql-request"; + +interface ClaimData { + id: string; + bridger: string; + stateroot: string; + timestamp: number; + challenged: boolean; + txHash: string; +} + +/** + * Fetches the claim data for a given epoch (used for claimer - happy path) + * @param epoch + * @returns ClaimData + * */ +const getClaimForEpoch = async (epoch: number): Promise => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + + const result = await request( + `${subgraph}`, + `{ + claims(where: {epoch: ${epoch}}) { + id + bridger + stateroot + timestamp + txHash + challenged + } + }` + ); + return result[`claims`][0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +/** + * Fetches the last claimed epoch (used for claimer - happy path) + * @returns ClaimData + */ +const getLastClaimedEpoch = async (): Promise => { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + + const result = await request( + `${subgraph}`, + `{ + claims(first:1, orderBy:timestamp, orderDirection:desc){ + id + bridger + stateroot + timestamp + challenged + txHash + } + + }` + ); + return result[`claims`][0]; +}; + +export { getClaimForEpoch, getLastClaimedEpoch, ClaimData }; diff --git a/validator-cli/src/utils/logger.ts b/validator-cli/src/utils/logger.ts index 7b858b2e..2a0fc4b4 100644 --- a/validator-cli/src/utils/logger.ts +++ b/validator-cli/src/utils/logger.ts @@ -1,5 +1,6 @@ import { EventEmitter } from "node:events"; import { BotEvents } from "./botEvents"; +import { BotPaths } from "./cli"; /** * Listens to relevant events of an EventEmitter instance and issues log lines @@ -18,8 +19,14 @@ export const initialize = (emitter: EventEmitter) => { export const configurableInitialize = (emitter: EventEmitter) => { // Bridger state logs - emitter.on(BotEvents.STARTED, () => { - console.log("Validator started"); + emitter.on(BotEvents.STARTED, (chainId: number, path: number) => { + let pathString = "challenger and claimer"; + if (path === BotPaths.CLAIMER) { + pathString = "bridger"; + } else if (path === BotPaths.CHALLENGER) { + pathString = "challenger"; + } + console.log(`Bot started for chainId ${chainId} as ${pathString}`); }); emitter.on(BotEvents.CHECKING, (epoch: number) => { @@ -30,16 +37,13 @@ export const configurableInitialize = (emitter: EventEmitter) => { console.log(`Waiting for next verifiable epoch after ${epoch}`); }); + // Epoch state logs emitter.on(BotEvents.NO_SNAPSHOT, () => { console.log("No snapshot saved for epoch"); }); - emitter.on(BotEvents.EPOCH_PASSED, (epoch: number) => { - console.log(`Epoch ${epoch} has passed`); - }); - - emitter.on(BotEvents.CHALLENGER_WON_CLAIM, () => { - console.log("Challenger won claim"); + emitter.on(BotEvents.CLAIM_EPOCH_PASSED, (epoch: number) => { + console.log(`Epoch ${epoch} has passed for claiming`); }); // Transaction state logs @@ -62,29 +66,44 @@ export const configurableInitialize = (emitter: EventEmitter) => { }); // Claim state logs - // makeClaim() + // claim() + emitter.on(BotEvents.CLAIMING, (epoch: number) => { + console.log(`Claiming for epoch ${epoch}`); + }); + // startVerification() + emitter.on(BotEvents.STARTING_VERIFICATION, (epoch: number) => { + console.log(`Starting verification for epoch ${epoch}`); + }); + emitter.on(BotEvents.VERIFICATION_CANT_START, (epoch: number, timeLeft: number) => { + console.log(`Verification cant start for epoch ${epoch}, time left: ${timeLeft}`); + }); + // verifySnapshot() + emitter.on(BotEvents.VERIFYING_SNAPSHOT, (epoch: number) => { + console.log(`Verifying snapshot for epoch ${epoch}`); + }); + emitter.on(BotEvents.CANT_VERIFY_SNAPSHOT, (epoch: number, timeLeft: number) => { + console.log(`Cant verify snapshot for epoch ${epoch}, time left: ${timeLeft}`); + }); + // challenge() emitter.on(BotEvents.CHALLENGING, (epoch: number) => { console.log(`Claim can be challenged, challenging for epoch ${epoch}`); }); - // startVerification() emitter.on(BotEvents.SENDING_SNAPSHOT, (epoch: number) => { console.log(`Sending snapshot for ${epoch}`); }); + // executeSnapshot() emitter.on(BotEvents.EXECUTING_SNAPSHOT, (epoch) => { console.log(`Executing snapshot to resolve dispute for epoch ${epoch}`); }); - // verifySnapshot() emitter.on(BotEvents.CANT_EXECUTE_SNAPSHOT, () => { console.log("Cant execute snapshot, waiting l2 challenge period to pass"); }); - // withdrawClaimDeposit() - emitter.on(BotEvents.WITHDRAWING, () => { + emitter.on(BotEvents.WITHDRAWING_CHALLENGE_DEPOSIT, () => { console.log(`Withdrawing challenge deposit for epoch`); }); - emitter.on(BotEvents.WAITING_ARB_TIMEOUT, (epoch: number) => { console.log(`Waiting for arbitrum bridge timeout for epoch ${epoch}`); }); @@ -96,4 +115,7 @@ export const configurableInitialize = (emitter: EventEmitter) => { emitter.on(BotEvents.VALID_CLAIM, (epoch: number) => { console.log(`Valid claim was made for ${epoch}`); }); + emitter.on(BotEvents.CHALLENGER_WON_CLAIM, () => { + console.log("Challenger won claim"); + }); }; diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index 0d5795ee..84c2a7ee 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -2,11 +2,12 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { getBridgeConfig, Bridge } from "./consts/bridgeRoutes"; import { getVeaInbox, getVeaOutbox, getTransactionHandler } from "./utils/ethers"; import { setEpochRange, getLatestChallengeableEpoch } from "./utils/epochHandler"; -import { getClaimValidator } from "./utils/ethers"; +import { getClaimValidator, getClaimer } from "./utils/ethers"; import { defaultEmitter } from "./utils/emitter"; import { BotEvents } from "./utils/botEvents"; import { initialize as initializeLogger } from "./utils/logger"; import { ShutdownSignal } from "./utils/shutdown"; +import { getBotPath, BotPaths } from "./utils/cli"; /** * @file This file contains the logic for watching a bridge and validating/resolving for claims. @@ -21,14 +22,17 @@ export const watch = async ( emitter: typeof defaultEmitter = defaultEmitter ) => { initializeLogger(emitter); - emitter.emit(BotEvents.STARTED); + const path = getBotPath(); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); + emitter.emit(BotEvents.STARTED, chainId, path); + const veaBridge: Bridge = getBridgeConfig(chainId); const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); const veaInboxProvider = new JsonRpcProvider(veaBridge.inboxRPC); const veaOutboxProvider = new JsonRpcProvider(veaBridge.outboxRPC); const checkAndChallengeResolve = getClaimValidator(chainId); + const checkAndClaim = getClaimer(chainId); const TransactionHandler = getTransactionHandler(chainId); let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); @@ -51,13 +55,20 @@ export const watch = async ( transactionHandler: transactionHandlers[epoch], emitter, }; - const updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); + let updatedTransactions; + if (path > BotPaths.CLAIMER) { + updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); + } + if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { + updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); + } + if (updatedTransactions) { transactionHandlers[epoch] = updatedTransactions; } else { delete transactionHandlers[epoch]; epochRange.splice(i, 1); - i--; + continue; } i++; } From e6b5d7ed84d4014c84893b48fcf1e5205e1579d2 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Jan 2025 16:21:18 +0530 Subject: [PATCH 02/33] feat: happy path tests --- validator-cli/src/ArbToEth/claimer.ts | 4 +- .../src/ArbToEth/transactionHandler.test.ts | 217 +++++++++++++++++- .../src/ArbToEth/transactionHandler.ts | 17 +- 3 files changed, 226 insertions(+), 12 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 87d64add..66363a2d 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -68,9 +68,9 @@ export async function checkAndClaim({ await transactionHandler.withdrawClaimDeposit(); } else if (claim.honest == 0) { if (claim.timestampVerification == 0) { - await transactionHandler.startVerification(); + await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); } else { - await transactionHandler.verifySnapshot(); + await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp); } } } else { diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index 12b7fb6f..2b29a7a8 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -3,8 +3,10 @@ import { MockEmitter, defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { ClaimNotSetError } from "../utils/errors"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; +import { getBridgeConfig } from "../consts/bridgeRoutes"; describe("ArbToEthTransactionHandler", () => { + const chainId = 11155111; let epoch: number = 100; let veaInbox: any; let veaOutbox: any; @@ -18,9 +20,15 @@ describe("ArbToEthTransactionHandler", () => { getBlock: jest.fn(), }; veaOutbox = { - estimateGas: jest.fn(), + estimateGas: { + claim: jest.fn(), + }, withdrawChallengeDeposit: jest.fn(), ["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"]: jest.fn(), + claim: jest.fn(), + startVerification: jest.fn(), + verifySnapshot: jest.fn(), + withdrawClaimDeposit: jest.fn(), }; veaInbox = { sendSnapshot: jest.fn(), @@ -131,6 +139,205 @@ describe("ArbToEthTransactionHandler", () => { }); }); + // Happy path (claimer) + describe("makeClaim", () => { + let transactionHandler: ArbToEthTransactionHandler; + const mockEmitter = new MockEmitter(); + const { deposit } = getBridgeConfig(chainId); + beforeEach(() => { + const mockClaim = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockClaim as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["claim(uint256,bytes32)"] = mockClaim; + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + mockEmitter + ); + veaOutbox.claim.mockResolvedValue({ hash: "0x1234" }); + }); + + it("should make a claim and set pending claim trnx", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + + await transactionHandler.makeClaim(claim.stateRoot as string); + + expect(veaOutbox.claim).toHaveBeenCalledWith(epoch, claim.stateRoot, { + gasLimit: BigInt(100000), + value: deposit, + }); + expect(transactionHandler.transactions.claimTxn).toEqual("0x1234"); + }); + + it("should not make a claim if a claim transaction is pending", async () => { + // Mock checkTransactionPendingStatus to always return true + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); + await transactionHandler.makeClaim(claim.stateRoot as string); + expect(veaOutbox.claim).not.toHaveBeenCalled(); + expect(transactionHandler.transactions.claimTxn).toBeNull(); + }); + }); + + describe("startVerification", () => { + let transactionHandler: ArbToEthTransactionHandler; + const mockEmitter = new MockEmitter(); + const { epochPeriod, sequencerDelayLimit } = getBridgeConfig(chainId); + let startVerificationFlipTime: number; + const mockStartVerification = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockStartVerification as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + beforeEach(() => { + veaOutbox["startVerification(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = + mockStartVerification; + veaOutbox.startVerification.mockResolvedValue({ hash: "0x1234" }); + startVerificationFlipTime = Number(claim.timestampClaimed) + epochPeriod + sequencerDelayLimit; + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + mockEmitter + ); + transactionHandler.claim = claim; + }); + + it("should start verification and set pending startVerificationTxm", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + + await transactionHandler.startVerification(startVerificationFlipTime); + + expect( + veaOutbox["startVerification(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"].estimateGas + ).toHaveBeenCalledWith(epoch, claim); + expect(veaOutbox.startVerification).toHaveBeenCalledWith(epoch, claim, { gasLimit: BigInt(100000) }); + expect(transactionHandler.transactions.startVerificationTxn).toEqual("0x1234"); + }); + + it("should not start verification if a startVerification transaction is pending", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); + + await transactionHandler.startVerification(startVerificationFlipTime); + + expect(veaOutbox.startVerification).not.toHaveBeenCalled(); + expect(transactionHandler.transactions.startVerificationTxn).toBeNull(); + }); + + it("should throw an error if claim is not set", async () => { + transactionHandler.claim = null; + await expect(transactionHandler.startVerification(startVerificationFlipTime)).rejects.toThrow(ClaimNotSetError); + }); + + it("should not start verification if timeout has not passed", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + await transactionHandler.startVerification(startVerificationFlipTime - 1); + expect(veaOutbox.startVerification).not.toHaveBeenCalled(); + expect(transactionHandler.transactions.startVerificationTxn).toBeNull(); + }); + }); + + describe("verifySnapshot", () => { + let verificationFlipTime: number; + let transactionHandler: ArbToEthTransactionHandler; + const mockEmitter = new MockEmitter(); + beforeEach(() => { + const mockVerifySnapshot = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockVerifySnapshot as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockVerifySnapshot; + veaOutbox.verifySnapshot.mockResolvedValue({ hash: "0x1234" }); + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + mockEmitter + ); + verificationFlipTime = Number(claim.timestampClaimed) + getBridgeConfig(chainId).minChallengePeriod; + transactionHandler.claim = claim; + }); + + it("should verify snapshot and set pending verifySnapshotTxn", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + + await transactionHandler.verifySnapshot(verificationFlipTime); + + expect( + veaOutbox["verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"].estimateGas + ).toHaveBeenCalledWith(epoch, claim); + expect(veaOutbox.verifySnapshot).toHaveBeenCalledWith(epoch, claim, { gasLimit: BigInt(100000) }); + expect(transactionHandler.transactions.verifySnapshotTxn).toEqual("0x1234"); + }); + + it("should not verify snapshot if a verifySnapshot transaction is pending", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); + + await transactionHandler.verifySnapshot(verificationFlipTime); + + expect(veaOutbox.verifySnapshot).not.toHaveBeenCalled(); + expect(transactionHandler.transactions.verifySnapshotTxn).toBeNull(); + }); + + it("should throw an error if claim is not set", async () => { + transactionHandler.claim = null; + await expect(transactionHandler.verifySnapshot(verificationFlipTime)).rejects.toThrow(ClaimNotSetError); + }); + + it("should not verify snapshot if timeout has not passed", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + await transactionHandler.verifySnapshot(verificationFlipTime - 1); + expect(veaOutbox.verifySnapshot).not.toHaveBeenCalled(); + expect(transactionHandler.transactions.verifySnapshotTxn).toBeNull(); + }); + }); + + describe("withdrawClaimDeposit", () => { + let transactionHandler: ArbToEthTransactionHandler; + const mockEmitter = new MockEmitter(); + beforeEach(() => { + const mockWithdrawClaimDeposit = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockWithdrawClaimDeposit as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["withdrawClaimDeposit(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = + mockWithdrawClaimDeposit; + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + mockEmitter + ); + veaOutbox.withdrawClaimDeposit.mockResolvedValue("0x1234"); + transactionHandler.claim = claim; + }); + + it("should withdraw deposit", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + veaOutbox.withdrawClaimDeposit.mockResolvedValue({ hash: "0x1234" }); + await transactionHandler.withdrawClaimDeposit(); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.OUTBOX); + expect(transactionHandler.transactions.withdrawClaimDepositTxn).toEqual("0x1234"); + }); + + it("should not withdraw deposit if txn is pending", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); + transactionHandler.transactions.withdrawClaimDepositTxn = "0x1234"; + await transactionHandler.withdrawClaimDeposit(); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( + transactionHandler.transactions.withdrawClaimDepositTxn, + ContractType.OUTBOX + ); + }); + + it("should throw an error if claim is not set", async () => { + transactionHandler.claim = null; + await expect(transactionHandler.withdrawClaimDeposit()).rejects.toThrow(ClaimNotSetError); + }); + }); + + // Unhappy path (challenger) describe("challengeClaim", () => { let transactionHandler: ArbToEthTransactionHandler; const mockEmitter = new MockEmitter(); @@ -161,7 +368,9 @@ describe("ArbToEthTransactionHandler", () => { transactionHandler.transactions.challengeTxn, ContractType.OUTBOX ); - expect(veaOutbox.estimateGas).not.toHaveBeenCalled(); + expect( + veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] + ).not.toHaveBeenCalled(); }); it("should challenge claim", async () => { @@ -177,7 +386,7 @@ describe("ArbToEthTransactionHandler", () => { it.todo("should set challengeTxn as completed when txn is final"); }); - describe("withdrawDeposit", () => { + describe("withdrawChallengeDeposit", () => { let transactionHandler: ArbToEthTransactionHandler; const mockEmitter = new MockEmitter(); beforeEach(() => { @@ -219,7 +428,7 @@ describe("ArbToEthTransactionHandler", () => { it("should emit WITHDRAWING event", async () => { jest.spyOn(mockEmitter, "emit"); await transactionHandler.withdrawChallengeDeposit(); - expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.WITHDRAWING); + expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.WITHDRAWING_CHALLENGE_DEPOSIT); }); }); diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index dd11c659..1a6846b7 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -10,6 +10,10 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; /** * @file This file contains the logic for handling transactions from Arbitrum to Ethereum. * It is responsible for: + * makeClaim() - Make a claim on the VeaOutbox(ETH). + * startVerification() - Start verification for this.epoch in VeaOutbox(ETH). + * verifySnapshot() - Verify snapshot for this.epoch in VeaOutbox(ETH). + * withdrawClaimDeposit() - Withdraw the claim deposit. * challenge() - Challenge a claim on VeaOutbox(ETH). * withdrawChallengeDeposit() - Withdraw the challenge deposit. * sendSnapshot() - Send a snapshot from the VeaInbox(ARB) to the VeaOutox(ETH). @@ -144,7 +148,7 @@ export class ArbToEthTransactionHandler { /** * Start verification for this.epoch in VeaOutbox(ETH). */ - public async startVerification() { + public async startVerification(currentTimestamp: number) { this.emitter.emit(BotEvents.STARTING_VERIFICATION, this.epoch); if (this.claim == null) { throw new ClaimNotSetError(); @@ -152,11 +156,10 @@ export class ArbToEthTransactionHandler { if ((await this.checkTransactionStatus(this.transactions.startVerificationTxn, ContractType.OUTBOX)) > 0) { return; } - const latestBlockTimestamp = (await this.veaOutboxProvider.getBlock("latest")).timestamp; const bridgeConfig = getBridgeConfig(this.chainId); const timeOver = - latestBlockTimestamp - + currentTimestamp - Number(this.claim.timestampClaimed) - bridgeConfig.sequencerDelayLimit - bridgeConfig.epochPeriod; @@ -176,7 +179,7 @@ export class ArbToEthTransactionHandler { /** * Verify snapshot for this.epoch in VeaOutbox(ETH). */ - public async verifySnapshot() { + public async verifySnapshot(currentTimestamp: number) { this.emitter.emit(BotEvents.VERIFYING_SNAPSHOT, this.epoch); if (this.claim == null) { throw new ClaimNotSetError(); @@ -184,10 +187,9 @@ export class ArbToEthTransactionHandler { if ((await this.checkTransactionStatus(this.transactions.verifySnapshotTxn, ContractType.OUTBOX)) > 0) { return; } - const latestBlockTimestamp = (await this.veaOutboxProvider.getBlock("latest")).timestamp; const bridgeConfig = getBridgeConfig(this.chainId); - const timeLeft = latestBlockTimestamp - Number(this.claim.timestampClaimed) - bridgeConfig.minChallengePeriod; + const timeLeft = currentTimestamp - Number(this.claim.timestampClaimed) - bridgeConfig.minChallengePeriod; // Claim not resolved yet, check if we can verifySnapshot if (timeLeft < 0) { @@ -211,6 +213,9 @@ export class ArbToEthTransactionHandler { */ public async withdrawClaimDeposit() { this.emitter.emit(BotEvents.WITHDRAWING_CLAIM_DEPOSIT, this.epoch); + if (this.claim == null) { + throw new ClaimNotSetError(); + } if ((await this.checkTransactionStatus(this.transactions.withdrawClaimDepositTxn, ContractType.OUTBOX)) > 0) { return; } From 829bd776f3501052f4f1b760d883d1faa552ad4c Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Jan 2025 18:34:29 +0530 Subject: [PATCH 03/33] feat: claimer tests --- validator-cli/src/ArbToEth/claimer.test.ts | 176 +++++++++++++++++++++ validator-cli/src/ArbToEth/claimer.ts | 68 ++++---- 2 files changed, 207 insertions(+), 37 deletions(-) create mode 100644 validator-cli/src/ArbToEth/claimer.test.ts diff --git a/validator-cli/src/ArbToEth/claimer.test.ts b/validator-cli/src/ArbToEth/claimer.test.ts new file mode 100644 index 00000000..7392d207 --- /dev/null +++ b/validator-cli/src/ArbToEth/claimer.test.ts @@ -0,0 +1,176 @@ +import { ethers } from "ethers"; +import { checkAndClaim } from "./claimer"; +import { ArbToEthTransactionHandler } from "./transactionHandler"; +import { ClaimHonestState } from "../utils/claim"; +import { start } from "pm2"; +describe("claimer", () => { + let veaOutbox: any; + let veaInbox: any; + let veaInboxProvider: any; + let veaOutboxProvider: any; + let emitter: any; + let mockGetClaim: any; + let mockClaim: any; + let mockGetLatestClaimedEpoch: any; + let mockDeps: any; + beforeEach(() => { + mockClaim = { + stateRoot: "0x1234", + claimer: "0xFa00D29d378EDC57AA1006946F0fc6230a5E3288", + timestampClaimed: 1234, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: ethers.ZeroAddress, + }; + veaInbox = { + snapshots: jest.fn().mockResolvedValue(mockClaim.stateRoot), + }; + + veaOutbox = { + stateRoot: jest.fn().mockResolvedValue(mockClaim.stateRoot), + }; + veaOutboxProvider = { + getBlock: jest.fn().mockResolvedValue({ number: 0, timestamp: 100 }), + }; + emitter = { + emit: jest.fn(), + }; + + mockGetClaim = jest.fn(); + mockGetLatestClaimedEpoch = jest.fn(); + mockDeps = { + epoch: 10, + epochPeriod: 10, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: null, + emitter, + fetchClaim: mockGetClaim, + fetchLatestClaimedEpoch: mockGetLatestClaimedEpoch, + }; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe("checkAndClaim", () => { + let mockTransactionHandler: any; + const mockTransactions = { + claimTxn: "0x111", + withdrawClaimDepositTxn: "0x222", + startVerificationTxn: "0x333", + verifySnapshotTxn: "0x444", + }; + beforeEach(() => { + mockTransactionHandler = { + withdrawClaimDeposit: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.withdrawClaimDepositTxn = mockTransactions.withdrawClaimDepositTxn; + return Promise.resolve(); + }), + makeClaim: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.claimTxn = mockTransactions.claimTxn; + return Promise.resolve(); + }), + startVerification: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.startVerificationTxn = mockTransactions.startVerificationTxn; + return Promise.resolve(); + }), + verifySnapshot: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.verifySnapshotTxn = mockTransactions.verifySnapshotTxn; + return Promise.resolve(); + }), + transactions: { + claimTxn: "0x0", + withdrawClaimDepositTxn: "0x0", + startVerificationTxn: "0x0", + verifySnapshotTxn: "0x0", + }, + }; + }); + it("should return null if no claim is made for a passed epoch", async () => { + mockGetClaim = jest.fn().mockReturnValue(null); + mockDeps.epoch = 7; // claimable epoch - 3 + mockDeps.fetchClaim = mockGetClaim; + const result = await checkAndClaim(mockDeps); + expect(result).toBeNull(); + }); + it("should return null if no snapshot is saved on the inbox for a claimable epoch", async () => { + mockGetClaim = jest.fn().mockReturnValue(null); + veaInbox.snapshots = jest.fn().mockResolvedValue(ethers.ZeroHash); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: false, + stateroot: "0x1111", + }); + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + const result = await checkAndClaim(mockDeps); + expect(result).toBeNull(); + }); + it("should return null if there are no new messages in the inbox", async () => { + mockGetClaim = jest.fn().mockReturnValue(null); + veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: false, + stateroot: "0x1111", + }); + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + const result = await checkAndClaim(mockDeps); + expect(result).toBeNull(); + }); + it("should make a valid calim if no claim is made", async () => { + mockGetClaim = jest.fn().mockReturnValue(null); + veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890"); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: false, + stateroot: mockClaim.stateRoot, + }); + mockDeps.transactionHandler = mockTransactionHandler; + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + mockDeps.fetchClaim = mockGetClaim; + mockDeps.veaInbox = veaInbox; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.claimTxn).toBe(mockTransactions.claimTxn); + }); + it("should make a valid calim if last claim was challenged", async () => { + mockGetClaim = jest.fn().mockReturnValue(null); + veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: true, + stateroot: mockClaim.stateRoot, + }); + mockDeps.transactionHandler = mockTransactionHandler; + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + mockDeps.fetchClaim = mockGetClaim; + mockDeps.veaInbox = veaInbox; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.claimTxn).toEqual(mockTransactions.claimTxn); + }); + it("should withdraw claim deposit if claimer is honest", async () => { + mockDeps.transactionHandler = mockTransactionHandler; + mockClaim.honest = ClaimHonestState.CLAIMER; + + mockGetClaim = jest.fn().mockResolvedValue(mockClaim); + mockDeps.fetchClaim = mockGetClaim; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.withdrawClaimDepositTxn).toEqual(mockTransactions.withdrawClaimDepositTxn); + }); + it("should start verification if verification is not started", async () => { + mockDeps.transactionHandler = mockTransactionHandler; + mockClaim.honest = ClaimHonestState.NONE; + mockGetClaim = jest.fn().mockResolvedValue(mockClaim); + mockDeps.fetchClaim = mockGetClaim; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.startVerificationTxn).toEqual(mockTransactions.startVerificationTxn); + }); + it("should verify snapshot if verification is started", async () => { + mockDeps.transactionHandler = mockTransactionHandler; + mockClaim.honest = ClaimHonestState.NONE; + mockClaim.timestampVerification = 1234; + mockGetClaim = jest.fn().mockResolvedValue(mockClaim); + mockDeps.fetchClaim = mockGetClaim; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.verifySnapshotTxn).toEqual(mockTransactions.verifySnapshotTxn); + }); + }); +}); diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 66363a2d..b6836b76 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; -import { JsonRpcProvider } from "@ethersproject/providers"; import { ethers } from "ethers"; -import { getClaim } from "../utils/claim"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { getClaim, ClaimHonestState } from "../utils/claim"; import { getLastClaimedEpoch } from "../utils/graphQueries"; import { ArbToEthTransactionHandler } from "./transactionHandler"; import { BotEvents } from "../utils/botEvents"; @@ -14,6 +14,8 @@ interface checkAndClaimParams { veaOutboxProvider: JsonRpcProvider; transactionHandler: ArbToEthTransactionHandler | null; emitter: EventEmitter; + fetchClaim?: typeof getClaim; + fetchLatestClaimedEpoch?: typeof getLastClaimedEpoch; } export async function checkAndClaim({ @@ -25,57 +27,49 @@ export async function checkAndClaim({ veaOutboxProvider, transactionHandler, emitter, + fetchClaim = getClaim, + fetchLatestClaimedEpoch = getLastClaimedEpoch, }: checkAndClaimParams) { let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); const claimAbleEpoch = finalizedOutboxBlock.timestamp / epochPeriod; - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, finalizedOutboxBlock.number, "finalized"); + const claim = await fetchClaim(veaOutbox, veaOutboxProvider, epoch, finalizedOutboxBlock.number, "finalized"); + if (!transactionHandler) { + transactionHandler = new ArbToEthTransactionHandler( + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim + ); + } else { + transactionHandler.claim = claim; + } if (claim == null && epoch == claimAbleEpoch) { - const savedSnapshot = await veaInbox.snapshots(epoch); - if (savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash) { - const claimData = await getLastClaimedEpoch(); - if (claimData.challenged || claimData.stateroot != savedSnapshot) { - if (!transactionHandler) { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - emitter, - claim - ); - } - await transactionHandler.makeClaim(savedSnapshot); - } + const [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); + const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + const lastClaimChallenged: boolean = claimData.challenged && savedSnapshot == outboxStateRoot; + + if (newMessagesToBridge || lastClaimChallenged) { + await transactionHandler.makeClaim(savedSnapshot); + return transactionHandler; } - return null; } else if (claim != null) { - if (!transactionHandler) { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - emitter, - claim - ); - } else { - transactionHandler.claim = claim; - } - if (claim.honest == 1) { + if (claim.honest == ClaimHonestState.CLAIMER) { await transactionHandler.withdrawClaimDeposit(); - } else if (claim.honest == 0) { + return transactionHandler; + } else if (claim.honest == ClaimHonestState.NONE) { if (claim.timestampVerification == 0) { await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); } else { await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp); } + return transactionHandler; } } else { emitter.emit(BotEvents.CLAIM_EPOCH_PASSED, epoch); } - if (transactionHandler) return transactionHandler; return null; } From fc5020c0dd1be2fbb03c02d2900dcc6c76e2ddb0 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Jan 2025 18:36:42 +0530 Subject: [PATCH 04/33] feat: cli path tests --- validator-cli/src/utils/cli.test.ts | 25 +++++++++++++++++++++++++ validator-cli/src/utils/cli.ts | 13 +++++++++---- validator-cli/src/watcher.ts | 3 ++- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 validator-cli/src/utils/cli.test.ts diff --git a/validator-cli/src/utils/cli.test.ts b/validator-cli/src/utils/cli.test.ts new file mode 100644 index 00000000..3ad8cc81 --- /dev/null +++ b/validator-cli/src/utils/cli.test.ts @@ -0,0 +1,25 @@ +import { getBotPath, BotPaths } from "./cli"; +import { InvalidBotPathError } from "./errors"; +describe("cli", () => { + describe("getBotPath", () => { + const defCommand = ["yarn", "start"]; + it("should return the default path", () => { + const path = getBotPath({ cliCommand: defCommand }); + expect(path).toEqual(BotPaths.BOTH); + }); + it("should return the claimer path", () => { + const command = ["yarn", "start", "--path=claimer"]; + const path = getBotPath({ cliCommand: command }); + expect(path).toEqual(BotPaths.CLAIMER); + }); + it("should return the challenger path", () => { + const command = ["yarn", "start", "--path=challenger"]; + const path = getBotPath({ cliCommand: command }); + expect(path).toEqual(BotPaths.CHALLENGER); + }); + it("should throw an error for invalid path", () => { + const command = ["yarn", "start", "--path=invalid"]; + expect(() => getBotPath({ cliCommand: command })).toThrow(new InvalidBotPathError()); + }); + }); +}); diff --git a/validator-cli/src/utils/cli.ts b/validator-cli/src/utils/cli.ts index 83ca4d98..e869beed 100644 --- a/validator-cli/src/utils/cli.ts +++ b/validator-cli/src/utils/cli.ts @@ -1,16 +1,22 @@ +import { InvalidBotPathError } from "./errors"; export enum BotPaths { CLAIMER = 0, // happy path CHALLENGER = 1, // unhappy path BOTH = 2, // both happy and unhappy path } +interface BotPathParams { + cliCommand: string[]; + defaultPath?: BotPaths; +} + /** * Get the bot path from the command line arguments * @param defaultPath - default path to use if not specified in the command line arguments * @returns BotPaths - the bot path (BotPaths) */ -export function getBotPath(defaultPath: number = BotPaths.BOTH): number { - const args = process.argv.slice(2); +export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathParams): number { + const args = cliCommand.slice(2); const pathFlag = args.find((arg) => arg.startsWith("--path=")); const path = pathFlag ? pathFlag.split("=")[1] : null; @@ -22,8 +28,7 @@ export function getBotPath(defaultPath: number = BotPaths.BOTH): number { }; if (path && !(path in pathMapping)) { - console.error(`Error: Invalid path '${path}'. Use one of: ${Object.keys(pathMapping).join(", ")}.`); - process.exit(1); + throw new InvalidBotPathError(); } return path ? pathMapping[path] : defaultPath; diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index 84c2a7ee..ece7799a 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -22,7 +22,8 @@ export const watch = async ( emitter: typeof defaultEmitter = defaultEmitter ) => { initializeLogger(emitter); - const path = getBotPath(); + const cliCommand = process.argv; + const path = getBotPath({ cliCommand }); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); emitter.emit(BotEvents.STARTED, chainId, path); From e3671733c2ea65e7151816fada8f5ad2342bf169 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 22 Jan 2025 18:37:58 +0530 Subject: [PATCH 05/33] feat: increased test coverage --- validator-cli/src/utils/claim.ts | 2 +- validator-cli/src/utils/errors.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index b988a7c5..3a76e268 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -141,4 +141,4 @@ const hashClaim = (claim: ClaimStruct) => { ); }; -export { getClaim, hashClaim, getClaimResolveState }; +export { getClaim, hashClaim, getClaimResolveState, ClaimHonestState }; diff --git a/validator-cli/src/utils/errors.ts b/validator-cli/src/utils/errors.ts index 7d4256e5..79566328 100644 --- a/validator-cli/src/utils/errors.ts +++ b/validator-cli/src/utils/errors.ts @@ -1,3 +1,4 @@ +import { BotPaths } from "./cli"; class ClaimNotFoundError extends Error { constructor(epoch: number) { super(); @@ -22,4 +23,12 @@ class TransactionHandlerNotDefinedError extends Error { } } -export { ClaimNotFoundError, ClaimNotSetError, TransactionHandlerNotDefinedError }; +class InvalidBotPathError extends Error { + constructor() { + super(); + this.name = "InvalidBotPath"; + this.message = `Invalid path provided, Use one of: ${Object.keys(BotPaths).join("), ")}`; + } +} + +export { ClaimNotFoundError, ClaimNotSetError, TransactionHandlerNotDefinedError, InvalidBotPathError }; From c488c4aced2cf6b817fcd5b837512e02ab405ec1 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 5 Feb 2025 09:54:48 +0530 Subject: [PATCH 06/33] chore: outscoped claim fetch --- validator-cli/src/ArbToEth/claimer.test.ts | 34 +++++++------------- validator-cli/src/ArbToEth/claimer.ts | 8 ++--- validator-cli/src/ArbToEth/validator.test.ts | 22 +++---------- validator-cli/src/ArbToEth/validator.ts | 13 ++++---- validator-cli/src/watcher.ts | 22 +++++++------ 5 files changed, 39 insertions(+), 60 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.test.ts b/validator-cli/src/ArbToEth/claimer.test.ts index 7392d207..c0aea9a4 100644 --- a/validator-cli/src/ArbToEth/claimer.test.ts +++ b/validator-cli/src/ArbToEth/claimer.test.ts @@ -1,15 +1,13 @@ import { ethers } from "ethers"; import { checkAndClaim } from "./claimer"; -import { ArbToEthTransactionHandler } from "./transactionHandler"; import { ClaimHonestState } from "../utils/claim"; -import { start } from "pm2"; + describe("claimer", () => { let veaOutbox: any; let veaInbox: any; let veaInboxProvider: any; let veaOutboxProvider: any; let emitter: any; - let mockGetClaim: any; let mockClaim: any; let mockGetLatestClaimedEpoch: any; let mockDeps: any; @@ -31,15 +29,15 @@ describe("claimer", () => { stateRoot: jest.fn().mockResolvedValue(mockClaim.stateRoot), }; veaOutboxProvider = { - getBlock: jest.fn().mockResolvedValue({ number: 0, timestamp: 100 }), + getBlock: jest.fn().mockResolvedValue({ number: 0, timestamp: 110 }), }; emitter = { emit: jest.fn(), }; - mockGetClaim = jest.fn(); mockGetLatestClaimedEpoch = jest.fn(); mockDeps = { + claim: mockClaim, epoch: 10, epochPeriod: 10, veaInbox, @@ -48,7 +46,6 @@ describe("claimer", () => { veaOutbox, transactionHandler: null, emitter, - fetchClaim: mockGetClaim, fetchLatestClaimedEpoch: mockGetLatestClaimedEpoch, }; }); @@ -90,36 +87,34 @@ describe("claimer", () => { }; }); it("should return null if no claim is made for a passed epoch", async () => { - mockGetClaim = jest.fn().mockReturnValue(null); mockDeps.epoch = 7; // claimable epoch - 3 - mockDeps.fetchClaim = mockGetClaim; + mockDeps.claim = null; const result = await checkAndClaim(mockDeps); expect(result).toBeNull(); }); it("should return null if no snapshot is saved on the inbox for a claimable epoch", async () => { - mockGetClaim = jest.fn().mockReturnValue(null); veaInbox.snapshots = jest.fn().mockResolvedValue(ethers.ZeroHash); mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ challenged: false, stateroot: "0x1111", }); + mockDeps.claim = null; mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; const result = await checkAndClaim(mockDeps); expect(result).toBeNull(); }); it("should return null if there are no new messages in the inbox", async () => { - mockGetClaim = jest.fn().mockReturnValue(null); veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot); mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ challenged: false, stateroot: "0x1111", }); + mockDeps.claim = null; mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; const result = await checkAndClaim(mockDeps); expect(result).toBeNull(); }); - it("should make a valid calim if no claim is made", async () => { - mockGetClaim = jest.fn().mockReturnValue(null); + it("should make a valid claim if no claim is made", async () => { veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890"); mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ challenged: false, @@ -127,13 +122,12 @@ describe("claimer", () => { }); mockDeps.transactionHandler = mockTransactionHandler; mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; - mockDeps.fetchClaim = mockGetClaim; + mockDeps.claim = null; mockDeps.veaInbox = veaInbox; const result = await checkAndClaim(mockDeps); expect(result.transactions.claimTxn).toBe(mockTransactions.claimTxn); }); - it("should make a valid calim if last claim was challenged", async () => { - mockGetClaim = jest.fn().mockReturnValue(null); + it("should make a valid claim if last claim was challenged", async () => { veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot); mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ challenged: true, @@ -141,7 +135,7 @@ describe("claimer", () => { }); mockDeps.transactionHandler = mockTransactionHandler; mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; - mockDeps.fetchClaim = mockGetClaim; + mockDeps.claim = null; mockDeps.veaInbox = veaInbox; const result = await checkAndClaim(mockDeps); expect(result.transactions.claimTxn).toEqual(mockTransactions.claimTxn); @@ -149,17 +143,12 @@ describe("claimer", () => { it("should withdraw claim deposit if claimer is honest", async () => { mockDeps.transactionHandler = mockTransactionHandler; mockClaim.honest = ClaimHonestState.CLAIMER; - - mockGetClaim = jest.fn().mockResolvedValue(mockClaim); - mockDeps.fetchClaim = mockGetClaim; const result = await checkAndClaim(mockDeps); expect(result.transactions.withdrawClaimDepositTxn).toEqual(mockTransactions.withdrawClaimDepositTxn); }); it("should start verification if verification is not started", async () => { mockDeps.transactionHandler = mockTransactionHandler; mockClaim.honest = ClaimHonestState.NONE; - mockGetClaim = jest.fn().mockResolvedValue(mockClaim); - mockDeps.fetchClaim = mockGetClaim; const result = await checkAndClaim(mockDeps); expect(result.transactions.startVerificationTxn).toEqual(mockTransactions.startVerificationTxn); }); @@ -167,8 +156,7 @@ describe("claimer", () => { mockDeps.transactionHandler = mockTransactionHandler; mockClaim.honest = ClaimHonestState.NONE; mockClaim.timestampVerification = 1234; - mockGetClaim = jest.fn().mockResolvedValue(mockClaim); - mockDeps.fetchClaim = mockGetClaim; + mockDeps.claim = mockClaim; const result = await checkAndClaim(mockDeps); expect(result.transactions.verifySnapshotTxn).toEqual(mockTransactions.verifySnapshotTxn); }); diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index b6836b76..2810a039 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -5,7 +5,9 @@ import { getClaim, ClaimHonestState } from "../utils/claim"; import { getLastClaimedEpoch } from "../utils/graphQueries"; import { ArbToEthTransactionHandler } from "./transactionHandler"; import { BotEvents } from "../utils/botEvents"; +import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; interface checkAndClaimParams { + claim: ClaimStruct | null; epochPeriod: number; epoch: number; veaInbox: any; @@ -19,6 +21,7 @@ interface checkAndClaimParams { } export async function checkAndClaim({ + claim, epoch, epochPeriod, veaInbox, @@ -27,13 +30,11 @@ export async function checkAndClaim({ veaOutboxProvider, transactionHandler, emitter, - fetchClaim = getClaim, fetchLatestClaimedEpoch = getLastClaimedEpoch, }: checkAndClaimParams) { let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); - const claimAbleEpoch = finalizedOutboxBlock.timestamp / epochPeriod; - const claim = await fetchClaim(veaOutbox, veaOutboxProvider, epoch, finalizedOutboxBlock.number, "finalized"); + const claimAbleEpoch = Math.floor(finalizedOutboxBlock.timestamp / epochPeriod) - 1; if (!transactionHandler) { transactionHandler = new ArbToEthTransactionHandler( epoch, @@ -51,7 +52,6 @@ export async function checkAndClaim({ const [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; const lastClaimChallenged: boolean = claimData.challenged && savedSnapshot == outboxStateRoot; - if (newMessagesToBridge || lastClaimChallenged) { await transactionHandler.makeClaim(savedSnapshot); return transactionHandler; diff --git a/validator-cli/src/ArbToEth/validator.test.ts b/validator-cli/src/ArbToEth/validator.test.ts index 21ccb8ca..253831c6 100644 --- a/validator-cli/src/ArbToEth/validator.test.ts +++ b/validator-cli/src/ArbToEth/validator.test.ts @@ -8,7 +8,6 @@ describe("validator", () => { let veaInboxProvider: any; let veaOutboxProvider: any; let emitter: any; - let mockGetClaim: any; let mockClaim: any; let mockGetClaimState: any; let mockGetBlockFinality: any; @@ -39,9 +38,9 @@ describe("validator", () => { honest: 0, challenger: ethers.ZeroAddress, }; - mockGetClaim = jest.fn(); mockGetBlockFinality = jest.fn().mockResolvedValue([{ number: 0 }, { number: 0, timestamp: 100 }, false]); mockDeps = { + claim: mockClaim, epoch: 0, epochPeriod: 10, veaInbox, @@ -50,15 +49,16 @@ describe("validator", () => { veaOutbox, transactionHandler: null, emitter, - fetchClaim: mockGetClaim, fetchClaimResolveState: mockGetClaimState, fetchBlocksAndCheckFinality: mockGetBlockFinality, }; }); + afterEach(() => { + jest.clearAllMocks(); + }); describe("challengeAndResolveClaim", () => { it("should return null if no claim is made", async () => { - mockGetClaim = jest.fn().mockReturnValue(null); - mockDeps.fetchClaim = mockGetClaim; + mockDeps.claim = null; const result = await challengeAndResolveClaim(mockDeps); expect(result).toBeNull(); @@ -77,27 +77,20 @@ describe("validator", () => { }, }; veaInbox.snapshots = jest.fn().mockReturnValue("0x321"); - mockGetClaim = jest.fn().mockReturnValue(mockClaim); - mockDeps.transactionHandler = mockTransactionHandler; - mockDeps.fetchClaim = mockGetClaim; const updatedTransactionHandler = await challengeAndResolveClaim(mockDeps); expect(updatedTransactionHandler.transactions.challengeTxn).toBe(challengeTxn); expect(mockTransactionHandler.challengeClaim).toHaveBeenCalled(); }); it("should not challenge if claim is valid", async () => { - mockClaim.challenger = mockClaim.claimer; - mockGetClaim = jest.fn().mockReturnValue(mockClaim); veaInbox.snapshots = jest.fn().mockReturnValue(mockClaim.stateRoot); - mockDeps.fetchClaim = mockGetClaim; const updatedTransactionHandler = await challengeAndResolveClaim(mockDeps); expect(updatedTransactionHandler).toBeNull(); }); it("send snapshot if snapshot not sent", async () => { mockClaim.challenger = mockClaim.claimer; - mockGetClaim = jest.fn().mockReturnValue(mockClaim); mockGetClaimState = jest .fn() .mockReturnValue({ sendSnapshot: { status: false, txnHash: "" }, execution: { status: 0, txnHash: "" } }); @@ -113,7 +106,6 @@ describe("validator", () => { }; mockDeps.transactionHandler = mockTransactionHandler; mockDeps.fetchClaimResolveState = mockGetClaimState; - mockDeps.fetchClaim = mockGetClaim; const updatedTransactionHandler = await challengeAndResolveClaim(mockDeps); expect(updatedTransactionHandler.transactions.sendSnapshotTxn).toEqual("0x123"); expect(mockTransactionHandler.sendSnapshot).toHaveBeenCalled(); @@ -122,7 +114,6 @@ describe("validator", () => { it("resolve challenged claim if snapshot sent but not executed", async () => { mockClaim.challenger = mockClaim.claimer; - mockGetClaim = jest.fn().mockReturnValue(mockClaim); mockGetClaimState = jest .fn() .mockReturnValue({ sendSnapshot: { status: true, txnHash: "0x123" }, execution: { status: 1, txnHash: "" } }); @@ -138,7 +129,6 @@ describe("validator", () => { }; mockDeps.transactionHandler = mockTransactionHandler; mockDeps.fetchClaimResolveState = mockGetClaimState; - mockDeps.fetchClaim = mockGetClaim; const updatedTransactionHandler = await challengeAndResolveClaim(mockDeps); expect(updatedTransactionHandler.transactions.executeSnapshotTxn).toEqual("0x123"); expect(mockTransactionHandler.resolveChallengedClaim).toHaveBeenCalled(); @@ -147,7 +137,6 @@ describe("validator", () => { it("withdraw challenge deposit if snapshot sent and executed", async () => { mockClaim.challenger = mockClaim.claimer; - mockGetClaim = jest.fn().mockReturnValue(mockClaim); mockGetClaimState = jest.fn().mockReturnValue({ sendSnapshot: { status: true, txnHash: "0x123" }, execution: { status: 2, txnHash: "0x321" }, @@ -164,7 +153,6 @@ describe("validator", () => { }; mockDeps.transactionHandler = mockTransactionHandler; mockDeps.fetchClaimResolveState = mockGetClaimState; - mockDeps.fetchClaim = mockGetClaim; const updatedTransactionHandler = await challengeAndResolveClaim(mockDeps); expect(updatedTransactionHandler.transactions.withdrawChallengeDepositTxn).toEqual("0x1234"); expect(mockTransactionHandler.withdrawChallengeDeposit).toHaveBeenCalled(); diff --git a/validator-cli/src/ArbToEth/validator.ts b/validator-cli/src/ArbToEth/validator.ts index f5fef06b..c3b76dcb 100644 --- a/validator-cli/src/ArbToEth/validator.ts +++ b/validator-cli/src/ArbToEth/validator.ts @@ -6,11 +6,13 @@ import { getClaim, getClaimResolveState } from "../utils/claim"; import { defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { getBlocksAndCheckFinality } from "../utils/arbToEthState"; +import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; // https://github.com/prysmaticlabs/prysm/blob/493905ee9e33a64293b66823e69704f012b39627/config/params/mainnet_config.go#L103 const secondsPerSlotEth = 12; export interface ChallengeAndResolveClaimParams { + claim: ClaimStruct; epoch: number; epochPeriod: number; veaInbox: any; @@ -25,6 +27,7 @@ export interface ChallengeAndResolveClaimParams { } export async function challengeAndResolveClaim({ + claim, epoch, epochPeriod, veaInbox, @@ -33,10 +36,13 @@ export async function challengeAndResolveClaim({ veaOutbox, transactionHandler, emitter = defaultEmitter, - fetchClaim = getClaim, fetchClaimResolveState = getClaimResolveState, fetchBlocksAndCheckFinality = getBlocksAndCheckFinality, }: ChallengeAndResolveClaimParams): Promise { + if (!claim) { + emitter.emit(BotEvents.NO_CLAIM, epoch); + return null; + } const [arbitrumBlock, ethFinalizedBlock, finalityIssueFlagEth] = await fetchBlocksAndCheckFinality( veaOutboxProvider, veaInboxProvider, @@ -53,11 +59,6 @@ export async function challengeAndResolveClaim({ blockNumberOutboxLowerBound = ethFinalizedBlock.number - Math.ceil(epochPeriod / secondsPerSlotEth); } const ethBlockTag = finalityIssueFlagEth ? "finalized" : "latest"; - const claim = await fetchClaim(veaOutbox, veaOutboxProvider, epoch, blockNumberOutboxLowerBound, ethBlockTag); - if (!claim) { - emitter.emit(BotEvents.NO_CLAIM, epoch); - return null; - } if (!transactionHandler) { transactionHandler = new ArbToEthTransactionHandler( epoch, diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index ece7799a..8165e665 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -1,13 +1,14 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { getBridgeConfig, Bridge } from "./consts/bridgeRoutes"; import { getVeaInbox, getVeaOutbox, getTransactionHandler } from "./utils/ethers"; -import { setEpochRange, getLatestChallengeableEpoch } from "./utils/epochHandler"; +import { getBlockFromEpoch, setEpochRange } from "./utils/epochHandler"; import { getClaimValidator, getClaimer } from "./utils/ethers"; import { defaultEmitter } from "./utils/emitter"; import { BotEvents } from "./utils/botEvents"; import { initialize as initializeLogger } from "./utils/logger"; import { ShutdownSignal } from "./utils/shutdown"; import { getBotPath, BotPaths } from "./utils/cli"; +import { getClaim } from "./utils/claim"; /** * @file This file contains the logic for watching a bridge and validating/resolving for claims. @@ -26,7 +27,6 @@ export const watch = async ( const path = getBotPath({ cliCommand }); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); emitter.emit(BotEvents.STARTED, chainId, path); - const veaBridge: Bridge = getBridgeConfig(chainId); const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); @@ -39,14 +39,16 @@ export const watch = async ( let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); const transactionHandlers: { [epoch: number]: InstanceType } = {}; const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); - - let latestEpoch = getLatestChallengeableEpoch(chainId); + let latestEpoch = epochRange[epochRange.length - 1]; while (!shutDownSignal.getIsShutdownSignal()) { let i = 0; while (i < epochRange.length) { const epoch = epochRange[i]; emitter.emit(BotEvents.CHECKING, epoch); + const epochBlock = await getBlockFromEpoch(epoch, veaBridge.epochPeriod, veaOutboxProvider); + const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); const checkAndChallengeResolveDeps = { + claim, epoch, epochPeriod: veaBridge.epochPeriod, veaInbox, @@ -57,7 +59,7 @@ export const watch = async ( emitter, }; let updatedTransactions; - if (path > BotPaths.CLAIMER) { + if (path > BotPaths.CLAIMER && claim != null) { updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); } if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { @@ -66,17 +68,17 @@ export const watch = async ( if (updatedTransactions) { transactionHandlers[epoch] = updatedTransactions; - } else { + } else if (epoch != latestEpoch) { delete transactionHandlers[epoch]; epochRange.splice(i, 1); continue; } i++; } - const newEpoch = getLatestChallengeableEpoch(chainId); - if (newEpoch > latestEpoch) { - epochRange.push(newEpoch); - latestEpoch = newEpoch; + const newVerifiableEpoch = Math.floor(Date.now() / (1000 * veaBridge.epochPeriod)) - 1; + if (newVerifiableEpoch > latestEpoch) { + epochRange.push(newVerifiableEpoch); + latestEpoch = newVerifiableEpoch; } else { emitter.emit(BotEvents.WAITING, latestEpoch); } From a91a9c67573344f7fee60ace5defbaa10af651be Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 5 Feb 2025 09:56:31 +0530 Subject: [PATCH 07/33] fix: verifySnapshot time flag --- .../src/ArbToEth/transactionHandler.test.ts | 155 ++++++++++----- .../src/ArbToEth/transactionHandler.ts | 184 ++++++++++++------ 2 files changed, 235 insertions(+), 104 deletions(-) diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index 2b29a7a8..ffcdf735 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -1,4 +1,10 @@ -import { ArbToEthTransactionHandler, ContractType } from "./transactionHandler"; +import { + ArbToEthTransactionHandler, + ContractType, + Transaction, + MAX_PENDING_CONFIRMATIONS, + MAX_PENDING_TIME, +} from "./transactionHandler"; import { MockEmitter, defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { ClaimNotSetError } from "../utils/errors"; @@ -81,6 +87,7 @@ describe("ArbToEthTransactionHandler", () => { let transactionHandler: ArbToEthTransactionHandler; let finalityBlock: number = 100; const mockEmitter = new MockEmitter(); + let mockBroadcastedTimestamp: number = 1000; beforeEach(() => { transactionHandler = new ArbToEthTransactionHandler( epoch, @@ -96,45 +103,54 @@ describe("ArbToEthTransactionHandler", () => { it("should return 2 if transaction is not final", async () => { jest.spyOn(mockEmitter, "emit"); veaInboxProvider.getTransactionReceipt.mockResolvedValue({ - blockNumber: finalityBlock - (transactionHandler.requiredConfirmations - 1), + blockNumber: finalityBlock - (MAX_PENDING_CONFIRMATIONS - 1), }); - const trnxHash = "0x123456"; - const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); - expect(status).toEqual(2); - expect(mockEmitter.emit).toHaveBeenCalledWith( - BotEvents.TXN_NOT_FINAL, - trnxHash, - transactionHandler.requiredConfirmations - 1 + const trnx: Transaction = { hash: "0x123456", broadcastedTimestamp: mockBroadcastedTimestamp }; + const status = await transactionHandler.checkTransactionStatus( + trnx, + ContractType.INBOX, + mockBroadcastedTimestamp + 1 ); + expect(status).toEqual(2); + expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_NOT_FINAL, trnx.hash, MAX_PENDING_CONFIRMATIONS - 1); }); it("should return 1 if transaction is pending", async () => { jest.spyOn(mockEmitter, "emit"); veaInboxProvider.getTransactionReceipt.mockResolvedValue(null); - const trnxHash = "0x123456"; - const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); + const trnx: Transaction = { hash: "0x123456", broadcastedTimestamp: mockBroadcastedTimestamp }; + const status = await transactionHandler.checkTransactionStatus( + trnx, + ContractType.INBOX, + mockBroadcastedTimestamp + 1 + ); expect(status).toEqual(1); - expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_PENDING, trnxHash); + expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_PENDING, trnx.hash); }); it("should return 3 if transaction is final", async () => { jest.spyOn(mockEmitter, "emit"); veaInboxProvider.getTransactionReceipt.mockResolvedValue({ - blockNumber: finalityBlock - transactionHandler.requiredConfirmations, + blockNumber: finalityBlock - MAX_PENDING_CONFIRMATIONS, }); - const trnxHash = "0x123456"; - const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); - expect(status).toEqual(3); - expect(mockEmitter.emit).toHaveBeenCalledWith( - BotEvents.TXN_FINAL, - trnxHash, - transactionHandler.requiredConfirmations + const trnx: Transaction = { hash: "0x123456", broadcastedTimestamp: mockBroadcastedTimestamp }; + + const status = await transactionHandler.checkTransactionStatus( + trnx, + ContractType.INBOX, + mockBroadcastedTimestamp + 1 ); + expect(status).toEqual(3); + expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_FINAL, trnx.hash, MAX_PENDING_CONFIRMATIONS); }); it("should return 0 if transaction hash is null", async () => { - const trnxHash = null; - const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); + const trnx = null; + const status = await transactionHandler.checkTransactionStatus( + trnx, + ContractType.INBOX, + mockBroadcastedTimestamp + ); expect(status).toEqual(0); }); }); @@ -169,7 +185,10 @@ describe("ArbToEthTransactionHandler", () => { gasLimit: BigInt(100000), value: deposit, }); - expect(transactionHandler.transactions.claimTxn).toEqual("0x1234"); + expect(transactionHandler.transactions.claimTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not make a claim if a claim transaction is pending", async () => { @@ -213,7 +232,10 @@ describe("ArbToEthTransactionHandler", () => { veaOutbox["startVerification(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"].estimateGas ).toHaveBeenCalledWith(epoch, claim); expect(veaOutbox.startVerification).toHaveBeenCalledWith(epoch, claim, { gasLimit: BigInt(100000) }); - expect(transactionHandler.transactions.startVerificationTxn).toEqual("0x1234"); + expect(transactionHandler.transactions.startVerificationTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not start verification if a startVerification transaction is pending", async () => { @@ -255,7 +277,7 @@ describe("ArbToEthTransactionHandler", () => { veaOutboxProvider, mockEmitter ); - verificationFlipTime = Number(claim.timestampClaimed) + getBridgeConfig(chainId).minChallengePeriod; + verificationFlipTime = Number(claim.timestampVerification) + getBridgeConfig(chainId).minChallengePeriod; transactionHandler.claim = claim; }); @@ -268,7 +290,10 @@ describe("ArbToEthTransactionHandler", () => { veaOutbox["verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"].estimateGas ).toHaveBeenCalledWith(epoch, claim); expect(veaOutbox.verifySnapshot).toHaveBeenCalledWith(epoch, claim, { gasLimit: BigInt(100000) }); - expect(transactionHandler.transactions.verifySnapshotTxn).toEqual("0x1234"); + expect(transactionHandler.transactions.verifySnapshotTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not verify snapshot if a verifySnapshot transaction is pending", async () => { @@ -317,17 +342,25 @@ describe("ArbToEthTransactionHandler", () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); veaOutbox.withdrawClaimDeposit.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.withdrawClaimDeposit(); - expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.OUTBOX); - expect(transactionHandler.transactions.withdrawClaimDepositTxn).toEqual("0x1234"); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( + null, + ContractType.OUTBOX, + expect.any(Number) + ); + expect(transactionHandler.transactions.withdrawClaimDepositTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not withdraw deposit if txn is pending", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); - transactionHandler.transactions.withdrawClaimDepositTxn = "0x1234"; + transactionHandler.transactions.withdrawClaimDepositTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; await transactionHandler.withdrawClaimDeposit(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( transactionHandler.transactions.withdrawClaimDepositTxn, - ContractType.OUTBOX + ContractType.OUTBOX, + expect.any(Number) ); }); @@ -362,11 +395,12 @@ describe("ArbToEthTransactionHandler", () => { it("should not challenge claim if txn is pending", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); - transactionHandler.transactions.challengeTxn = "0x1234"; + transactionHandler.transactions.challengeTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; await transactionHandler.challengeClaim(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( transactionHandler.transactions.challengeTxn, - ContractType.OUTBOX + ContractType.OUTBOX, + expect.any(Number) ); expect( veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] @@ -379,8 +413,15 @@ describe("ArbToEthTransactionHandler", () => { (mockChallenge as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockChallenge; await transactionHandler.challengeClaim(); - expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.OUTBOX); - expect(transactionHandler.transactions.challengeTxn).toEqual("0x1234"); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( + null, + ContractType.OUTBOX, + expect.any(Number) + ); + expect(transactionHandler.transactions.challengeTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it.todo("should set challengeTxn as completed when txn is final"); @@ -406,17 +447,25 @@ describe("ArbToEthTransactionHandler", () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); veaOutbox.withdrawChallengeDeposit.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.withdrawChallengeDeposit(); - expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.OUTBOX); - expect(transactionHandler.transactions.withdrawChallengeDepositTxn).toEqual("0x1234"); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( + null, + ContractType.OUTBOX, + expect.any(Number) + ); + expect(transactionHandler.transactions.withdrawChallengeDepositTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not withdraw deposit if txn is pending", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); - transactionHandler.transactions.withdrawChallengeDepositTxn = "0x1234"; + transactionHandler.transactions.withdrawChallengeDepositTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; await transactionHandler.withdrawChallengeDeposit(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( transactionHandler.transactions.withdrawChallengeDepositTxn, - ContractType.OUTBOX + ContractType.OUTBOX, + expect.any(Number) ); }); @@ -451,17 +500,25 @@ describe("ArbToEthTransactionHandler", () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); veaInbox.sendSnapshot.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.sendSnapshot(); - expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.INBOX); - expect(transactionHandler.transactions.sendSnapshotTxn).toEqual("0x1234"); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( + null, + ContractType.INBOX, + expect.any(Number) + ); + expect(transactionHandler.transactions.sendSnapshotTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not send snapshot if txn is pending", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); - transactionHandler.transactions.sendSnapshotTxn = "0x1234"; + transactionHandler.transactions.sendSnapshotTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; await transactionHandler.sendSnapshot(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( transactionHandler.transactions.sendSnapshotTxn, - ContractType.INBOX + ContractType.INBOX, + expect.any(Number) ); expect(veaInbox.sendSnapshot).not.toHaveBeenCalled(); }); @@ -491,22 +548,26 @@ describe("ArbToEthTransactionHandler", () => { }); it("should resolve challenged claim", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); - transactionHandler.transactions.sendSnapshotTxn = "0x1234"; + transactionHandler.transactions.sendSnapshotTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; mockMessageExecutor.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.resolveChallengedClaim( - transactionHandler.transactions.sendSnapshotTxn, + transactionHandler.transactions.sendSnapshotTxn.hash, mockMessageExecutor ); - expect(transactionHandler.transactions.executeSnapshotTxn).toEqual("0x1234"); + expect(transactionHandler.transactions.executeSnapshotTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); }); it("should not resolve challenged claim if txn is pending", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); - transactionHandler.transactions.executeSnapshotTxn = "0x1234"; + transactionHandler.transactions.executeSnapshotTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; await transactionHandler.resolveChallengedClaim(mockMessageExecutor); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( transactionHandler.transactions.executeSnapshotTxn, - ContractType.OUTBOX + ContractType.OUTBOX, + expect.any(Number) ); }); }); diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index 1a6846b7..664e197e 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -20,15 +20,20 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; * executeSnapshot() - Execute a sent snapshot to resolve dispute in VeaOutbox (ETH). */ +export type Transaction = { + hash: string; + broadcastedTimestamp: number; +}; + type Transactions = { - claimTxn: string | null; - withdrawClaimDepositTxn: string | null; - startVerificationTxn: string | null; - verifySnapshotTxn: string | null; - challengeTxn: string | null; - withdrawChallengeDepositTxn: string | null; - sendSnapshotTxn: string | null; - executeSnapshotTxn: string | null; + claimTxn: Transaction | null; + withdrawClaimDepositTxn: Transaction | null; + startVerificationTxn: Transaction | null; + verifySnapshotTxn: Transaction | null; + challengeTxn: Transaction | null; + withdrawChallengeDepositTxn: Transaction | null; + sendSnapshotTxn: Transaction | null; + executeSnapshotTxn: Transaction | null; }; enum TransactionStatus { @@ -36,6 +41,7 @@ enum TransactionStatus { PENDING = 1, NOT_FINAL = 2, FINAL = 3, + EXPIRED = 4, } export enum ContractType { @@ -43,10 +49,12 @@ export enum ContractType { OUTBOX = "outbox", } +export const MAX_PENDING_TIME = 5 * 60 * 1000; // 3 minutes +export const MAX_PENDING_CONFIRMATIONS = 10; +const CHAIN_ID = 11155111; + export class ArbToEthTransactionHandler { - public requiredConfirmations = 10; public claim: ClaimStruct | null = null; - public chainId = 11155111; public veaInbox: VeaInboxArbToEth; public veaOutbox: VeaOutboxArbToEth; @@ -92,33 +100,35 @@ export class ArbToEthTransactionHandler { * * @returns TransactionStatus. */ - public async checkTransactionStatus(trnxHash: string | null, contract: ContractType): Promise { - let provider: JsonRpcProvider; - if (contract === ContractType.INBOX) { - provider = this.veaInboxProvider; - } else if (contract === ContractType.OUTBOX) { - provider = this.veaOutboxProvider; - } - - if (trnxHash == null) { + public async checkTransactionStatus( + trnx: Transaction | null, + contract: ContractType, + currentTime: number + ): Promise { + const provider = contract === ContractType.INBOX ? this.veaInboxProvider : this.veaOutboxProvider; + if (trnx == null) { return TransactionStatus.NOT_MADE; } - const receipt = await provider.getTransactionReceipt(trnxHash); + const receipt = await provider.getTransactionReceipt(trnx.hash); if (!receipt) { - this.emitter.emit(BotEvents.TXN_PENDING, trnxHash); + this.emitter.emit(BotEvents.TXN_PENDING, trnx.hash); + if (currentTime - trnx.broadcastedTimestamp > MAX_PENDING_TIME) { + this.emitter.emit(BotEvents.TXN_EXPIRED, trnx.hash); + return TransactionStatus.EXPIRED; + } return TransactionStatus.PENDING; } const currentBlock = await provider.getBlock("latest"); const confirmations = currentBlock.number - receipt.blockNumber; - if (confirmations >= this.requiredConfirmations) { - this.emitter.emit(BotEvents.TXN_FINAL, trnxHash, confirmations); + if (confirmations >= MAX_PENDING_CONFIRMATIONS) { + this.emitter.emit(BotEvents.TXN_FINAL, trnx.hash, confirmations); return TransactionStatus.FINAL; } - this.emitter.emit(BotEvents.TXN_NOT_FINAL, trnxHash, confirmations); + this.emitter.emit(BotEvents.TXN_NOT_FINAL, trnx.hash, confirmations); return TransactionStatus.NOT_FINAL; } @@ -129,10 +139,16 @@ export class ArbToEthTransactionHandler { */ public async makeClaim(stateRoot: string) { this.emitter.emit(BotEvents.CLAIMING, this.epoch); - if ((await this.checkTransactionStatus(this.transactions.claimTxn, ContractType.OUTBOX)) > 0) { + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.claimTxn, + ContractType.OUTBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } - const { deposit } = getBridgeConfig(this.chainId); + const { deposit } = getBridgeConfig(CHAIN_ID); const estimateGas = await this.veaOutbox["claim(uint256,bytes32)"].estimateGas(this.epoch, stateRoot, { value: deposit, @@ -141,8 +157,11 @@ export class ArbToEthTransactionHandler { value: deposit, gasLimit: estimateGas, }); - this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Claim"); - this.transactions.claimTxn = claimTransaction.hash; + this.emitter.emit(BotEvents.TXN_MADE, claimTransaction.hash, this.epoch, "Claim"); + this.transactions.claimTxn = { + hash: claimTransaction.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -153,11 +172,17 @@ export class ArbToEthTransactionHandler { if (this.claim == null) { throw new ClaimNotSetError(); } - if ((await this.checkTransactionStatus(this.transactions.startVerificationTxn, ContractType.OUTBOX)) > 0) { + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.startVerificationTxn, + ContractType.OUTBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } - const bridgeConfig = getBridgeConfig(this.chainId); + const bridgeConfig = getBridgeConfig(CHAIN_ID); const timeOver = currentTimestamp - Number(this.claim.timestampClaimed) - @@ -165,15 +190,18 @@ export class ArbToEthTransactionHandler { bridgeConfig.epochPeriod; if (timeOver < 0) { - this.emitter.emit(BotEvents.VERIFICATION_CANT_START, -1 * timeOver); + this.emitter.emit(BotEvents.VERIFICATION_CANT_START, this.epoch, -1 * timeOver); return; } const estimateGas = await this.veaOutbox[ "startVerification(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))" ].estimateGas(this.epoch, this.claim); const startVerifTrx = await this.veaOutbox.startVerification(this.epoch, this.claim, { gasLimit: estimateGas }); - this.emitter.emit(BotEvents.TXN_MADE, this.epoch, startVerifTrx.hash, "Start Verification"); - this.transactions.startVerificationTxn = startVerifTrx.hash; + this.emitter.emit(BotEvents.TXN_MADE, startVerifTrx.hash, this.epoch, "Start Verification"); + this.transactions.startVerificationTxn = { + hash: startVerifTrx.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -184,16 +212,20 @@ export class ArbToEthTransactionHandler { if (this.claim == null) { throw new ClaimNotSetError(); } - if ((await this.checkTransactionStatus(this.transactions.verifySnapshotTxn, ContractType.OUTBOX)) > 0) { + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.verifySnapshotTxn, + ContractType.OUTBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } - const bridgeConfig = getBridgeConfig(this.chainId); - - const timeLeft = currentTimestamp - Number(this.claim.timestampClaimed) - bridgeConfig.minChallengePeriod; - + const bridgeConfig = getBridgeConfig(CHAIN_ID); + const timeLeft = currentTimestamp - Number(this.claim.timestampVerification) - bridgeConfig.minChallengePeriod; // Claim not resolved yet, check if we can verifySnapshot if (timeLeft < 0) { - this.emitter.emit(BotEvents.CANT_VERIFY_SNAPSHOT, -1 * timeLeft); + this.emitter.emit(BotEvents.CANT_VERIFY_SNAPSHOT, this.epoch, -1 * timeLeft); return; } // Estimate gas for verifySnapshot @@ -203,8 +235,11 @@ export class ArbToEthTransactionHandler { const claimTransaction = await this.veaOutbox.verifySnapshot(this.epoch, this.claim, { gasLimit: estimateGas, }); - this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Verify Snapshot"); - this.transactions.verifySnapshotTxn = claimTransaction.hash; + this.emitter.emit(BotEvents.TXN_MADE, claimTransaction.hash, this.epoch, "Verify Snapshot"); + this.transactions.verifySnapshotTxn = { + hash: claimTransaction.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -216,7 +251,13 @@ export class ArbToEthTransactionHandler { if (this.claim == null) { throw new ClaimNotSetError(); } - if ((await this.checkTransactionStatus(this.transactions.withdrawClaimDepositTxn, ContractType.OUTBOX)) > 0) { + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.withdrawClaimDepositTxn, + ContractType.OUTBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } const estimateGas = await this.veaOutbox[ @@ -225,8 +266,11 @@ export class ArbToEthTransactionHandler { const withdrawTxn = await this.veaOutbox.withdrawClaimDeposit(this.epoch, this.claim, { gasLimit: estimateGas, }); - this.emitter.emit(BotEvents.TXN_MADE, this.epoch, withdrawTxn.hash, "Withdraw Deposit"); - this.transactions.withdrawClaimDepositTxn = withdrawTxn.hash; + this.emitter.emit(BotEvents.TXN_MADE, withdrawTxn.hash, this.epoch, "Withdraw Deposit"); + this.transactions.withdrawClaimDepositTxn = { + hash: withdrawTxn.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -238,11 +282,16 @@ export class ArbToEthTransactionHandler { if (!this.claim) { throw new ClaimNotSetError(); } - const transactionStatus = await this.checkTransactionStatus(this.transactions.challengeTxn, ContractType.OUTBOX); - if (transactionStatus > 0) { + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.challengeTxn, + ContractType.OUTBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } - const { deposit } = getBridgeConfig(this.chainId); + const { deposit } = getBridgeConfig(CHAIN_ID); const gasEstimate: bigint = await this.veaOutbox[ "challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))" ].estimateGas(this.epoch, this.claim, { value: deposit }); @@ -265,7 +314,10 @@ export class ArbToEthTransactionHandler { gasLimit: gasEstimate, }); this.emitter.emit(BotEvents.TXN_MADE, challengeTxn.hash, this.epoch, "Challenge"); - this.transactions.challengeTxn = challengeTxn.hash; + this.transactions.challengeTxn = { + hash: challengeTxn.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -277,16 +329,21 @@ export class ArbToEthTransactionHandler { if (!this.claim) { throw new ClaimNotSetError(); } + const currentTime = Date.now(); const transactionStatus = await this.checkTransactionStatus( this.transactions.withdrawChallengeDepositTxn, - ContractType.OUTBOX + ContractType.OUTBOX, + currentTime ); - if (transactionStatus > 0) { + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } const withdrawDepositTxn = await this.veaOutbox.withdrawChallengeDeposit(this.epoch, this.claim); this.emitter.emit(BotEvents.TXN_MADE, withdrawDepositTxn.hash, this.epoch, "Withdraw"); - this.transactions.withdrawChallengeDepositTxn = withdrawDepositTxn.hash; + this.transactions.withdrawChallengeDepositTxn = { + hash: withdrawDepositTxn.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -297,13 +354,21 @@ export class ArbToEthTransactionHandler { if (!this.claim) { throw new ClaimNotSetError(); } - const transactionStatus = await this.checkTransactionStatus(this.transactions.sendSnapshotTxn, ContractType.INBOX); - if (transactionStatus > 0) { + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.sendSnapshotTxn, + ContractType.INBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } const sendSnapshotTxn = await this.veaInbox.sendSnapshot(this.epoch, this.claim); this.emitter.emit(BotEvents.TXN_MADE, sendSnapshotTxn.hash, this.epoch, "Send Snapshot"); - this.transactions.sendSnapshotTxn = sendSnapshotTxn.hash; + this.transactions.sendSnapshotTxn = { + hash: sendSnapshotTxn.hash, + broadcastedTimestamp: currentTime, + }; } /** @@ -311,15 +376,20 @@ export class ArbToEthTransactionHandler { */ public async resolveChallengedClaim(sendSnapshotTxn: string, executeMsg: typeof messageExecutor = messageExecutor) { this.emitter.emit(BotEvents.EXECUTING_SNAPSHOT, this.epoch); + const currentTime = Date.now(); const transactionStatus = await this.checkTransactionStatus( this.transactions.executeSnapshotTxn, - ContractType.OUTBOX + ContractType.OUTBOX, + currentTime ); - if (transactionStatus > 0) { + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } const msgExecuteTrnx = await executeMsg(sendSnapshotTxn, this.veaInboxProvider, this.veaOutboxProvider); this.emitter.emit(BotEvents.TXN_MADE, msgExecuteTrnx.hash, this.epoch, "Execute Snapshot"); - this.transactions.executeSnapshotTxn = msgExecuteTrnx.hash; + this.transactions.executeSnapshotTxn = { + hash: msgExecuteTrnx.hash, + broadcastedTimestamp: currentTime, + }; } } From 4ef27a7acaae2e56318a10b9ed7df40d33e27004 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 5 Feb 2025 09:57:22 +0530 Subject: [PATCH 08/33] fix: happy path logs --- validator-cli/src/utils/botEvents.ts | 1 + validator-cli/src/utils/epochHandler.ts | 13 +++++++++++-- validator-cli/src/utils/logger.ts | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index ba3cb8b6..bd717f1b 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -32,4 +32,5 @@ export enum BotEvents { TXN_PENDING_CONFIRMATIONS = "txn_pending_confirmations", TXN_FINAL = "txn_final", TXN_NOT_FINAL = "txn_not_final", + TXN_EXPIRED = "txn_expired", } diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index 0d57128a..3d9ec386 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -1,3 +1,4 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; import { getBridgeConfig } from "../consts/bridgeRoutes"; /** @@ -30,7 +31,6 @@ const setEpochRange = ( const timeLocal = Math.floor(now / 1000); let veaEpochOutboxClaimableNow = Math.floor(timeLocal / epochPeriod) - 1; - // only past epochs are claimable, hence shift by one here const veaEpochOutboxRange = veaEpochOutboxClaimableNow - veaEpochOutboxWatchLowerBound; const veaEpochOutboxCheckClaimsRangeArray: number[] = new Array(veaEpochOutboxRange) @@ -61,4 +61,13 @@ const getLatestChallengeableEpoch = ( return Math.floor(now / 1000 / epochPeriod) - 2; }; -export { setEpochRange, getLatestChallengeableEpoch }; +const getBlockFromEpoch = async (epoch: number, epochPeriod: number, provider: JsonRpcProvider): Promise => { + const epochTimestamp = epoch * epochPeriod; + const latestBlock = await provider.getBlock("latest"); + const baseBlock = await provider.getBlock(latestBlock.number - 100); + const secPerBlock = (latestBlock.timestamp - baseBlock.timestamp) / (latestBlock.number - baseBlock.number); + const blockFallBack = Math.floor((latestBlock.timestamp - epochTimestamp) / secPerBlock); + return latestBlock.number - blockFallBack; +}; + +export { setEpochRange, getLatestChallengeableEpoch, getBlockFromEpoch }; diff --git a/validator-cli/src/utils/logger.ts b/validator-cli/src/utils/logger.ts index 2a0fc4b4..b2123e3e 100644 --- a/validator-cli/src/utils/logger.ts +++ b/validator-cli/src/utils/logger.ts @@ -64,6 +64,9 @@ export const configurableInitialize = (emitter: EventEmitter) => { emitter.on(BotEvents.TXN_PENDING_CONFIRMATIONS, (transaction: string, confirmations: number) => { console.log(`Transaction(${transaction}) is pending with ${confirmations} confirmations`); }); + emitter.on(BotEvents.TXN_EXPIRED, (transaction: string) => { + console.log(`Transaction(${transaction}) is expired`); + }); // Claim state logs // claim() From 4c7d59a1b2296bd86df6b790f4206986d9b32993 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 5 Feb 2025 09:58:06 +0530 Subject: [PATCH 09/33] chore: env.dist update --- validator-cli/.env.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/validator-cli/.env.dist b/validator-cli/.env.dist index c152544b..b2a4929d 100644 --- a/validator-cli/.env.dist +++ b/validator-cli/.env.dist @@ -21,6 +21,7 @@ VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193 GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d VEAOUTBOX_CHAIN_ID=421611 +VEAOUTBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/outbox-arb-sep/version/latest # Devnet Addresses VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410 From 89b6b0b2b448a150107b36a141078fff319a8849 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 10 Mar 2025 17:21:51 +0530 Subject: [PATCH 10/33] feat: multi network support --- validator-cli/.env.dist | 8 +- validator-cli/src/ArbToEth/claimer.ts | 47 ++++-- .../src/ArbToEth/transactionHandler.ts | 36 +++-- .../src/ArbToEth/transactionHandlerDevnet.ts | 71 +++++++++ validator-cli/src/consts/bridgeRoutes.ts | 60 ++++++-- .../utils/{cli.test.ts => botConfig.test.ts} | 2 +- validator-cli/src/utils/botConfig.ts | 77 ++++++++++ validator-cli/src/utils/botEvents.ts | 5 + validator-cli/src/utils/cli.ts | 35 ----- validator-cli/src/utils/epochHandler.ts | 3 +- validator-cli/src/utils/errors.ts | 28 +++- validator-cli/src/utils/ethers.ts | 22 ++- validator-cli/src/utils/logger.ts | 18 ++- validator-cli/src/watcher.ts | 137 ++++++++++-------- validator-cli/tsconfig.json | 6 +- 15 files changed, 414 insertions(+), 141 deletions(-) create mode 100644 validator-cli/src/ArbToEth/transactionHandlerDevnet.ts rename validator-cli/src/utils/{cli.test.ts => botConfig.test.ts} (94%) create mode 100644 validator-cli/src/utils/botConfig.ts delete mode 100644 validator-cli/src/utils/cli.ts diff --git a/validator-cli/.env.dist b/validator-cli/.env.dist index b2a4929d..2d123711 100644 --- a/validator-cli/.env.dist +++ b/validator-cli/.env.dist @@ -1,10 +1,16 @@ PRIVATE_KEY= +# Networks: devnet, testnet, mainnet +NETWORKS=devnet,testnet + # Devnet RPCs RPC_CHIADO=https://rpc.chiadochain.net RPC_ARB_SEPOLIA=https://sepolia-rollup.arbitrum.io/rpc RPC_SEPOLIA= +# Devnet Owner +DEVNET_OWNER=0x5f4eC3Df9Cf2f0f1fDfCfCfCfCfCfCfCfCfCfCfC + # Testnet or Mainnet RPCs RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc RPC_ETH= @@ -20,7 +26,7 @@ VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS=0x2f1788F7B74e01c4C85578748290467A5f063B0b VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193 GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d -VEAOUTBOX_CHAIN_ID=421611 +VEAOUTBOX_CHAINS=11155111,421611 VEAOUTBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/outbox-arb-sep/version/latest # Devnet Addresses diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 2810a039..b0ae8bb3 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -6,7 +6,12 @@ import { getLastClaimedEpoch } from "../utils/graphQueries"; import { ArbToEthTransactionHandler } from "./transactionHandler"; import { BotEvents } from "../utils/botEvents"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; +import { ArbToEthDevnetTransactionHandler } from "./transactionHandlerDevnet"; +import { getTransactionHandler } from "../utils/ethers"; +import { Network } from "../consts/bridgeRoutes"; interface checkAndClaimParams { + chainId: number; + network: Network; claim: ClaimStruct | null; epochPeriod: number; epoch: number; @@ -21,6 +26,8 @@ interface checkAndClaimParams { } export async function checkAndClaim({ + chainId, + network, claim, epoch, epochPeriod, @@ -34,25 +41,43 @@ export async function checkAndClaim({ }: checkAndClaimParams) { let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); - const claimAbleEpoch = Math.floor(finalizedOutboxBlock.timestamp / epochPeriod) - 1; + const claimAbleEpoch = Math.floor(Date.now() / (1000 * epochPeriod)) - 1; + if (!transactionHandler) { - transactionHandler = new ArbToEthTransactionHandler( + const TransactionHandler = getTransactionHandler(chainId, network); + transactionHandler = new TransactionHandler({ epoch, veaInbox, veaOutbox, veaInboxProvider, veaOutboxProvider, emitter, - claim - ); + claim, + }); } else { transactionHandler.claim = claim; } - if (claim == null && epoch == claimAbleEpoch) { - const [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); - const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; - const lastClaimChallenged: boolean = claimData.challenged && savedSnapshot == outboxStateRoot; - if (newMessagesToBridge || lastClaimChallenged) { + var savedSnapshot; + var claimData; + var newMessagesToBridge: boolean; + var lastClaimChallenged: boolean; + + if (network == Network.DEVNET) { + const devnetTransactionHandler = transactionHandler as ArbToEthDevnetTransactionHandler; + if (claim == null) { + [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); + newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + lastClaimChallenged = claimData.challenged && savedSnapshot == outboxStateRoot; + if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { + await devnetTransactionHandler.devnetAdvanceState(outboxStateRoot); + return devnetTransactionHandler; + } + } + } else if (claim == null && epoch == claimAbleEpoch) { + [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); + newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + lastClaimChallenged = claimData.challenged && savedSnapshot == outboxStateRoot; + if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { await transactionHandler.makeClaim(savedSnapshot); return transactionHandler; } @@ -61,6 +86,10 @@ export async function checkAndClaim({ await transactionHandler.withdrawClaimDeposit(); return transactionHandler; } else if (claim.honest == ClaimHonestState.NONE) { + if (claim.challenger != ethers.ZeroAddress) { + emitter.emit(BotEvents.CLAIM_CHALLENGED, epoch); + return transactionHandler; + } if (claim.timestampVerification == 0) { await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); } else { diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index 664e197e..8ad383be 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -1,4 +1,4 @@ -import { VeaInboxArbToEth, VeaOutboxArbToEth } from "@kleros/vea-contracts/typechain-types"; +import { VeaInboxArbToEth, VeaOutboxArbToEth, VeaOutboxArbToEthDevnet } from "@kleros/vea-contracts/typechain-types"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; import { JsonRpcProvider } from "@ethersproject/providers"; import { messageExecutor } from "../utils/arbMsgExecutor"; @@ -20,12 +20,22 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; * executeSnapshot() - Execute a sent snapshot to resolve dispute in VeaOutbox (ETH). */ +export interface TransactionHandlerConstructor { + epoch: number; + veaInbox: VeaInboxArbToEth; + veaOutbox: VeaOutboxArbToEth; + veaInboxProvider: JsonRpcProvider; + veaOutboxProvider: JsonRpcProvider; + emitter: typeof defaultEmitter; + claim: ClaimStruct | null; +} + export type Transaction = { hash: string; broadcastedTimestamp: number; }; -type Transactions = { +export type Transactions = { claimTxn: Transaction | null; withdrawClaimDepositTxn: Transaction | null; startVerificationTxn: Transaction | null; @@ -36,7 +46,7 @@ type Transactions = { executeSnapshotTxn: Transaction | null; }; -enum TransactionStatus { +export enum TransactionStatus { NOT_MADE = 0, PENDING = 1, NOT_FINAL = 2, @@ -57,7 +67,7 @@ export class ArbToEthTransactionHandler { public claim: ClaimStruct | null = null; public veaInbox: VeaInboxArbToEth; - public veaOutbox: VeaOutboxArbToEth; + public veaOutbox: VeaOutboxArbToEth | VeaOutboxArbToEthDevnet; public veaInboxProvider: JsonRpcProvider; public veaOutboxProvider: JsonRpcProvider; public epoch: number; @@ -74,15 +84,15 @@ export class ArbToEthTransactionHandler { executeSnapshotTxn: null, }; - constructor( - epoch: number, - veaInbox: VeaInboxArbToEth, - veaOutbox: VeaOutboxArbToEth, - veaInboxProvider: JsonRpcProvider, - veaOutboxProvider: JsonRpcProvider, - emitter: typeof defaultEmitter = defaultEmitter, - claim: ClaimStruct | null = null - ) { + constructor({ + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim, + }: TransactionHandlerConstructor) { this.epoch = epoch; this.veaInbox = veaInbox; this.veaOutbox = veaOutbox; diff --git a/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts new file mode 100644 index 00000000..9aea2916 --- /dev/null +++ b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts @@ -0,0 +1,71 @@ +import { JsonRpcProvider } from "@ethersproject/providers"; +import { VeaInboxArbToEth, VeaOutboxArbToEthDevnet } from "@kleros/vea-contracts/typechain-types"; +import { + ArbToEthTransactionHandler, + ContractType, + TransactionStatus, + Transactions, + Transaction, + TransactionHandlerConstructor, +} from "./transactionHandler"; +import { defaultEmitter } from "../utils/emitter"; +import { BotEvents } from "../utils/botEvents"; + +type DevnetTransactions = Transactions & { + devnetAdvanceStateTxn: Transaction | null; +}; + +export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler { + public veaOutboxDevnet: VeaOutboxArbToEthDevnet; + public transactions: DevnetTransactions = { + claimTxn: null, + withdrawClaimDepositTxn: null, + startVerificationTxn: null, + verifySnapshotTxn: null, + challengeTxn: null, + withdrawChallengeDepositTxn: null, + sendSnapshotTxn: null, + executeSnapshotTxn: null, + devnetAdvanceStateTxn: null, + }; + constructor({ + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + epoch, + emitter, + }: TransactionHandlerConstructor) { + super({ + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + } as TransactionHandlerConstructor); + this.veaOutboxDevnet = this.veaOutbox as VeaOutboxArbToEthDevnet; + } + public async devnetAdvanceState(stateRoot: string): Promise { + this.emitter.emit(BotEvents.ADV_DEVNET, this.epoch); + + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.devnetAdvanceStateTxn, + ContractType.OUTBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { + return; + } + const estimateGas = await this.veaOutbox["devnetAdvanceState(uint256,bytes32)"].estimateGas(this.epoch, this.claim); + const startVerifTrx = await this.veaOutboxDevnet.devnetAdvanceState(this.epoch, stateRoot, { + gasLimit: estimateGas.mul(2), + }); + this.emitter.emit(BotEvents.TXN_MADE, startVerifTrx.hash, this.epoch, "Advance Devnet State"); + this.transactions.devnetAdvanceStateTxn = { + hash: startVerifTrx.hash, + broadcastedTimestamp: currentTime, + }; + } +} diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index b61e4696..4d0032be 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -1,5 +1,16 @@ require("dotenv").config(); +import veaInboxArbToEthDevnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToEthDevnet.json"; +import veaOutboxArbToEthDevnet from "@kleros/vea-contracts/deployments/sepolia/VeaOutboxArbToEthDevnet.json"; +import veaInboxArbToEthTestnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToEthTestnet.json"; +import veaOutboxArbToEthTestnet from "@kleros/vea-contracts/deployments/sepolia/VeaOutboxArbToEthTestnet.json"; + +import veaInboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisDevnet.json"; +import veaOutboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisDevnet.json"; + +import veaInboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/VeaOutboxArbToEthTestnet.json"; +import veaOutboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json"; +import veaRouterArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/RouterArbToGnosisTestnet.json"; interface Bridge { chain: string; epochPeriod: number; @@ -8,12 +19,44 @@ interface Bridge { sequencerDelayLimit: number; inboxRPC: string; outboxRPC: string; - inboxAddress: string; - outboxAddress: string; - routerAddress?: string; - routerProvider?: string; + routerRPC?: string; + veaContracts: { [key in Network]: VeaContracts }; } +type VeaContracts = { + veaInbox: any; + veaOutbox: any; + veaRouter?: any; +}; + +export enum Network { + DEVNET = "devnet", + TESTNET = "testnet", +} + +const arbToEthContracts: { [key in Network]: VeaContracts } = { + [Network.DEVNET]: { + veaInbox: veaInboxArbToEthDevnet, + veaOutbox: veaOutboxArbToEthDevnet, + }, + [Network.TESTNET]: { + veaInbox: veaInboxArbToEthTestnet, + veaOutbox: veaOutboxArbToEthTestnet, + }, +}; + +const arbToGnosisContracts: { [key in Network]: VeaContracts } = { + [Network.DEVNET]: { + veaInbox: veaInboxArbToGnosisDevnet, + veaOutbox: veaOutboxArbToGnosisDevnet, + }, + [Network.TESTNET]: { + veaInbox: veaInboxArbToGnosisTestnet, + veaOutbox: veaOutboxArbToGnosisTestnet, + veaRouter: veaRouterArbToGnosisTestnet, + }, +}; + const bridges: { [chainId: number]: Bridge } = { 11155111: { chain: "sepolia", @@ -23,8 +66,7 @@ const bridges: { [chainId: number]: Bridge } = { sequencerDelayLimit: 86400, inboxRPC: process.env.RPC_ARB, outboxRPC: process.env.RPC_ETH, - inboxAddress: process.env.VEAINBOX_ARB_TO_ETH_ADDRESS, - outboxAddress: process.env.VEAOUTBOX_ARB_TO_ETH_ADDRESS, + veaContracts: arbToEthContracts, }, 10200: { chain: "chiado", @@ -34,10 +76,8 @@ const bridges: { [chainId: number]: Bridge } = { sequencerDelayLimit: 86400, inboxRPC: process.env.RPC_ARB, outboxRPC: process.env.RPC_GNOSIS, - routerProvider: process.env.RPC_ETH, - inboxAddress: process.env.VEAINBOX_ARB_TO_GNOSIS_ADDRESS, - routerAddress: process.env.VEA_ROUTER_ARB_TO_GNOSIS_ADDRESS, - outboxAddress: process.env.VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS, + routerRPC: process.env.RPC_ETH, + veaContracts: arbToGnosisContracts, }, }; diff --git a/validator-cli/src/utils/cli.test.ts b/validator-cli/src/utils/botConfig.test.ts similarity index 94% rename from validator-cli/src/utils/cli.test.ts rename to validator-cli/src/utils/botConfig.test.ts index 3ad8cc81..175b357d 100644 --- a/validator-cli/src/utils/cli.test.ts +++ b/validator-cli/src/utils/botConfig.test.ts @@ -1,4 +1,4 @@ -import { getBotPath, BotPaths } from "./cli"; +import { getBotPath, BotPaths } from "./botConfig"; import { InvalidBotPathError } from "./errors"; describe("cli", () => { describe("getBotPath", () => { diff --git a/validator-cli/src/utils/botConfig.ts b/validator-cli/src/utils/botConfig.ts new file mode 100644 index 00000000..cee3ede9 --- /dev/null +++ b/validator-cli/src/utils/botConfig.ts @@ -0,0 +1,77 @@ +import { InvalidBotPathError, DevnetOwnerNotSetError, InvalidNetworkError } from "./errors"; +import { Network } from "../consts/bridgeRoutes"; +require("dotenv").config(); + +export enum BotPaths { + CLAIMER = 0, // happy path + CHALLENGER = 1, // unhappy path + BOTH = 2, // both happy and unhappy path +} + +interface BotPathParams { + cliCommand: string[]; + defaultPath?: BotPaths; +} + +/** + * Get the bot path from the command line arguments + * @param defaultPath - default path to use if not specified in the command line arguments + * @returns BotPaths - the bot path (BotPaths) + */ +export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathParams): number { + const args = cliCommand.slice(2); + const pathFlag = args.find((arg) => arg.startsWith("--path=")); + + const path = pathFlag ? pathFlag.split("=")[1] : null; + + const pathMapping: Record = { + claimer: BotPaths.CLAIMER, + challenger: BotPaths.CHALLENGER, + both: BotPaths.BOTH, + }; + + if (path && !(path in pathMapping)) { + throw new InvalidBotPathError(); + } + + return path ? pathMapping[path] : defaultPath; +} + +interface NetworkConfig { + chainId: number; + networks: Network[]; + devnetOwner?: string; +} + +/** + * Get the network configuration: chainId, networks, and devnet owner + * @returns NetworkConfig[] - the network configuration + */ +export function getNetworkConfig(): NetworkConfig[] { + const chainIds = process.env.VEAOUTBOX_CHAINS ? process.env.VEAOUTBOX_CHAINS.split(",") : []; + const devnetOwner = process.env.DEVNET_OWNER; + const rawNetwork = process.env.NETWORKS ? process.env.NETWORKS.split(",") : []; + const networks = validateNetworks(rawNetwork); + if (networks.includes(Network.DEVNET) && !devnetOwner) { + throw new DevnetOwnerNotSetError(); + } + const networkConfig: NetworkConfig[] = []; + for (const chainId of chainIds) { + networkConfig.push({ + chainId: Number(chainId), + networks, + devnetOwner, + }); + } + return networkConfig; +} + +function validateNetworks(networks: string[]): Network[] { + const validNetworks = Object.values(Network); + for (const network of networks) { + if (!validNetworks.includes(network as Network)) { + throw new InvalidNetworkError(network); + } + } + return networks as unknown as Network[]; +} diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index bd717f1b..ae7dc663 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -1,6 +1,7 @@ export enum BotEvents { // Bridger state STARTED = "started", + WATCHING = "watching", CHECKING = "checking", WAITING = "waiting", NO_CLAIM = "no_claim", @@ -18,6 +19,7 @@ export enum BotEvents { VERIFYING_SNAPSHOT = "verifying_snapshot", CANT_VERIFY_SNAPSHOT = "cant_verify_snapshot", CHALLENGING = "challenging", + CLAIM_CHALLENGED = "claim_challenged", CHALLENGER_WON_CLAIM = "challenger_won_claim", SENDING_SNAPSHOT = "sending_snapshot", EXECUTING_SNAPSHOT = "executing_snapshot", @@ -26,6 +28,9 @@ export enum BotEvents { WITHDRAWING_CLAIM_DEPOSIT = "withdrawing_claim_deposit", WAITING_ARB_TIMEOUT = "waiting_arb_timeout", + // Devnet state + ADV_DEVNET = "advance_devnet", + // Transaction state TXN_MADE = "txn_made", TXN_PENDING = "txn_pending", diff --git a/validator-cli/src/utils/cli.ts b/validator-cli/src/utils/cli.ts deleted file mode 100644 index e869beed..00000000 --- a/validator-cli/src/utils/cli.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { InvalidBotPathError } from "./errors"; -export enum BotPaths { - CLAIMER = 0, // happy path - CHALLENGER = 1, // unhappy path - BOTH = 2, // both happy and unhappy path -} - -interface BotPathParams { - cliCommand: string[]; - defaultPath?: BotPaths; -} - -/** - * Get the bot path from the command line arguments - * @param defaultPath - default path to use if not specified in the command line arguments - * @returns BotPaths - the bot path (BotPaths) - */ -export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathParams): number { - const args = cliCommand.slice(2); - const pathFlag = args.find((arg) => arg.startsWith("--path=")); - - const path = pathFlag ? pathFlag.split("=")[1] : null; - - const pathMapping: Record = { - claimer: BotPaths.CLAIMER, - challenger: BotPaths.CHALLENGER, - both: BotPaths.BOTH, - }; - - if (path && !(path in pathMapping)) { - throw new InvalidBotPathError(); - } - - return path ? pathMapping[path] : defaultPath; -} diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index 3d9ec386..f33f3797 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -36,7 +36,6 @@ const setEpochRange = ( const veaEpochOutboxCheckClaimsRangeArray: number[] = new Array(veaEpochOutboxRange) .fill(veaEpochOutboxWatchLowerBound) .map((el, i) => el + i); - return veaEpochOutboxCheckClaimsRangeArray; }; @@ -64,7 +63,7 @@ const getLatestChallengeableEpoch = ( const getBlockFromEpoch = async (epoch: number, epochPeriod: number, provider: JsonRpcProvider): Promise => { const epochTimestamp = epoch * epochPeriod; const latestBlock = await provider.getBlock("latest"); - const baseBlock = await provider.getBlock(latestBlock.number - 100); + const baseBlock = await provider.getBlock(latestBlock.number - 1000); const secPerBlock = (latestBlock.timestamp - baseBlock.timestamp) / (latestBlock.number - baseBlock.number); const blockFallBack = Math.floor((latestBlock.timestamp - epochTimestamp) / secPerBlock); return latestBlock.number - blockFallBack; diff --git a/validator-cli/src/utils/errors.ts b/validator-cli/src/utils/errors.ts index 79566328..e7042de0 100644 --- a/validator-cli/src/utils/errors.ts +++ b/validator-cli/src/utils/errors.ts @@ -1,4 +1,5 @@ -import { BotPaths } from "./cli"; +import { BotPaths } from "./botConfig"; +import { Network } from "../consts/bridgeRoutes"; class ClaimNotFoundError extends Error { constructor(epoch: number) { super(); @@ -31,4 +32,27 @@ class InvalidBotPathError extends Error { } } -export { ClaimNotFoundError, ClaimNotSetError, TransactionHandlerNotDefinedError, InvalidBotPathError }; +class DevnetOwnerNotSetError extends Error { + constructor() { + super(); + this.name = "DevnetOwnerNotSetError"; + this.message = "Devnet owner address not set"; + } +} + +class InvalidNetworkError extends Error { + constructor(network: string) { + super(); + this.name = "InvalidNetworkError"; + this.message = `Invalid network: ${network}, use from: ${Object.values(Network).join(", ")}`; + } +} + +export { + ClaimNotFoundError, + ClaimNotSetError, + TransactionHandlerNotDefinedError, + InvalidBotPathError, + DevnetOwnerNotSetError, + InvalidNetworkError, +}; diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 5707a2b8..880f84cb 100644 --- a/validator-cli/src/utils/ethers.ts +++ b/validator-cli/src/utils/ethers.ts @@ -12,7 +12,9 @@ import { import { challengeAndResolveClaim as challengeAndResolveClaimArbToEth } from "../ArbToEth/validator"; import { checkAndClaim } from "../ArbToEth/claimer"; import { ArbToEthTransactionHandler } from "../ArbToEth/transactionHandler"; +import { ArbToEthDevnetTransactionHandler } from "../ArbToEth/transactionHandlerDevnet"; import { TransactionHandlerNotDefinedError } from "./errors"; +import { Network } from "../consts/bridgeRoutes"; function getWallet(privateKey: string, web3ProviderURL: string) { return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL)); @@ -59,22 +61,32 @@ function getAMB(ambAddress: string, privateKey: string, web3ProviderURL: string) return IAMB__factory.connect(ambAddress, getWallet(privateKey, web3ProviderURL)); } -const getClaimValidator = (chainId: number) => { +const getClaimValidator = (chainId: number, network: Network) => { switch (chainId) { case 11155111: return challengeAndResolveClaimArbToEth; } }; -const getClaimer = (chainId: number) => { +const getClaimer = (chainId: number, network: Network) => { switch (chainId) { case 11155111: - return checkAndClaim; + switch (network) { + case Network.DEVNET: + + case Network.TESTNET: + return checkAndClaim; + } } }; -const getTransactionHandler = (chainId: number) => { +const getTransactionHandler = (chainId: number, network: Network) => { switch (chainId) { case 11155111: - return ArbToEthTransactionHandler; + switch (network) { + case Network.DEVNET: + return ArbToEthDevnetTransactionHandler; + case Network.TESTNET: + return ArbToEthTransactionHandler; + } default: throw new TransactionHandlerNotDefinedError(); } diff --git a/validator-cli/src/utils/logger.ts b/validator-cli/src/utils/logger.ts index b2123e3e..8918188a 100644 --- a/validator-cli/src/utils/logger.ts +++ b/validator-cli/src/utils/logger.ts @@ -1,6 +1,7 @@ import { EventEmitter } from "node:events"; import { BotEvents } from "./botEvents"; -import { BotPaths } from "./cli"; +import { BotPaths } from "./botConfig"; +import { Network } from "../consts/bridgeRoutes"; /** * Listens to relevant events of an EventEmitter instance and issues log lines @@ -19,14 +20,18 @@ export const initialize = (emitter: EventEmitter) => { export const configurableInitialize = (emitter: EventEmitter) => { // Bridger state logs - emitter.on(BotEvents.STARTED, (chainId: number, path: number) => { - let pathString = "challenger and claimer"; + emitter.on(BotEvents.STARTED, (path: BotPaths, networks: Network[]) => { + let pathString = "claimer and challenger"; if (path === BotPaths.CLAIMER) { - pathString = "bridger"; + pathString = "claimer"; } else if (path === BotPaths.CHALLENGER) { pathString = "challenger"; } - console.log(`Bot started for chainId ${chainId} as ${pathString}`); + console.log(`Bot started for ${pathString} on ${networks}`); + }); + + emitter.on(BotEvents.WATCHING, (chainId: number, network: Network) => { + console.log(`Watching for chain ${chainId} on ${network}`); }); emitter.on(BotEvents.CHECKING, (epoch: number) => { @@ -91,6 +96,9 @@ export const configurableInitialize = (emitter: EventEmitter) => { emitter.on(BotEvents.CHALLENGING, (epoch: number) => { console.log(`Claim can be challenged, challenging for epoch ${epoch}`); }); + emitter.on(BotEvents.CLAIM_CHALLENGED, (epoch: number) => { + console.log(`Claim is challenged for epoch ${epoch}`); + }); // startVerification() emitter.on(BotEvents.SENDING_SNAPSHOT, (epoch: number) => { console.log(`Sending snapshot for ${epoch}`); diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index 8165e665..2dc1b0b9 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -7,7 +7,7 @@ import { defaultEmitter } from "./utils/emitter"; import { BotEvents } from "./utils/botEvents"; import { initialize as initializeLogger } from "./utils/logger"; import { ShutdownSignal } from "./utils/shutdown"; -import { getBotPath, BotPaths } from "./utils/cli"; +import { getBotPath, BotPaths, getNetworkConfig } from "./utils/botConfig"; import { getClaim } from "./utils/claim"; /** @@ -25,65 +25,88 @@ export const watch = async ( initializeLogger(emitter); const cliCommand = process.argv; const path = getBotPath({ cliCommand }); - const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); - emitter.emit(BotEvents.STARTED, chainId, path); - const veaBridge: Bridge = getBridgeConfig(chainId); - const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); - const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); - const veaInboxProvider = new JsonRpcProvider(veaBridge.inboxRPC); - const veaOutboxProvider = new JsonRpcProvider(veaBridge.outboxRPC); - const checkAndChallengeResolve = getClaimValidator(chainId); - const checkAndClaim = getClaimer(chainId); - const TransactionHandler = getTransactionHandler(chainId); + const networkConfigs = getNetworkConfig(); + emitter.emit(BotEvents.STARTED, path, networkConfigs[0].networks); + for (const networkConfig of networkConfigs) { + const { chainId, networks } = networkConfig; + const { veaContracts, inboxRPC, outboxRPC } = getBridgeConfig(chainId); + for (const network of networks) { + emitter.emit(BotEvents.WATCHING, chainId, network); + const veaInbox = getVeaInbox(veaContracts[network].veaInbox.address, process.env.PRIVATE_KEY, inboxRPC, chainId); + const veaOutbox = getVeaOutbox( + veaContracts[network].veaOutbox.address, + process.env.PRIVATE_KEY, + outboxRPC, + chainId + ); + const veaInboxProvider = new JsonRpcProvider(inboxRPC); + const veaOutboxProvider = new JsonRpcProvider(outboxRPC); - let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - const transactionHandlers: { [epoch: number]: InstanceType } = {}; - const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); - let latestEpoch = epochRange[epochRange.length - 1]; - while (!shutDownSignal.getIsShutdownSignal()) { - let i = 0; - while (i < epochRange.length) { - const epoch = epochRange[i]; - emitter.emit(BotEvents.CHECKING, epoch); - const epochBlock = await getBlockFromEpoch(epoch, veaBridge.epochPeriod, veaOutboxProvider); - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); - const checkAndChallengeResolveDeps = { - claim, - epoch, - epochPeriod: veaBridge.epochPeriod, - veaInbox, - veaInboxProvider, - veaOutboxProvider, - veaOutbox, - transactionHandler: transactionHandlers[epoch], - emitter, - }; - let updatedTransactions; - if (path > BotPaths.CLAIMER && claim != null) { - updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); - } - if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { - updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); - } - - if (updatedTransactions) { - transactionHandlers[epoch] = updatedTransactions; - } else if (epoch != latestEpoch) { - delete transactionHandlers[epoch]; - epochRange.splice(i, 1); - continue; - } - i++; - } - const newVerifiableEpoch = Math.floor(Date.now() / (1000 * veaBridge.epochPeriod)) - 1; - if (newVerifiableEpoch > latestEpoch) { - epochRange.push(newVerifiableEpoch); - latestEpoch = newVerifiableEpoch; - } else { - emitter.emit(BotEvents.WAITING, latestEpoch); + const checkAndChallengeResolve = getClaimValidator(chainId, network); + const checkAndClaim = getClaimer(chainId, network); } - await wait(1000 * 10); } + return; + // const chainId = Number(process.env.VEAOUTBOX_CHAINS); + + // emitter.emit(BotEvents.STARTED, chainId, path); + // + // const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); + // const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); + // const veaInboxProvider = new JsonRpcProvider(veaBridge.inboxRPC); + // const veaOutboxProvider = new JsonRpcProvider(veaBridge.outboxRPC); + // const checkAndChallengeResolve = getClaimValidator(chainId); + // const checkAndClaim = getClaimer(chainId); + // const TransactionHandler = getTransactionHandler(chainId); + + // let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); + // const transactionHandlers: { [epoch: number]: InstanceType } = {}; + // const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); + // let latestEpoch = epochRange[epochRange.length - 1]; + // while (!shutDownSignal.getIsShutdownSignal()) { + // let i = 0; + // while (i < epochRange.length) { + // const epoch = epochRange[i]; + // emitter.emit(BotEvents.CHECKING, epoch); + // const epochBlock = await getBlockFromEpoch(epoch, veaBridge.epochPeriod, veaOutboxProvider); + // const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); + // const checkAndChallengeResolveDeps = { + // claim, + // epoch, + // epochPeriod: veaBridge.epochPeriod, + // veaInbox, + // veaInboxProvider, + // veaOutboxProvider, + // veaOutbox, + // transactionHandler: transactionHandlers[epoch], + // emitter, + // }; + // let updatedTransactions; + // if (path > BotPaths.CLAIMER && claim != null) { + // updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); + // } + // if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { + // updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); + // } + + // if (updatedTransactions) { + // transactionHandlers[epoch] = updatedTransactions; + // } else if (epoch != latestEpoch) { + // delete transactionHandlers[epoch]; + // epochRange.splice(i, 1); + // continue; + // } + // i++; + // } + // const newVerifiableEpoch = Math.floor(Date.now() / (1000 * veaBridge.epochPeriod)) - 1; + // if (newVerifiableEpoch > latestEpoch) { + // epochRange.push(newVerifiableEpoch); + // latestEpoch = newVerifiableEpoch; + // } else { + // emitter.emit(BotEvents.WAITING, latestEpoch); + // } + // await wait(1000 * 10); + // } }; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); diff --git a/validator-cli/tsconfig.json b/validator-cli/tsconfig.json index dbe3a5ab..1a96b974 100644 --- a/validator-cli/tsconfig.json +++ b/validator-cli/tsconfig.json @@ -1,5 +1,9 @@ { "include": [ "src" - ] + ], + "compilerOptions": { + "resolveJsonModule": true, + "esModuleInterop": true + } } From cffaa005ba2c36b3c898f804842f77f29031b0b4 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 10 Mar 2025 19:45:23 +0530 Subject: [PATCH 11/33] chore: refactor for networks --- validator-cli/src/ArbToEth/claimer.ts | 1 + .../src/ArbToEth/transactionHandler.ts | 9 +- validator-cli/src/ArbToEth/validator.ts | 10 +- validator-cli/src/consts/bridgeRoutes.ts | 20 +- validator-cli/src/utils/epochHandler.ts | 8 +- validator-cli/src/watcher.ts | 201 +++++++++++------- 6 files changed, 154 insertions(+), 95 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index b0ae8bb3..9466f8b3 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -46,6 +46,7 @@ export async function checkAndClaim({ if (!transactionHandler) { const TransactionHandler = getTransactionHandler(chainId, network); transactionHandler = new TransactionHandler({ + network, epoch, veaInbox, veaOutbox, diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index 8ad383be..d715e5c6 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -5,7 +5,7 @@ import { messageExecutor } from "../utils/arbMsgExecutor"; import { defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { ClaimNotSetError } from "../utils/errors"; -import { getBridgeConfig } from "../consts/bridgeRoutes"; +import { getBridgeConfig, Network } from "../consts/bridgeRoutes"; /** * @file This file contains the logic for handling transactions from Arbitrum to Ethereum. @@ -21,6 +21,7 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; */ export interface TransactionHandlerConstructor { + network: Network; epoch: number; veaInbox: VeaInboxArbToEth; veaOutbox: VeaOutboxArbToEth; @@ -65,7 +66,7 @@ const CHAIN_ID = 11155111; export class ArbToEthTransactionHandler { public claim: ClaimStruct | null = null; - + public network: Network; public veaInbox: VeaInboxArbToEth; public veaOutbox: VeaOutboxArbToEth | VeaOutboxArbToEthDevnet; public veaInboxProvider: JsonRpcProvider; @@ -85,6 +86,7 @@ export class ArbToEthTransactionHandler { }; constructor({ + network, epoch, veaInbox, veaOutbox, @@ -93,6 +95,7 @@ export class ArbToEthTransactionHandler { emitter, claim, }: TransactionHandlerConstructor) { + this.network = network; this.epoch = epoch; this.veaInbox = veaInbox; this.veaOutbox = veaOutbox; @@ -197,7 +200,7 @@ export class ArbToEthTransactionHandler { currentTimestamp - Number(this.claim.timestampClaimed) - bridgeConfig.sequencerDelayLimit - - bridgeConfig.epochPeriod; + bridgeConfig.routeConfig[this.network].epochPeriod; if (timeOver < 0) { this.emitter.emit(BotEvents.VERIFICATION_CANT_START, this.epoch, -1 * timeOver); diff --git a/validator-cli/src/ArbToEth/validator.ts b/validator-cli/src/ArbToEth/validator.ts index c3b76dcb..881ed6e0 100644 --- a/validator-cli/src/ArbToEth/validator.ts +++ b/validator-cli/src/ArbToEth/validator.ts @@ -6,6 +6,7 @@ import { getClaim, getClaimResolveState } from "../utils/claim"; import { defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { getBlocksAndCheckFinality } from "../utils/arbToEthState"; +import { Network } from "../consts/bridgeRoutes"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; // https://github.com/prysmaticlabs/prysm/blob/493905ee9e33a64293b66823e69704f012b39627/config/params/mainnet_config.go#L103 @@ -60,15 +61,16 @@ export async function challengeAndResolveClaim({ } const ethBlockTag = finalityIssueFlagEth ? "finalized" : "latest"; if (!transactionHandler) { - transactionHandler = new ArbToEthTransactionHandler( + transactionHandler = new ArbToEthTransactionHandler({ + network: Network.TESTNET, // Hardcoded as TESTNET & MAINNET have same contracts epoch, veaInbox, veaOutbox, veaInboxProvider, veaOutboxProvider, - defaultEmitter, - claim - ); + emitter: defaultEmitter, + claim, + }); } else { transactionHandler.claim = claim; } diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index 4d0032be..b8867f58 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -13,20 +13,20 @@ import veaOutboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/chiad import veaRouterArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/RouterArbToGnosisTestnet.json"; interface Bridge { chain: string; - epochPeriod: number; deposit: bigint; minChallengePeriod: number; sequencerDelayLimit: number; inboxRPC: string; outboxRPC: string; routerRPC?: string; - veaContracts: { [key in Network]: VeaContracts }; + routeConfig: { [key in Network]: RouteConfigs }; } -type VeaContracts = { +type RouteConfigs = { veaInbox: any; veaOutbox: any; veaRouter?: any; + epochPeriod: number; }; export enum Network { @@ -34,50 +34,52 @@ export enum Network { TESTNET = "testnet", } -const arbToEthContracts: { [key in Network]: VeaContracts } = { +const arbToEthConfigs: { [key in Network]: RouteConfigs } = { [Network.DEVNET]: { veaInbox: veaInboxArbToEthDevnet, veaOutbox: veaOutboxArbToEthDevnet, + epochPeriod: 3600, }, [Network.TESTNET]: { veaInbox: veaInboxArbToEthTestnet, veaOutbox: veaOutboxArbToEthTestnet, + epochPeriod: 7200, }, }; -const arbToGnosisContracts: { [key in Network]: VeaContracts } = { +const arbToGnosisConfigs: { [key in Network]: RouteConfigs } = { [Network.DEVNET]: { veaInbox: veaInboxArbToGnosisDevnet, veaOutbox: veaOutboxArbToGnosisDevnet, + epochPeriod: 3600, }, [Network.TESTNET]: { veaInbox: veaInboxArbToGnosisTestnet, veaOutbox: veaOutboxArbToGnosisTestnet, veaRouter: veaRouterArbToGnosisTestnet, + epochPeriod: 7200, }, }; const bridges: { [chainId: number]: Bridge } = { 11155111: { chain: "sepolia", - epochPeriod: 7200, deposit: BigInt("1000000000000000000"), minChallengePeriod: 10800, sequencerDelayLimit: 86400, inboxRPC: process.env.RPC_ARB, outboxRPC: process.env.RPC_ETH, - veaContracts: arbToEthContracts, + routeConfig: arbToEthConfigs, }, 10200: { chain: "chiado", - epochPeriod: 3600, deposit: BigInt("1000000000000000000"), minChallengePeriod: 10800, sequencerDelayLimit: 86400, inboxRPC: process.env.RPC_ARB, outboxRPC: process.env.RPC_GNOSIS, routerRPC: process.env.RPC_ETH, - veaContracts: arbToGnosisContracts, + routeConfig: arbToGnosisConfigs, }, }; diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index f33f3797..5ecac538 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -13,12 +13,13 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; */ const setEpochRange = ( - currentTimestamp: number, chainId: number, + currentTimestamp: number, + epochPeriod: number, now: number = Date.now(), fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig ): Array => { - const { sequencerDelayLimit, epochPeriod } = fetchBridgeConfig(chainId); + const { sequencerDelayLimit } = fetchBridgeConfig(chainId); const coldStartBacklog = 7 * 24 * 60 * 60; // when starting the watcher, specify an extra backlog to check // When Sequencer is malicious, even when L1 is finalized, L2 state might be unknown for up to sequencerDelayLimit + epochPeriod. @@ -36,6 +37,7 @@ const setEpochRange = ( const veaEpochOutboxCheckClaimsRangeArray: number[] = new Array(veaEpochOutboxRange) .fill(veaEpochOutboxWatchLowerBound) .map((el, i) => el + i); + return [241886, 241887, 241888]; return veaEpochOutboxCheckClaimsRangeArray; }; @@ -53,10 +55,10 @@ const setEpochRange = ( */ const getLatestChallengeableEpoch = ( chainId: number, + epochPeriod: number, now: number = Date.now(), fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig ): number => { - const { epochPeriod } = fetchBridgeConfig(chainId); return Math.floor(now / 1000 / epochPeriod) - 2; }; diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index 2dc1b0b9..be896a9b 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -1,5 +1,5 @@ import { JsonRpcProvider } from "@ethersproject/providers"; -import { getBridgeConfig, Bridge } from "./consts/bridgeRoutes"; +import { getBridgeConfig, Bridge, Network } from "./consts/bridgeRoutes"; import { getVeaInbox, getVeaOutbox, getTransactionHandler } from "./utils/ethers"; import { getBlockFromEpoch, setEpochRange } from "./utils/epochHandler"; import { getClaimValidator, getClaimer } from "./utils/ethers"; @@ -27,87 +27,136 @@ export const watch = async ( const path = getBotPath({ cliCommand }); const networkConfigs = getNetworkConfig(); emitter.emit(BotEvents.STARTED, path, networkConfigs[0].networks); - for (const networkConfig of networkConfigs) { - const { chainId, networks } = networkConfig; - const { veaContracts, inboxRPC, outboxRPC } = getBridgeConfig(chainId); - for (const network of networks) { - emitter.emit(BotEvents.WATCHING, chainId, network); - const veaInbox = getVeaInbox(veaContracts[network].veaInbox.address, process.env.PRIVATE_KEY, inboxRPC, chainId); - const veaOutbox = getVeaOutbox( - veaContracts[network].veaOutbox.address, - process.env.PRIVATE_KEY, - outboxRPC, - chainId - ); - const veaInboxProvider = new JsonRpcProvider(inboxRPC); - const veaOutboxProvider = new JsonRpcProvider(outboxRPC); + const transactionHandlers: { [epoch: number]: any } = {}; + const watcherStarted: { chainId: number; network: string }[] = []; + while (!shutDownSignal.getIsShutdownSignal()) { + for (const networkConfig of networkConfigs) { + const { chainId, networks } = networkConfig; + const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); + for (const network of networks) { + emitter.emit(BotEvents.WATCHING, chainId, network); + const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, process.env.PRIVATE_KEY, inboxRPC, chainId); + const veaOutbox = getVeaOutbox( + routeConfig[network].veaOutbox.address, + process.env.PRIVATE_KEY, + outboxRPC, + chainId + ); + const veaInboxProvider = new JsonRpcProvider(inboxRPC); + const veaOutboxProvider = new JsonRpcProvider(outboxRPC); + let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); + var epochRange = setEpochRange(chainId, veaOutboxLatestBlock.timestamp, routeConfig[network].epochPeriod); - const checkAndChallengeResolve = getClaimValidator(chainId, network); - const checkAndClaim = getClaimer(chainId, network); + // If the watcher has already started, only check the latest epoch + if ( + watcherStarted.find((watcher) => watcher.chainId == chainId && watcher.network == network) != null || + network == Network.DEVNET + ) { + epochRange = [epochRange[epochRange.length - 1]]; + } + let i = epochRange.length - 1; + while (i >= 0) { + const epoch = epochRange[i]; + let latestEpoch = epochRange[epochRange.length - 1]; + const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); + const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); + const checkAndChallengeResolveDeps = { + network, + chainId, + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; + + const checkAndChallengeResolve = getClaimValidator(chainId, network); + const checkAndClaim = getClaimer(chainId, network); + let updatedTransactions; + if (path > BotPaths.CLAIMER && claim != null) { + updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); + } + if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { + updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); + } + + if (updatedTransactions) { + transactionHandlers[epoch] = updatedTransactions; + } else if (epoch != latestEpoch) { + delete transactionHandlers[epoch]; + epochRange.splice(i, 1); + } + i--; + } + } } + await wait(1000 * 10); } - return; - // const chainId = Number(process.env.VEAOUTBOX_CHAINS); +}; +// const chainId = Number(process.env.VEAOUTBOX_CHAINS); - // emitter.emit(BotEvents.STARTED, chainId, path); - // - // const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); - // const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); - // const veaInboxProvider = new JsonRpcProvider(veaBridge.inboxRPC); - // const veaOutboxProvider = new JsonRpcProvider(veaBridge.outboxRPC); - // const checkAndChallengeResolve = getClaimValidator(chainId); - // const checkAndClaim = getClaimer(chainId); - // const TransactionHandler = getTransactionHandler(chainId); +// emitter.emit(BotEvents.STARTED, chainId, path); +// +// const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); +// const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); +// const veaInboxProvider = new JsonRpcProvider(veaBridge.inboxRPC); +// const veaOutboxProvider = new JsonRpcProvider(veaBridge.outboxRPC); +// const checkAndChallengeResolve = getClaimValidator(chainId); +// const checkAndClaim = getClaimer(chainId); +// const TransactionHandler = getTransactionHandler(chainId); - // let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - // const transactionHandlers: { [epoch: number]: InstanceType } = {}; - // const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); - // let latestEpoch = epochRange[epochRange.length - 1]; - // while (!shutDownSignal.getIsShutdownSignal()) { - // let i = 0; - // while (i < epochRange.length) { - // const epoch = epochRange[i]; - // emitter.emit(BotEvents.CHECKING, epoch); - // const epochBlock = await getBlockFromEpoch(epoch, veaBridge.epochPeriod, veaOutboxProvider); - // const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); - // const checkAndChallengeResolveDeps = { - // claim, - // epoch, - // epochPeriod: veaBridge.epochPeriod, - // veaInbox, - // veaInboxProvider, - // veaOutboxProvider, - // veaOutbox, - // transactionHandler: transactionHandlers[epoch], - // emitter, - // }; - // let updatedTransactions; - // if (path > BotPaths.CLAIMER && claim != null) { - // updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); - // } - // if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { - // updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); - // } +// let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); +// const transactionHandlers: { [epoch: number]: InstanceType } = {}; +// const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); +// let latestEpoch = epochRange[epochRange.length - 1]; +// while (!shutDownSignal.getIsShutdownSignal()) { +// let i = 0; +// while (i < epochRange.length) { +// const epoch = epochRange[i]; +// emitter.emit(BotEvents.CHECKING, epoch); +// const epochBlock = await getBlockFromEpoch(epoch, veaBridge.epochPeriod, veaOutboxProvider); +// const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); +// const checkAndChallengeResolveDeps = { +// claim, +// epoch, +// epochPeriod: veaBridge.epochPeriod, +// veaInbox, +// veaInboxProvider, +// veaOutboxProvider, +// veaOutbox, +// transactionHandler: transactionHandlers[epoch], +// emitter, +// }; +// let updatedTransactions; +// if (path > BotPaths.CLAIMER && claim != null) { +// updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); +// } +// if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { +// updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); +// } - // if (updatedTransactions) { - // transactionHandlers[epoch] = updatedTransactions; - // } else if (epoch != latestEpoch) { - // delete transactionHandlers[epoch]; - // epochRange.splice(i, 1); - // continue; - // } - // i++; - // } - // const newVerifiableEpoch = Math.floor(Date.now() / (1000 * veaBridge.epochPeriod)) - 1; - // if (newVerifiableEpoch > latestEpoch) { - // epochRange.push(newVerifiableEpoch); - // latestEpoch = newVerifiableEpoch; - // } else { - // emitter.emit(BotEvents.WAITING, latestEpoch); - // } - // await wait(1000 * 10); - // } -}; +// if (updatedTransactions) { +// transactionHandlers[epoch] = updatedTransactions; +// } else if (epoch != latestEpoch) { +// delete transactionHandlers[epoch]; +// epochRange.splice(i, 1); +// continue; +// } +// i++; +// } +// const newVerifiableEpoch = Math.floor(Date.now() / (1000 * veaBridge.epochPeriod)) - 1; +// if (newVerifiableEpoch > latestEpoch) { +// epochRange.push(newVerifiableEpoch); +// latestEpoch = newVerifiableEpoch; +// } else { +// emitter.emit(BotEvents.WAITING, latestEpoch); +// } +// await wait(1000 * 10); +// } const wait = (ms) => new Promise((r) => setTimeout(r, ms)); From 9a7cc652030f7cb9ff39c264a3e748e7857be682 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:20:10 +0530 Subject: [PATCH 12/33] feat(validator): devent network support --- validator-cli/src/ArbToEth/claimer.test.ts | 172 ++++++++++++------ validator-cli/src/ArbToEth/claimer.ts | 33 ++-- .../src/ArbToEth/transactionHandler.test.ts | 129 ++++--------- .../src/ArbToEth/transactionHandler.ts | 3 +- .../src/ArbToEth/transactionHandlerDevnet.ts | 14 +- 5 files changed, 183 insertions(+), 168 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.test.ts b/validator-cli/src/ArbToEth/claimer.test.ts index c0aea9a4..b5763ae2 100644 --- a/validator-cli/src/ArbToEth/claimer.test.ts +++ b/validator-cli/src/ArbToEth/claimer.test.ts @@ -1,8 +1,10 @@ import { ethers } from "ethers"; -import { checkAndClaim } from "./claimer"; +import { checkAndClaim, CheckAndClaimParams } from "./claimer"; import { ClaimHonestState } from "../utils/claim"; +import { Network } from "../consts/bridgeRoutes"; describe("claimer", () => { + const NETWORK = Network.DEVNET; let veaOutbox: any; let veaInbox: any; let veaInboxProvider: any; @@ -10,7 +12,17 @@ describe("claimer", () => { let emitter: any; let mockClaim: any; let mockGetLatestClaimedEpoch: any; - let mockDeps: any; + let mockGetTransactionHandler: any; + let mockDeps: CheckAndClaimParams; + + let mockTransactionHandler: any; + const mockTransactions = { + claimTxn: "0x111", + withdrawClaimDepositTxn: "0x222", + startVerificationTxn: "0x333", + verifySnapshotTxn: "0x444", + devnetAdvanceStateTxn: "0x555", + }; beforeEach(() => { mockClaim = { stateRoot: "0x1234", @@ -36,8 +48,14 @@ describe("claimer", () => { }; mockGetLatestClaimedEpoch = jest.fn(); + mockGetTransactionHandler = jest.fn().mockReturnValue(function DummyTransactionHandler(params: any) { + // Return an object that matches our expected transaction handler. + return mockTransactionHandler; + }); mockDeps = { + chainId: 0, claim: mockClaim, + network: NETWORK, epoch: 10, epochPeriod: 10, veaInbox, @@ -47,19 +65,38 @@ describe("claimer", () => { transactionHandler: null, emitter, fetchLatestClaimedEpoch: mockGetLatestClaimedEpoch, + now: 110000, // (epoch+ 1) * epochPeriod * 1000 for claimable epoch + }; + + mockTransactionHandler = { + withdrawClaimDeposit: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.withdrawClaimDepositTxn = mockTransactions.withdrawClaimDepositTxn; + return Promise.resolve(); + }), + makeClaim: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.claimTxn = mockTransactions.claimTxn; + return Promise.resolve(); + }), + startVerification: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.startVerificationTxn = mockTransactions.startVerificationTxn; + return Promise.resolve(); + }), + verifySnapshot: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.verifySnapshotTxn = mockTransactions.verifySnapshotTxn; + return Promise.resolve(); + }), + transactions: { + claimTxn: "0x0", + withdrawClaimDepositTxn: "0x0", + startVerificationTxn: "0x0", + verifySnapshotTxn: "0x0", + }, }; }); afterEach(() => { jest.clearAllMocks(); }); describe("checkAndClaim", () => { - let mockTransactionHandler: any; - const mockTransactions = { - claimTxn: "0x111", - withdrawClaimDepositTxn: "0x222", - startVerificationTxn: "0x333", - verifySnapshotTxn: "0x444", - }; beforeEach(() => { mockTransactionHandler = { withdrawClaimDeposit: jest.fn().mockImplementation(() => { @@ -78,6 +115,10 @@ describe("claimer", () => { mockTransactionHandler.transactions.verifySnapshotTxn = mockTransactions.verifySnapshotTxn; return Promise.resolve(); }), + devnetAdvanceState: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.devnetAdvanceStateTxn = mockTransactions.devnetAdvanceStateTxn; + return Promise.resolve(); + }), transactions: { claimTxn: "0x0", withdrawClaimDepositTxn: "0x0", @@ -85,10 +126,16 @@ describe("claimer", () => { verifySnapshotTxn: "0x0", }, }; + mockGetTransactionHandler = jest.fn().mockReturnValue(function DummyTransactionHandler(param: any) { + return mockTransactionHandler; + }); + mockDeps.fetchTransactionHandler = mockGetTransactionHandler; }); it("should return null if no claim is made for a passed epoch", async () => { mockDeps.epoch = 7; // claimable epoch - 3 mockDeps.claim = null; + + mockDeps.fetchTransactionHandler = mockGetTransactionHandler; const result = await checkAndClaim(mockDeps); expect(result).toBeNull(); }); @@ -114,51 +161,74 @@ describe("claimer", () => { const result = await checkAndClaim(mockDeps); expect(result).toBeNull(); }); - it("should make a valid claim if no claim is made", async () => { - veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890"); - mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ - challenged: false, - stateroot: mockClaim.stateRoot, + describe("devnet", () => { + beforeEach(() => { + mockDeps.network = Network.DEVNET; }); - mockDeps.transactionHandler = mockTransactionHandler; - mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; - mockDeps.claim = null; - mockDeps.veaInbox = veaInbox; - const result = await checkAndClaim(mockDeps); - expect(result.transactions.claimTxn).toBe(mockTransactions.claimTxn); - }); - it("should make a valid claim if last claim was challenged", async () => { - veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot); - mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ - challenged: true, - stateroot: mockClaim.stateRoot, + it("should make a valid claim and advance state", async () => { + veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890"); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: false, + stateroot: mockClaim.stateRoot, + }); + mockDeps.transactionHandler = mockTransactionHandler; + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + mockDeps.claim = null; + mockDeps.veaInbox = veaInbox; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.devnetAdvanceStateTxn).toBe(mockTransactions.devnetAdvanceStateTxn); }); - mockDeps.transactionHandler = mockTransactionHandler; - mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; - mockDeps.claim = null; - mockDeps.veaInbox = veaInbox; - const result = await checkAndClaim(mockDeps); - expect(result.transactions.claimTxn).toEqual(mockTransactions.claimTxn); - }); - it("should withdraw claim deposit if claimer is honest", async () => { - mockDeps.transactionHandler = mockTransactionHandler; - mockClaim.honest = ClaimHonestState.CLAIMER; - const result = await checkAndClaim(mockDeps); - expect(result.transactions.withdrawClaimDepositTxn).toEqual(mockTransactions.withdrawClaimDepositTxn); - }); - it("should start verification if verification is not started", async () => { - mockDeps.transactionHandler = mockTransactionHandler; - mockClaim.honest = ClaimHonestState.NONE; - const result = await checkAndClaim(mockDeps); - expect(result.transactions.startVerificationTxn).toEqual(mockTransactions.startVerificationTxn); }); - it("should verify snapshot if verification is started", async () => { - mockDeps.transactionHandler = mockTransactionHandler; - mockClaim.honest = ClaimHonestState.NONE; - mockClaim.timestampVerification = 1234; - mockDeps.claim = mockClaim; - const result = await checkAndClaim(mockDeps); - expect(result.transactions.verifySnapshotTxn).toEqual(mockTransactions.verifySnapshotTxn); + describe("testnet", () => { + beforeEach(() => { + mockDeps.network = Network.TESTNET; + }); + it("should make a valid claim if no claim is made", async () => { + veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890"); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: false, + stateroot: mockClaim.stateRoot, + }); + mockDeps.transactionHandler = mockTransactionHandler; + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + mockDeps.claim = null; + mockDeps.veaInbox = veaInbox; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.claimTxn).toBe(mockTransactions.claimTxn); + }); + it("should make a valid claim if last claim was challenged", async () => { + veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot); + mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({ + challenged: true, + stateroot: mockClaim.stateRoot, + }); + mockDeps.transactionHandler = mockTransactionHandler; + mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch; + mockDeps.claim = null; + mockDeps.veaInbox = veaInbox; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.claimTxn).toEqual(mockTransactions.claimTxn); + }); + it("should withdraw claim deposit if claimer is honest", async () => { + mockDeps.transactionHandler = mockTransactionHandler; + mockClaim.honest = ClaimHonestState.CLAIMER; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.withdrawClaimDepositTxn).toEqual(mockTransactions.withdrawClaimDepositTxn); + }); + it("should start verification if verification is not started", async () => { + mockDeps.transactionHandler = mockTransactionHandler; + mockClaim.honest = ClaimHonestState.NONE; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.startVerificationTxn).toEqual(mockTransactions.startVerificationTxn); + }); + it("should verify snapshot if verification is started", async () => { + mockDeps.transactionHandler = mockTransactionHandler; + mockClaim.honest = ClaimHonestState.NONE; + mockClaim.timestampVerification = 1234; + mockDeps.claim = mockClaim; + const result = await checkAndClaim(mockDeps); + expect(result.transactions.verifySnapshotTxn).toEqual(mockTransactions.verifySnapshotTxn); + }); }); }); }); diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 9466f8b3..418f1d49 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -9,7 +9,7 @@ import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth import { ArbToEthDevnetTransactionHandler } from "./transactionHandlerDevnet"; import { getTransactionHandler } from "../utils/ethers"; import { Network } from "../consts/bridgeRoutes"; -interface checkAndClaimParams { +interface CheckAndClaimParams { chainId: number; network: Network; claim: ClaimStruct | null; @@ -23,9 +23,11 @@ interface checkAndClaimParams { emitter: EventEmitter; fetchClaim?: typeof getClaim; fetchLatestClaimedEpoch?: typeof getLastClaimedEpoch; + fetchTransactionHandler?: typeof getTransactionHandler; + now?: number; } -export async function checkAndClaim({ +async function checkAndClaim({ chainId, network, claim, @@ -38,13 +40,14 @@ export async function checkAndClaim({ transactionHandler, emitter, fetchLatestClaimedEpoch = getLastClaimedEpoch, -}: checkAndClaimParams) { + fetchTransactionHandler = getTransactionHandler, + now = Date.now(), +}: CheckAndClaimParams) { let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); - const claimAbleEpoch = Math.floor(Date.now() / (1000 * epochPeriod)) - 1; - + const claimAbleEpoch = Math.floor(now / (1000 * epochPeriod)) - 1; if (!transactionHandler) { - const TransactionHandler = getTransactionHandler(chainId, network); + const TransactionHandler = fetchTransactionHandler(chainId, network); transactionHandler = new TransactionHandler({ network, epoch, @@ -66,16 +69,22 @@ export async function checkAndClaim({ if (network == Network.DEVNET) { const devnetTransactionHandler = transactionHandler as ArbToEthDevnetTransactionHandler; if (claim == null) { - [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); + [savedSnapshot, claimData] = await Promise.all([ + veaInbox.snapshots(epoch), + fetchLatestClaimedEpoch(veaOutbox.target), + ]); + newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; - lastClaimChallenged = claimData.challenged && savedSnapshot == outboxStateRoot; - if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { - await devnetTransactionHandler.devnetAdvanceState(outboxStateRoot); + if (newMessagesToBridge && savedSnapshot != ethers.ZeroHash) { + await devnetTransactionHandler.devnetAdvanceState(savedSnapshot); return devnetTransactionHandler; } } } else if (claim == null && epoch == claimAbleEpoch) { - [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]); + [savedSnapshot, claimData] = await Promise.all([ + veaInbox.snapshots(epoch), + fetchLatestClaimedEpoch(veaOutbox.target), + ]); newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; lastClaimChallenged = claimData.challenged && savedSnapshot == outboxStateRoot; if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { @@ -103,3 +112,5 @@ export async function checkAndClaim({ } return null; } + +export { checkAndClaim, CheckAndClaimParams }; diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index ffcdf735..68ab533c 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -1,15 +1,15 @@ +import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; import { ArbToEthTransactionHandler, ContractType, Transaction, MAX_PENDING_CONFIRMATIONS, - MAX_PENDING_TIME, + TransactionHandlerConstructor, } from "./transactionHandler"; import { MockEmitter, defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { ClaimNotSetError } from "../utils/errors"; -import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; -import { getBridgeConfig } from "../consts/bridgeRoutes"; +import { getBridgeConfig, Network } from "../consts/bridgeRoutes"; describe("ArbToEthTransactionHandler", () => { const chainId = 11155111; @@ -19,7 +19,8 @@ describe("ArbToEthTransactionHandler", () => { let veaInboxProvider: any; let veaOutboxProvider: any; let claim: ClaimStruct = null; - + let transactionHandlerParams: TransactionHandlerConstructor; + const mockEmitter = new MockEmitter(); beforeEach(() => { veaInboxProvider = { getTransactionReceipt: jest.fn(), @@ -48,17 +49,21 @@ describe("ArbToEthTransactionHandler", () => { honest: 0, challenger: "0x1234", }; + transactionHandlerParams = { + network: Network.TESTNET, + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter: mockEmitter, + claim: null, + }; }); describe("constructor", () => { it("should create a new TransactionHandler without claim", () => { - const transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider - ); + const transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); expect(transactionHandler).toBeDefined(); expect(transactionHandler.epoch).toEqual(epoch); expect(transactionHandler.veaOutbox).toEqual(veaOutbox); @@ -66,15 +71,8 @@ describe("ArbToEthTransactionHandler", () => { }); it("should create a new TransactionHandler with claim", () => { - const transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - defaultEmitter, - claim - ); + transactionHandlerParams.claim = claim; + const transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); expect(transactionHandler).toBeDefined(); expect(transactionHandler.epoch).toEqual(epoch); expect(transactionHandler.veaOutbox).toEqual(veaOutbox); @@ -86,17 +84,9 @@ describe("ArbToEthTransactionHandler", () => { describe("checkTransactionStatus", () => { let transactionHandler: ArbToEthTransactionHandler; let finalityBlock: number = 100; - const mockEmitter = new MockEmitter(); let mockBroadcastedTimestamp: number = 1000; beforeEach(() => { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); veaInboxProvider.getBlock.mockResolvedValue({ number: finalityBlock }); }); @@ -112,7 +102,7 @@ describe("ArbToEthTransactionHandler", () => { mockBroadcastedTimestamp + 1 ); expect(status).toEqual(2); - expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_NOT_FINAL, trnx.hash, MAX_PENDING_CONFIRMATIONS - 1); + expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_NOT_FINAL, trnx.hash, 1); }); it("should return 1 if transaction is pending", async () => { @@ -164,14 +154,8 @@ describe("ArbToEthTransactionHandler", () => { const mockClaim = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; (mockClaim as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["claim(uint256,bytes32)"] = mockClaim; - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); veaOutbox.claim.mockResolvedValue({ hash: "0x1234" }); }); @@ -203,7 +187,8 @@ describe("ArbToEthTransactionHandler", () => { describe("startVerification", () => { let transactionHandler: ArbToEthTransactionHandler; const mockEmitter = new MockEmitter(); - const { epochPeriod, sequencerDelayLimit } = getBridgeConfig(chainId); + const { routeConfig, sequencerDelayLimit } = getBridgeConfig(chainId); + const epochPeriod = routeConfig[Network.TESTNET].epochPeriod; let startVerificationFlipTime: number; const mockStartVerification = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; (mockStartVerification as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); @@ -212,14 +197,7 @@ describe("ArbToEthTransactionHandler", () => { mockStartVerification; veaOutbox.startVerification.mockResolvedValue({ hash: "0x1234" }); startVerificationFlipTime = Number(claim.timestampClaimed) + epochPeriod + sequencerDelayLimit; - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); transactionHandler.claim = claim; }); @@ -269,14 +247,7 @@ describe("ArbToEthTransactionHandler", () => { (mockVerifySnapshot as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockVerifySnapshot; veaOutbox.verifySnapshot.mockResolvedValue({ hash: "0x1234" }); - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); verificationFlipTime = Number(claim.timestampVerification) + getBridgeConfig(chainId).minChallengePeriod; transactionHandler.claim = claim; }); @@ -326,14 +297,7 @@ describe("ArbToEthTransactionHandler", () => { (mockWithdrawClaimDeposit as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["withdrawClaimDeposit(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockWithdrawClaimDeposit; - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); veaOutbox.withdrawClaimDeposit.mockResolvedValue("0x1234"); transactionHandler.claim = claim; }); @@ -373,16 +337,8 @@ describe("ArbToEthTransactionHandler", () => { // Unhappy path (challenger) describe("challengeClaim", () => { let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); beforeEach(() => { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); transactionHandler.claim = claim; }); @@ -429,16 +385,8 @@ describe("ArbToEthTransactionHandler", () => { describe("withdrawChallengeDeposit", () => { let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); beforeEach(() => { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); veaOutbox.withdrawChallengeDeposit.mockResolvedValue("0x1234"); transactionHandler.claim = claim; }); @@ -483,16 +431,8 @@ describe("ArbToEthTransactionHandler", () => { describe("sendSnapshot", () => { let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); beforeEach(() => { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); transactionHandler.claim = claim; }); @@ -537,14 +477,7 @@ describe("ArbToEthTransactionHandler", () => { const mockEmitter = new MockEmitter(); beforeEach(() => { mockMessageExecutor = jest.fn(); - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); }); it("should resolve challenged claim", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index d715e5c6..4635ca7d 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -45,6 +45,7 @@ export type Transactions = { withdrawChallengeDepositTxn: Transaction | null; sendSnapshotTxn: Transaction | null; executeSnapshotTxn: Transaction | null; + devnetAdvanceStateTxn?: Transaction | null; }; export enum TransactionStatus { @@ -141,7 +142,7 @@ export class ArbToEthTransactionHandler { this.emitter.emit(BotEvents.TXN_FINAL, trnx.hash, confirmations); return TransactionStatus.FINAL; } - this.emitter.emit(BotEvents.TXN_NOT_FINAL, trnx.hash, confirmations); + this.emitter.emit(BotEvents.TXN_NOT_FINAL, trnx.hash, MAX_PENDING_CONFIRMATIONS - confirmations); return TransactionStatus.NOT_FINAL; } diff --git a/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts index 9aea2916..f352464d 100644 --- a/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts +++ b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts @@ -1,5 +1,4 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; -import { VeaInboxArbToEth, VeaOutboxArbToEthDevnet } from "@kleros/vea-contracts/typechain-types"; +import { VeaOutboxArbToEthDevnet } from "@kleros/vea-contracts/typechain-types"; import { ArbToEthTransactionHandler, ContractType, @@ -8,13 +7,15 @@ import { Transaction, TransactionHandlerConstructor, } from "./transactionHandler"; -import { defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; +import { getBridgeConfig } from "../consts/bridgeRoutes"; type DevnetTransactions = Transactions & { devnetAdvanceStateTxn: Transaction | null; }; +const CHAIN_ID = 11155111; + export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler { public veaOutboxDevnet: VeaOutboxArbToEthDevnet; public transactions: DevnetTransactions = { @@ -44,11 +45,10 @@ export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler veaOutboxProvider, emitter, } as TransactionHandlerConstructor); - this.veaOutboxDevnet = this.veaOutbox as VeaOutboxArbToEthDevnet; + this.veaOutboxDevnet = veaOutbox as VeaOutboxArbToEthDevnet; } public async devnetAdvanceState(stateRoot: string): Promise { this.emitter.emit(BotEvents.ADV_DEVNET, this.epoch); - const currentTime = Date.now(); const transactionStatus = await this.checkTransactionStatus( this.transactions.devnetAdvanceStateTxn, @@ -58,9 +58,9 @@ export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { return; } - const estimateGas = await this.veaOutbox["devnetAdvanceState(uint256,bytes32)"].estimateGas(this.epoch, this.claim); + const deposit = getBridgeConfig(CHAIN_ID).deposit; const startVerifTrx = await this.veaOutboxDevnet.devnetAdvanceState(this.epoch, stateRoot, { - gasLimit: estimateGas.mul(2), + value: deposit, }); this.emitter.emit(BotEvents.TXN_MADE, startVerifTrx.hash, this.epoch, "Advance Devnet State"); this.transactions.devnetAdvanceStateTxn = { From 025345efbb5ce0c4a0720a58a0d09555ff068d74 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:21:34 +0530 Subject: [PATCH 13/33] chore(validator): refactor for network support --- validator-cli/src/consts/bridgeRoutes.ts | 2 +- validator-cli/src/utils/ethers.ts | 44 +++++++----- validator-cli/src/watcher.ts | 89 ++++++------------------ 3 files changed, 52 insertions(+), 83 deletions(-) diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index b8867f58..f9e7769b 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -38,7 +38,7 @@ const arbToEthConfigs: { [key in Network]: RouteConfigs } = { [Network.DEVNET]: { veaInbox: veaInboxArbToEthDevnet, veaOutbox: veaOutboxArbToEthDevnet, - epochPeriod: 3600, + epochPeriod: 1800, }, [Network.TESTNET]: { veaInbox: veaInboxArbToEthTestnet, diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 880f84cb..63d9d6cd 100644 --- a/validator-cli/src/utils/ethers.ts +++ b/validator-cli/src/utils/ethers.ts @@ -2,6 +2,7 @@ import { Wallet, JsonRpcProvider } from "ethers"; import { VeaOutboxArbToEth__factory, VeaOutboxArbToGnosis__factory, + VeaOutboxArbToGnosisDevnet__factory, VeaOutboxArbToEthDevnet__factory, VeaInboxArbToEth__factory, VeaInboxArbToGnosis__factory, @@ -16,49 +17,60 @@ import { ArbToEthDevnetTransactionHandler } from "../ArbToEth/transactionHandler import { TransactionHandlerNotDefinedError } from "./errors"; import { Network } from "../consts/bridgeRoutes"; -function getWallet(privateKey: string, web3ProviderURL: string) { - return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL)); +function getWallet(privateKey: string, rpcUrl: string) { + return new Wallet(privateKey, new JsonRpcProvider(rpcUrl)); } function getWalletRPC(privateKey: string, rpc: JsonRpcProvider) { return new Wallet(privateKey, rpc); } -function getVeaInbox(veaInboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { +function getVeaInbox(veaInboxAddress: string, privateKey: string, rpcUrl: string, chainId: number, network) { switch (chainId) { case 11155111: - return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, rpcUrl)); case 10200: - return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, rpcUrl)); } } -function getVeaOutbox(veaOutboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { +function getVeaOutbox(veaOutboxAddress: string, privateKey: string, rpcUrl: string, chainId: number, network: Network) { switch (chainId) { case 11155111: - return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + switch (network) { + case Network.DEVNET: + return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + case Network.TESTNET: + return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + } + case 10200: - return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + switch (network) { + case Network.DEVNET: + return VeaOutboxArbToGnosisDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + case Network.TESTNET: + return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + } } } -function getVeaRouter(veaRouterAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { +function getVeaRouter(veaRouterAddress: string, privateKey: string, rpcUrl: string, chainId: number) { switch (chainId) { case 10200: - return RouterArbToGnosis__factory.connect(veaRouterAddress, getWallet(privateKey, web3ProviderURL)); + return RouterArbToGnosis__factory.connect(veaRouterAddress, getWallet(privateKey, rpcUrl)); } } -function getWETH(WETH: string, privateKey: string, web3ProviderURL: string) { - return IWETH__factory.connect(WETH, getWallet(privateKey, web3ProviderURL)); +function getWETH(WETH: string, privateKey: string, rpcUrl: string) { + return IWETH__factory.connect(WETH, getWallet(privateKey, rpcUrl)); } -function getVeaOutboxArbToEthDevnet(veaOutboxAddress: string, privateKey: string, web3ProviderURL: string) { - return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); +function getVeaOutboxArbToEthDevnet(veaOutboxAddress: string, privateKey: string, rpcUrl: string) { + return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); } -function getAMB(ambAddress: string, privateKey: string, web3ProviderURL: string) { - return IAMB__factory.connect(ambAddress, getWallet(privateKey, web3ProviderURL)); +function getAMB(ambAddress: string, privateKey: string, rpcUrl: string) { + return IAMB__factory.connect(ambAddress, getWallet(privateKey, rpcUrl)); } const getClaimValidator = (chainId: number, network: Network) => { diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index be896a9b..a9005aa7 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -28,31 +28,47 @@ export const watch = async ( const networkConfigs = getNetworkConfig(); emitter.emit(BotEvents.STARTED, path, networkConfigs[0].networks); const transactionHandlers: { [epoch: number]: any } = {}; - const watcherStarted: { chainId: number; network: string }[] = []; + const isWatched: { chainId: number; network: string }[] = []; while (!shutDownSignal.getIsShutdownSignal()) { for (const networkConfig of networkConfigs) { const { chainId, networks } = networkConfig; const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); for (const network of networks) { emitter.emit(BotEvents.WATCHING, chainId, network); - const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, process.env.PRIVATE_KEY, inboxRPC, chainId); + const veaInbox = getVeaInbox( + routeConfig[network].veaInbox.address, + process.env.PRIVATE_KEY, + inboxRPC, + chainId, + network + ); const veaOutbox = getVeaOutbox( routeConfig[network].veaOutbox.address, process.env.PRIVATE_KEY, outboxRPC, - chainId + chainId, + network ); const veaInboxProvider = new JsonRpcProvider(inboxRPC); const veaOutboxProvider = new JsonRpcProvider(outboxRPC); let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - var epochRange = setEpochRange(chainId, veaOutboxLatestBlock.timestamp, routeConfig[network].epochPeriod); + var epochRange = setEpochRange({ + chainId, + currentTimestamp: veaOutboxLatestBlock.timestamp, + epochPeriod: routeConfig[network].epochPeriod, + }); // If the watcher has already started, only check the latest epoch + console.log(isWatched); if ( - watcherStarted.find((watcher) => watcher.chainId == chainId && watcher.network == network) != null || + isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network) != null || network == Network.DEVNET ) { - epochRange = [epochRange[epochRange.length - 1]]; + if (network == Network.DEVNET) { + epochRange = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; + } else { + epochRange = [epochRange[epochRange.length - 1]]; + } } let i = epochRange.length - 1; while (i >= 0) { @@ -92,71 +108,12 @@ export const watch = async ( } i--; } + isWatched.push({ chainId, network }); } } await wait(1000 * 10); } }; -// const chainId = Number(process.env.VEAOUTBOX_CHAINS); - -// emitter.emit(BotEvents.STARTED, chainId, path); -// -// const veaInbox = getVeaInbox(veaBridge.inboxAddress, process.env.PRIVATE_KEY, veaBridge.inboxRPC, chainId); -// const veaOutbox = getVeaOutbox(veaBridge.outboxAddress, process.env.PRIVATE_KEY, veaBridge.outboxRPC, chainId); -// const veaInboxProvider = new JsonRpcProvider(veaBridge.inboxRPC); -// const veaOutboxProvider = new JsonRpcProvider(veaBridge.outboxRPC); -// const checkAndChallengeResolve = getClaimValidator(chainId); -// const checkAndClaim = getClaimer(chainId); -// const TransactionHandler = getTransactionHandler(chainId); - -// let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); -// const transactionHandlers: { [epoch: number]: InstanceType } = {}; -// const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); -// let latestEpoch = epochRange[epochRange.length - 1]; -// while (!shutDownSignal.getIsShutdownSignal()) { -// let i = 0; -// while (i < epochRange.length) { -// const epoch = epochRange[i]; -// emitter.emit(BotEvents.CHECKING, epoch); -// const epochBlock = await getBlockFromEpoch(epoch, veaBridge.epochPeriod, veaOutboxProvider); -// const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); -// const checkAndChallengeResolveDeps = { -// claim, -// epoch, -// epochPeriod: veaBridge.epochPeriod, -// veaInbox, -// veaInboxProvider, -// veaOutboxProvider, -// veaOutbox, -// transactionHandler: transactionHandlers[epoch], -// emitter, -// }; -// let updatedTransactions; -// if (path > BotPaths.CLAIMER && claim != null) { -// updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); -// } -// if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { -// updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); -// } - -// if (updatedTransactions) { -// transactionHandlers[epoch] = updatedTransactions; -// } else if (epoch != latestEpoch) { -// delete transactionHandlers[epoch]; -// epochRange.splice(i, 1); -// continue; -// } -// i++; -// } -// const newVerifiableEpoch = Math.floor(Date.now() / (1000 * veaBridge.epochPeriod)) - 1; -// if (newVerifiableEpoch > latestEpoch) { -// epochRange.push(newVerifiableEpoch); -// latestEpoch = newVerifiableEpoch; -// } else { -// emitter.emit(BotEvents.WAITING, latestEpoch); -// } -// await wait(1000 * 10); -// } const wait = (ms) => new Promise((r) => setTimeout(r, ms)); From 3a2ff607c9e4e2a5e3cdc826a961faa38bdd979e Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:22:53 +0530 Subject: [PATCH 14/33] chore(validator): update subgraph query for multi network --- validator-cli/src/utils/graphQueries.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts index a7b5ca6d..0ee412a9 100644 --- a/validator-cli/src/utils/graphQueries.ts +++ b/validator-cli/src/utils/graphQueries.ts @@ -14,14 +14,14 @@ interface ClaimData { * @param epoch * @returns ClaimData * */ -const getClaimForEpoch = async (epoch: number): Promise => { +const getClaimForEpoch = async (epoch: number, outbox: string): Promise => { try { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; const result = await request( `${subgraph}`, `{ - claims(where: {epoch: ${epoch}}) { + claims(where: {epoch: ${epoch}, outbox: "${outbox}"}) { id bridger stateroot @@ -42,13 +42,13 @@ const getClaimForEpoch = async (epoch: number): Promise = * Fetches the last claimed epoch (used for claimer - happy path) * @returns ClaimData */ -const getLastClaimedEpoch = async (): Promise => { +const getLastClaimedEpoch = async (outbox: string): Promise => { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; const result = await request( `${subgraph}`, `{ - claims(first:1, orderBy:timestamp, orderDirection:desc){ + claims(first:1, orderBy:timestamp, orderDirection:desc, where: {outbox: "${outbox}"}) { id bridger stateroot From f7ec86fe97e68c9499224a083ef9b6770964d9e0 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:24:09 +0530 Subject: [PATCH 15/33] chore(validator): type params & refactor --- validator-cli/src/utils/epochHandler.ts | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index 5ecac538..652db831 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -1,6 +1,14 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { getBridgeConfig } from "../consts/bridgeRoutes"; +interface EpochRangeParams { + chainId: number; + epochPeriod: number; + currentTimestamp: number; + now?: number; + fetchBridgeConfig?: typeof getBridgeConfig; +} + /** * Sets the epoch range to check for claims. * @@ -12,13 +20,13 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; * @returns The epoch range to check for claims */ -const setEpochRange = ( - chainId: number, - currentTimestamp: number, - epochPeriod: number, - now: number = Date.now(), - fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig -): Array => { +const setEpochRange = ({ + chainId, + currentTimestamp, + epochPeriod, + now = Date.now(), + fetchBridgeConfig = getBridgeConfig, +}: EpochRangeParams): Array => { const { sequencerDelayLimit } = fetchBridgeConfig(chainId); const coldStartBacklog = 7 * 24 * 60 * 60; // when starting the watcher, specify an extra backlog to check @@ -37,7 +45,6 @@ const setEpochRange = ( const veaEpochOutboxCheckClaimsRangeArray: number[] = new Array(veaEpochOutboxRange) .fill(veaEpochOutboxWatchLowerBound) .map((el, i) => el + i); - return [241886, 241887, 241888]; return veaEpochOutboxCheckClaimsRangeArray; }; @@ -53,12 +60,7 @@ const setEpochRange = ( * @example * currentEpoch = checkForNewEpoch(currentEpoch, 7200); */ -const getLatestChallengeableEpoch = ( - chainId: number, - epochPeriod: number, - now: number = Date.now(), - fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig -): number => { +const getLatestChallengeableEpoch = (epochPeriod: number, now: number = Date.now()): number => { return Math.floor(now / 1000 / epochPeriod) - 2; }; @@ -71,4 +73,4 @@ const getBlockFromEpoch = async (epoch: number, epochPeriod: number, provider: J return latestBlock.number - blockFallBack; }; -export { setEpochRange, getLatestChallengeableEpoch, getBlockFromEpoch }; +export { setEpochRange, getLatestChallengeableEpoch, getBlockFromEpoch, EpochRangeParams }; From 7b9ffa04a974ac2c6f060ac6db18963e05823dfc Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:24:27 +0530 Subject: [PATCH 16/33] chore(validator): update tests for refactor --- validator-cli/src/utils/epochHandler.test.ts | 24 ++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/validator-cli/src/utils/epochHandler.test.ts b/validator-cli/src/utils/epochHandler.test.ts index 23cb1ed9..92409384 100644 --- a/validator-cli/src/utils/epochHandler.test.ts +++ b/validator-cli/src/utils/epochHandler.test.ts @@ -1,4 +1,4 @@ -import { setEpochRange, getLatestChallengeableEpoch } from "./epochHandler"; +import { setEpochRange, getLatestChallengeableEpoch, EpochRangeParams } from "./epochHandler"; describe("epochHandler", () => { describe("setEpochRange", () => { @@ -17,7 +17,20 @@ describe("epochHandler", () => { epochPeriod: mockedEpochPeriod, sequencerDelayLimit: mockedSeqDelayLimit, })); - const result = setEpochRange(currentEpoch * mockedEpochPeriod, 1, now, mockedFetchBridgeConfig as any); + const mockParams: EpochRangeParams = { + chainId: 1, + currentTimestamp, + epochPeriod: mockedEpochPeriod, + now, + fetchBridgeConfig: mockedFetchBridgeConfig as any, + }; + const result = setEpochRange({ + chainId: 1, + currentTimestamp, + epochPeriod: mockedEpochPeriod, + now, + fetchBridgeConfig: mockedFetchBridgeConfig as any, + }); expect(result[result.length - 1]).toEqual(currentEpoch - 1); expect(result[0]).toEqual(startEpoch); }); @@ -25,12 +38,9 @@ describe("epochHandler", () => { describe("getLatestChallengeableEpoch", () => { it("should return the correct epoch number", () => { - const chainId = 1; const now = 1626325200000; - const fetchBridgeConfig = jest.fn(() => ({ - epochPeriod: 600, - })); - const result = getLatestChallengeableEpoch(chainId, now, fetchBridgeConfig as any); + const result = getLatestChallengeableEpoch(600, now); + expect(result).toEqual(now / (600 * 1000) - 2); }); }); From cd47f5f06f806c0e3794827a7e3d184c83882f9f Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:25:12 +0530 Subject: [PATCH 17/33] chore(validator): remove old watcher --- validator-cli/src/watcherDevnet.ts | 149 ----------------------------- 1 file changed, 149 deletions(-) delete mode 100644 validator-cli/src/watcherDevnet.ts diff --git a/validator-cli/src/watcherDevnet.ts b/validator-cli/src/watcherDevnet.ts deleted file mode 100644 index aae5662d..00000000 --- a/validator-cli/src/watcherDevnet.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { - VeaOutboxArbToEth__factory, - VeaInboxArbToEth__factory, - VeaInboxTouch__factory, -} from "@kleros/vea-contracts/typechain-types"; -import { WebSocketProvider, JsonRpcProvider } from "@ethersproject/providers"; -import { Wallet } from "@ethersproject/wallet"; -import { FlashbotsBundleProvider } from "@flashbots/ethers-provider-bundle"; -import { BigNumber } from "ethers"; -import { TransactionRequest } from "@ethersproject/abstract-provider"; - -require("dotenv").config(); - -const watch = async () => { - // connect to RPCs - const providerEth = new WebSocketProvider(process.env.RPC_ETH_WSS); - const providerArb = new JsonRpcProvider(process.env.RPC_ARB); - const signerArb = new Wallet(process.env.PRIVATE_KEY, providerArb); - const signerEth = new Wallet(process.env.PRIVATE_KEY, providerEth); - // `authSigner` is an Ethereum private key that does NOT store funds and is NOT your bot's primary key. - // This is an identifying key for signing payloads to establish reputation and whitelisting - // In production, this should be used across multiple bundles to build relationship. In this example, we generate a new wallet each time - const authSigner = new Wallet(process.env.FLASHBOTS_RELAY_SIGNING_KEY); - - // Flashbots provider requires passing in a standard provider - const flashbotsProvider = await FlashbotsBundleProvider.create( - providerEth, // a normal ethers.js provider, to perform gas estimations and nonce lookups - authSigner, // ethers.js signer wallet, only for signing request payloads, not transactions - "https://relay-sepolia.flashbots.net/", - "sepolia" - ); - - const veaInbox = VeaInboxArbToEth__factory.connect(process.env.VEAINBOX_ARB_TO_ETH_ADDRESS, signerArb); - const veaOutbox = VeaOutboxArbToEth__factory.connect(process.env.VEAOUTBOX_ARB_TO_ETH_ADDRESS, signerEth); - const veaInboxTouch = VeaInboxTouch__factory.connect(process.env.VEAINBOX_ARB_TO_ETH_TOUCH_ADDRESS, signerArb); - const epochPeriod = (await veaOutbox.epochPeriod()).toNumber(); - const deposit = await veaOutbox.deposit(); - const snapshotsFinalized = new Map(); - - let epochSnapshotFinalized: number = 0; - - //const gasEstimate = await retryOperation(() => veaOutbox.estimateGas["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"](epoch, claim, { value: deposit }), 1000, 10) as BigNumber; - const gasEstimate = 35000; // save time by hardcoding the gas estimate - - // deposit / 2 is the profit for challengers - // the initial challenge txn is roughly 1/3 of the cost of completing the challenge process. - const maxFeePerGasProfitable = deposit.div(gasEstimate * 3 * 2); - - veaOutbox.on(veaOutbox.filters["Claimed(address,uint256,bytes32)"](), async (claimer, epoch, stateRoot, event) => { - console.log("Claimed", claimer, epoch, stateRoot); - const block = event.getBlock(); - - var claim = { - stateRoot: stateRoot, - claimer: claimer, - timestampClaimed: (await block).timestamp, - timestampVerification: 0, - blocknumberVerification: 0, - honest: 0, - challenger: "0x0000000000000000000000000000000000000000", - }; - - if (epoch.toNumber() > epochSnapshotFinalized) { - // Math.random() is not cryptographically secure, but it's good enough for this purpose. - // can't set the seed, but multiplying by an unpredictable number (timestamp in ms) should be good enough. - const txnTouch = veaInboxTouch.touch(Math.floor(Math.random() * Date.now())); - - (await txnTouch).wait(); - - const snapshot = await veaInbox.snapshots(epoch); - - if (snapshot !== stateRoot) { - const data = veaOutbox.interface.encodeFunctionData( - "challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))", - [epoch, claim] - ); - - const tx: TransactionRequest = { - from: signerEth.address, - to: veaOutbox.address, - data: data, - value: deposit, - maxFeePerGas: maxFeePerGasProfitable, - maxPriorityFeePerGas: BigNumber.from(66666666667), // 66.7 gwei - gasLimit: BigNumber.from(35000), - }; - const privateTx = { - transaction: tx, - signer: signerEth, - }; - const res = await flashbotsProvider.sendPrivateTransaction(privateTx); - console.log(res); - } - } else if (snapshotsFinalized.get(epoch.toNumber()) !== stateRoot) { - const txnChallenge = veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"]( - epoch, - claim, - { - value: deposit, - gasLimit: gasEstimate, - maxFeePerGas: maxFeePerGasProfitable, - maxPriorityFeePerGas: BigNumber.from(66666666667), // 66.7 gwei - } - ); - console.log("Challenge txn", txnChallenge); - const txnReceiptChallenge = (await txnChallenge).wait(); - console.log("Challenge", txnReceiptChallenge); - } - }); - - epochSnapshotFinalized = Math.floor((await providerArb.getBlock("latest")).timestamp / epochPeriod) - 2; - - while (1) { - const blockLatestL2 = await providerArb.getBlock("latest"); - const timeL2 = blockLatestL2.timestamp; - const epochSnapshotFinalizedOld = epochSnapshotFinalized; - epochSnapshotFinalized = Math.floor(timeL2 / epochPeriod) - 1; - for (let epoch = epochSnapshotFinalizedOld + 1; epoch <= epochSnapshotFinalized; epoch++) { - const snapshot = await veaInbox.snapshots(epoch); - snapshotsFinalized.set(epoch, snapshot); - console.log("Snapshot finalized", epoch, snapshot); - } - await wait(3000); - } -}; - -const wait = (ms) => new Promise((r) => setTimeout(r, ms)); - -const retryOperation = (operation, delay, retries) => - new Promise((resolve, reject) => { - return operation() - .then(resolve) - .catch((reason) => { - if (retries > 0) { - // log retry - console.log("retrying", retries); - return wait(delay) - .then(retryOperation.bind(null, operation, delay, retries - 1)) - .then(resolve) - .catch(reject); - } - return reject(reason); - }); - }); - -(async () => { - await watch(); -})(); -export default watch; From 6e34daafb696912949414347bb22016c45bb86a4 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 12:27:35 +0530 Subject: [PATCH 18/33] fix(validator): existence check --- validator-cli/src/watcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index a9005aa7..93f2bed7 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -108,7 +108,9 @@ export const watch = async ( } i--; } - isWatched.push({ chainId, network }); + if (!isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network)) { + isWatched.push({ chainId, network }); + } } } await wait(1000 * 10); From d66c4552d67818e020ac6e5365a438d36be7acea Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 15:17:56 +0530 Subject: [PATCH 19/33] chore(validator): udpdate rpc in env example --- validator-cli/.env.dist | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/validator-cli/.env.dist b/validator-cli/.env.dist index 2d123711..c4fed13a 100644 --- a/validator-cli/.env.dist +++ b/validator-cli/.env.dist @@ -3,15 +3,11 @@ PRIVATE_KEY= # Networks: devnet, testnet, mainnet NETWORKS=devnet,testnet -# Devnet RPCs -RPC_CHIADO=https://rpc.chiadochain.net -RPC_ARB_SEPOLIA=https://sepolia-rollup.arbitrum.io/rpc -RPC_SEPOLIA= # Devnet Owner DEVNET_OWNER=0x5f4eC3Df9Cf2f0f1fDfCfCfCfCfCfCfCfCfCfCfC -# Testnet or Mainnet RPCs +# RPCs RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc RPC_ETH= RPC_GNOSIS=https://rpc.chiadochain.net From e4c617fdf80be1a20775250fc44bdd86ab0a92c8 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 15:19:19 +0530 Subject: [PATCH 20/33] chore(validator): add custom error --- validator-cli/src/utils/graphQueries.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts index 0ee412a9..89be2628 100644 --- a/validator-cli/src/utils/graphQueries.ts +++ b/validator-cli/src/utils/graphQueries.ts @@ -1,4 +1,5 @@ import request from "graphql-request"; +import { ClaimNotFoundError } from "./errors"; interface ClaimData { id: string; @@ -33,8 +34,7 @@ const getClaimForEpoch = async (epoch: number, outbox: string): Promise Date: Mon, 31 Mar 2025 16:19:30 +0530 Subject: [PATCH 21/33] chore(validator): pm2 bump & removed web3 dep --- validator-cli/ecosystem.config.js | 21 +- validator-cli/package.json | 6 +- validator-cli/src/consts/bridgeRoutes.ts | 4 +- validator-cli/src/utils/errors.ts | 17 +- validator-cli/src/utils/ethers.ts | 13 +- validator-cli/src/watcher.ts | 67 +++-- validator-cli/tsconfig.json | 7 +- yarn.lock | 365 +++++++---------------- 8 files changed, 186 insertions(+), 314 deletions(-) diff --git a/validator-cli/ecosystem.config.js b/validator-cli/ecosystem.config.js index 795f27f0..8b485a61 100644 --- a/validator-cli/ecosystem.config.js +++ b/validator-cli/ecosystem.config.js @@ -1,27 +1,16 @@ module.exports = { apps: [ { - name: "chiado-devnet", - script: "yarn", - args: "start-chiado-devnet", - interpreter: "/bin/bash", - log_date_format: "YYYY-MM-DD HH:mm Z", - watch: false, - autorestart: false, - env: { - NODE_ENV: "development", - }, - }, - { - name: "start-sepolia-devnet", - script: "yarn", - args: "start-sepolia-devnet", - interpreter: "/bin/bash", + name: "validator-cli", + script: "./src/watcher.ts", + interpreter: "../node_modules/.bin/ts-node", + interpreter_args: "--project tsconfig.json -r tsconfig-paths/register", log_date_format: "YYYY-MM-DD HH:mm Z", watch: false, autorestart: false, env: { NODE_ENV: "development", + TS_NODE_PROJECT: "./tsconfig.json", }, }, ], diff --git a/validator-cli/package.json b/validator-cli/package.json index c0334b5d..ee8481de 100644 --- a/validator-cli/package.json +++ b/validator-cli/package.json @@ -22,10 +22,8 @@ "@kleros/vea-contracts": "workspace:^", "@typechain/ethers-v6": "^0.5.1", "dotenv": "^16.4.5", - "pm2": "^5.2.2", - "typescript": "^4.9.5", - "web3": "^4.16.0", - "web3-batched-send": "^1.0.3" + "pm2": "^6.0.5", + "typescript": "^4.9.5" }, "devDependencies": { "@types/jest": "^29.5.14", diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index f9e7769b..b682888e 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -83,7 +83,9 @@ const bridges: { [chainId: number]: Bridge } = { }, }; -const getBridgeConfig = (chainId: number): Bridge | undefined => { +const getBridgeConfig = (chainId: number): Bridge => { + const bridge = bridges[chainId]; + if (!bridge) throw new Error(`Bridge not found for chain`); return bridges[chainId]; }; diff --git a/validator-cli/src/utils/errors.ts b/validator-cli/src/utils/errors.ts index e7042de0..0299afe3 100644 --- a/validator-cli/src/utils/errors.ts +++ b/validator-cli/src/utils/errors.ts @@ -16,11 +16,11 @@ class ClaimNotSetError extends Error { } } -class TransactionHandlerNotDefinedError extends Error { - constructor() { +class NotDefinedError extends Error { + constructor(param: string) { super(); this.name = "TransactionHandlerNotDefinedError"; - this.message = "TransactionHandler is not defined"; + this.message = `${param} is not defined`; } } @@ -48,11 +48,20 @@ class InvalidNetworkError extends Error { } } +class MissingEnvError extends Error { + constructor(envVar: string) { + super(); + this.name = "MissingEnvError"; + this.message = `Missing environment variable: ${envVar}`; + } +} + export { ClaimNotFoundError, ClaimNotSetError, - TransactionHandlerNotDefinedError, + NotDefinedError, InvalidBotPathError, DevnetOwnerNotSetError, InvalidNetworkError, + MissingEnvError, }; diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 63d9d6cd..9792916d 100644 --- a/validator-cli/src/utils/ethers.ts +++ b/validator-cli/src/utils/ethers.ts @@ -14,7 +14,7 @@ import { challengeAndResolveClaim as challengeAndResolveClaimArbToEth } from ".. import { checkAndClaim } from "../ArbToEth/claimer"; import { ArbToEthTransactionHandler } from "../ArbToEth/transactionHandler"; import { ArbToEthDevnetTransactionHandler } from "../ArbToEth/transactionHandlerDevnet"; -import { TransactionHandlerNotDefinedError } from "./errors"; +import { NotDefinedError, InvalidNetworkError } from "./errors"; import { Network } from "../consts/bridgeRoutes"; function getWallet(privateKey: string, rpcUrl: string) { @@ -77,9 +77,11 @@ const getClaimValidator = (chainId: number, network: Network) => { switch (chainId) { case 11155111: return challengeAndResolveClaimArbToEth; + default: + throw new NotDefinedError("Claim Validator"); } }; -const getClaimer = (chainId: number, network: Network) => { +const getClaimer = (chainId: number, network: Network): typeof checkAndClaim => { switch (chainId) { case 11155111: switch (network) { @@ -87,7 +89,12 @@ const getClaimer = (chainId: number, network: Network) => { case Network.TESTNET: return checkAndClaim; + + default: + throw new InvalidNetworkError(`${network}(claimer)`); } + default: + throw new NotDefinedError("Claimer"); } }; const getTransactionHandler = (chainId: number, network: Network) => { @@ -100,7 +107,7 @@ const getTransactionHandler = (chainId: number, network: Network) => { return ArbToEthTransactionHandler; } default: - throw new TransactionHandlerNotDefinedError(); + throw new NotDefinedError("Transaction Handler"); } }; export { diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index 93f2bed7..699e639b 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -1,6 +1,6 @@ import { JsonRpcProvider } from "@ethersproject/providers"; -import { getBridgeConfig, Bridge, Network } from "./consts/bridgeRoutes"; -import { getVeaInbox, getVeaOutbox, getTransactionHandler } from "./utils/ethers"; +import { getBridgeConfig, Network } from "./consts/bridgeRoutes"; +import { getVeaInbox, getVeaOutbox } from "./utils/ethers"; import { getBlockFromEpoch, setEpochRange } from "./utils/epochHandler"; import { getClaimValidator, getClaimer } from "./utils/ethers"; import { defaultEmitter } from "./utils/emitter"; @@ -9,6 +9,9 @@ import { initialize as initializeLogger } from "./utils/logger"; import { ShutdownSignal } from "./utils/shutdown"; import { getBotPath, BotPaths, getNetworkConfig } from "./utils/botConfig"; import { getClaim } from "./utils/claim"; +import { MissingEnvError } from "./utils/errors"; +import { CheckAndClaimParams } from "./ArbToEth/claimer"; +import { ChallengeAndResolveClaimParams } from "./ArbToEth/validator"; /** * @file This file contains the logic for watching a bridge and validating/resolving for claims. @@ -23,6 +26,8 @@ export const watch = async ( emitter: typeof defaultEmitter = defaultEmitter ) => { initializeLogger(emitter); + const privKey = process.env.PRIVATE_KEY; + if (!privKey) throw new MissingEnvError("PRIVATE_KEY"); const cliCommand = process.argv; const path = getBotPath({ cliCommand }); const networkConfigs = getNetworkConfig(); @@ -35,20 +40,8 @@ export const watch = async ( const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); for (const network of networks) { emitter.emit(BotEvents.WATCHING, chainId, network); - const veaInbox = getVeaInbox( - routeConfig[network].veaInbox.address, - process.env.PRIVATE_KEY, - inboxRPC, - chainId, - network - ); - const veaOutbox = getVeaOutbox( - routeConfig[network].veaOutbox.address, - process.env.PRIVATE_KEY, - outboxRPC, - chainId, - network - ); + const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, privKey, inboxRPC, chainId, network); + const veaOutbox = getVeaOutbox(routeConfig[network].veaOutbox.address, privKey, outboxRPC, chainId, network); const veaInboxProvider = new JsonRpcProvider(inboxRPC); const veaOutboxProvider = new JsonRpcProvider(outboxRPC); let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); @@ -59,7 +52,6 @@ export const watch = async ( }); // If the watcher has already started, only check the latest epoch - console.log(isWatched); if ( isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network) != null || network == Network.DEVNET @@ -76,28 +68,39 @@ export const watch = async ( let latestEpoch = epochRange[epochRange.length - 1]; const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); - const checkAndChallengeResolveDeps = { - network, - chainId, - claim, - epoch, - epochPeriod: routeConfig[network].epochPeriod, - veaInbox, - veaInboxProvider, - veaOutboxProvider, - veaOutbox, - transactionHandler: transactionHandlers[epoch], - emitter, - }; const checkAndChallengeResolve = getClaimValidator(chainId, network); const checkAndClaim = getClaimer(chainId, network); let updatedTransactions; if (path > BotPaths.CLAIMER && claim != null) { + const checkAndChallengeResolveDeps: ChallengeAndResolveClaimParams = { + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); } if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { - updatedTransactions = await checkAndClaim(checkAndChallengeResolveDeps); + const checkAndClaimParams: CheckAndClaimParams = { + network, + chainId, + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; + updatedTransactions = await checkAndClaim(checkAndClaimParams); } if (updatedTransactions) { @@ -117,7 +120,7 @@ export const watch = async ( } }; -const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +const wait = (ms: number): Promise => new Promise((resolve: () => void) => setTimeout(resolve, ms)); if (require.main === module) { const shutDownSignal = new ShutdownSignal(false); diff --git a/validator-cli/tsconfig.json b/validator-cli/tsconfig.json index 1a96b974..7922328e 100644 --- a/validator-cli/tsconfig.json +++ b/validator-cli/tsconfig.json @@ -3,7 +3,12 @@ "src" ], "compilerOptions": { + "baseUrl": ".", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, "resolveJsonModule": true, - "esModuleInterop": true + "allowSyntheticDefaultImports": true, + "outDir": "dist" } } diff --git a/yarn.lock b/yarn.lock index 4c6b0b21..9921cccd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,13 +19,6 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:^1.8.8": - version: 1.11.0 - resolution: "@adraffy/ens-normalize@npm:1.11.0" - checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb - languageName: node - linkType: hard - "@ampproject/remapping@npm:^2.2.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -1210,15 +1203,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/rlp@npm:^5.0.2": - version: 5.0.2 - resolution: "@ethereumjs/rlp@npm:5.0.2" - bin: - rlp: bin/rlp.cjs - checksum: 10/2af80d98faf7f64dfb6d739c2df7da7350ff5ad52426c3219897e843ee441215db0ffa346873200a6be6d11142edb9536e66acd62436b5005fa935baaf7eb6bd - languageName: node - linkType: hard - "@ethereumjs/tx@npm:3.5.2": version: 3.5.2 resolution: "@ethereumjs/tx@npm:3.5.2" @@ -3898,12 +3882,10 @@ __metadata: "@types/jest": "npm:^29.5.14" dotenv: "npm:^16.4.5" jest: "npm:^29.7.0" - pm2: "npm:^5.2.2" + pm2: "npm:^6.0.5" ts-jest: "npm:^29.2.5" ts-node: "npm:^10.9.2" typescript: "npm:^4.9.5" - web3: "npm:^4.16.0" - web3-batched-send: "npm:^1.0.3" languageName: unknown linkType: soft @@ -5610,6 +5592,26 @@ __metadata: languageName: node linkType: hard +"@pm2/agent@npm:~2.1.1": + version: 2.1.1 + resolution: "@pm2/agent@npm:2.1.1" + dependencies: + async: "npm:~3.2.0" + chalk: "npm:~3.0.0" + dayjs: "npm:~1.8.24" + debug: "npm:~4.3.1" + eventemitter2: "npm:~5.0.1" + fast-json-patch: "npm:^3.1.0" + fclone: "npm:~1.0.11" + pm2-axon: "npm:~4.0.1" + pm2-axon-rpc: "npm:~0.7.0" + proxy-agent: "npm:~6.4.0" + semver: "npm:~7.5.0" + ws: "npm:~7.5.10" + checksum: 10/8adc4e087bb69c609e98d1895cf4185483f14d8a6ea25cc3b62e9c13c6aa974582709fe51909ee3580976306ab14f84546b8e0156fd19c562dcf4548297671f8 + languageName: node + linkType: hard + "@pm2/io@npm:~6.0.1": version: 6.0.1 resolution: "@pm2/io@npm:6.0.1" @@ -5626,6 +5628,22 @@ __metadata: languageName: node linkType: hard +"@pm2/io@npm:~6.1.0": + version: 6.1.0 + resolution: "@pm2/io@npm:6.1.0" + dependencies: + async: "npm:~2.6.1" + debug: "npm:~4.3.1" + eventemitter2: "npm:^6.3.1" + require-in-the-middle: "npm:^5.0.0" + semver: "npm:~7.5.4" + shimmer: "npm:^1.2.0" + signal-exit: "npm:^3.0.3" + tslib: "npm:1.9.3" + checksum: 10/c95a1b4cf0b16877a503ed4eddcb94353db7f0dcfb39f59cac70cbad5c530a05a0d337602d38a7d38ffc185b142ec99f2ddf714a48ab9e1ec62f39a5760c9d74 + languageName: node + linkType: hard + "@pm2/js-api@npm:~0.8.0": version: 0.8.0 resolution: "@pm2/js-api@npm:0.8.0" @@ -6849,15 +6867,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:8.5.3": - version: 8.5.3 - resolution: "@types/ws@npm:8.5.3" - dependencies: - "@types/node": "npm:*" - checksum: 10/08aac698ce6480b532d8311f790a8744ae489ccdd98f374cfe4b8245855439825c64b031abcbba4f30fb280da6cc2b02a4e261e16341d058ffaeecaa24ba2bd3 - languageName: node - linkType: hard - "@types/ws@npm:^7.4.4": version: 7.4.7 resolution: "@types/ws@npm:7.4.7" @@ -7807,7 +7816,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^3.2.0, async@npm:^3.2.3, async@npm:~3.2.0": +"async@npm:^3.2.0, async@npm:^3.2.3, async@npm:~3.2.0, async@npm:~3.2.6": version: 3.2.6 resolution: "async@npm:3.2.6" checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368 @@ -9738,7 +9747,7 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.0, crc-32@npm:^1.2.2": +"crc-32@npm:^1.2.0": version: 1.2.2 resolution: "crc-32@npm:1.2.2" bin: @@ -10081,7 +10090,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:~1.11.5": +"dayjs@npm:~1.11.13, dayjs@npm:~1.11.5": version: 1.11.13 resolution: "dayjs@npm:1.11.13" checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 @@ -10118,7 +10127,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.4.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0": +"debug@npm:4, debug@npm:4.4.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7, debug@npm:^4.4.0": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -11975,7 +11984,7 @@ __metadata: languageName: node linkType: hard -"fast-json-patch@npm:^3.0.0-1": +"fast-json-patch@npm:^3.0.0-1, fast-json-patch@npm:^3.1.0": version: 3.1.1 resolution: "fast-json-patch@npm:3.1.1" checksum: 10/3e56304e1c95ad1862a50e5b3f557a74c65c0ff2ba5b15caab983b43e70e86ddbc5bc887e9f7064f0aacfd0f0435a29ab2f000fe463379e72b906486345e6671 @@ -13693,7 +13702,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -13754,7 +13763,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2, https-proxy-agent@npm:^7.0.6": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2, https-proxy-agent@npm:^7.0.3, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -18423,6 +18432,52 @@ __metadata: languageName: node linkType: hard +"pm2@npm:^6.0.5": + version: 6.0.5 + resolution: "pm2@npm:6.0.5" + dependencies: + "@pm2/agent": "npm:~2.1.1" + "@pm2/io": "npm:~6.1.0" + "@pm2/js-api": "npm:~0.8.0" + "@pm2/pm2-version-check": "npm:latest" + async: "npm:~3.2.6" + blessed: "npm:0.1.81" + chalk: "npm:3.0.0" + chokidar: "npm:^3.5.3" + cli-tableau: "npm:^2.0.0" + commander: "npm:2.15.1" + croner: "npm:~4.1.92" + dayjs: "npm:~1.11.13" + debug: "npm:^4.3.7" + enquirer: "npm:2.3.6" + eventemitter2: "npm:5.0.1" + fclone: "npm:1.0.11" + js-yaml: "npm:~4.1.0" + mkdirp: "npm:1.0.4" + needle: "npm:2.4.0" + pidusage: "npm:~3.0" + pm2-axon: "npm:~4.0.1" + pm2-axon-rpc: "npm:~0.7.1" + pm2-deploy: "npm:~1.0.2" + pm2-multimeter: "npm:^0.1.2" + pm2-sysmonit: "npm:^1.2.8" + promptly: "npm:^2" + semver: "npm:^7.6.2" + source-map-support: "npm:0.5.21" + sprintf-js: "npm:1.1.2" + vizion: "npm:~2.2.1" + dependenciesMeta: + pm2-sysmonit: + optional: true + bin: + pm2: bin/pm2 + pm2-dev: bin/pm2-dev + pm2-docker: bin/pm2-docker + pm2-runtime: bin/pm2-runtime + checksum: 10/1c45db52abf90c6e303c26eb1fd3688de0651625bb53232762b581b110d81dc1348bb273bfbe4603a106845837096cad09df492153615f2cfb07741d49dfb6d3 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -18659,6 +18714,22 @@ __metadata: languageName: node linkType: hard +"proxy-agent@npm:~6.4.0": + version: 6.4.0 + resolution: "proxy-agent@npm:6.4.0" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:^4.3.4" + http-proxy-agent: "npm:^7.0.1" + https-proxy-agent: "npm:^7.0.3" + lru-cache: "npm:^7.14.1" + pac-proxy-agent: "npm:^7.0.1" + proxy-from-env: "npm:^1.1.0" + socks-proxy-agent: "npm:^8.0.2" + checksum: 10/a22f202b74cc52f093efd9bfe52de8db08eda8bbc16b9d3d73acda2acc1b40223966e5521b1706788b06adf9265f093ed554d989b354e81b2d6ad482e5bd4d23 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -22434,27 +22505,7 @@ __metadata: languageName: node linkType: hard -"web3-core@npm:^4.4.0, web3-core@npm:^4.5.0, web3-core@npm:^4.6.0, web3-core@npm:^4.7.1": - version: 4.7.1 - resolution: "web3-core@npm:4.7.1" - dependencies: - web3-errors: "npm:^1.3.1" - web3-eth-accounts: "npm:^4.3.1" - web3-eth-iban: "npm:^4.0.7" - web3-providers-http: "npm:^4.2.0" - web3-providers-ipc: "npm:^4.0.7" - web3-providers-ws: "npm:^4.0.8" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - dependenciesMeta: - web3-providers-ipc: - optional: true - checksum: 10/c6b9447e62f5c57ccc3c96492adf5630cb3256968c15ce5675c660dec1f6da0bf60397efa88588029640f749ff45a1adaa0167a402ba0b4a46e600d8eda76334 - languageName: node - linkType: hard - -"web3-errors@npm:^1.1.3, web3-errors@npm:^1.2.0, web3-errors@npm:^1.3.0, web3-errors@npm:^1.3.1": +"web3-errors@npm:^1.2.0, web3-errors@npm:^1.3.1": version: 1.3.1 resolution: "web3-errors@npm:1.3.1" dependencies: @@ -22473,7 +22524,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-abi@npm:4.4.1, web3-eth-abi@npm:^4.4.1": +"web3-eth-abi@npm:4.4.1": version: 4.4.1 resolution: "web3-eth-abi@npm:4.4.1" dependencies: @@ -22504,21 +22555,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-accounts@npm:^4.3.1": - version: 4.3.1 - resolution: "web3-eth-accounts@npm:4.3.1" - dependencies: - "@ethereumjs/rlp": "npm:^4.0.1" - crc-32: "npm:^1.2.2" - ethereum-cryptography: "npm:^2.0.0" - web3-errors: "npm:^1.3.1" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/f8b689146c908d88b983bd467c3e794ed96e284490aa3f74e665580202db4f0826d4108f0aa95dc6ef1e14f9a8a41939ff2c4485e9713744dc6474d7082d9239 - languageName: node - linkType: hard - "web3-eth-contract@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-contract@npm:1.10.4" @@ -22535,22 +22571,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-contract@npm:^4.5.0, web3-eth-contract@npm:^4.7.2": - version: 4.7.2 - resolution: "web3-eth-contract@npm:4.7.2" - dependencies: - "@ethereumjs/rlp": "npm:^5.0.2" - web3-core: "npm:^4.7.1" - web3-errors: "npm:^1.3.1" - web3-eth: "npm:^4.11.1" - web3-eth-abi: "npm:^4.4.1" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/f5dd22199a69c6f10b0c38daee790341f80247a0155bad03e7c1a9ffad2d6c47722010b4fd0e3fe7832a43eb72a2fceadfd2892712ef199898c1e43067a92c0d - languageName: node - linkType: hard - "web3-eth-ens@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-ens@npm:1.10.4" @@ -22567,23 +22587,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-ens@npm:^4.4.0": - version: 4.4.0 - resolution: "web3-eth-ens@npm:4.4.0" - dependencies: - "@adraffy/ens-normalize": "npm:^1.8.8" - web3-core: "npm:^4.5.0" - web3-errors: "npm:^1.2.0" - web3-eth: "npm:^4.8.0" - web3-eth-contract: "npm:^4.5.0" - web3-net: "npm:^4.1.0" - web3-types: "npm:^1.7.0" - web3-utils: "npm:^4.3.0" - web3-validator: "npm:^2.0.6" - checksum: 10/25a1535e095d8ffcbc0641041af69e42aa60ba2989477108a5678c42a06135df9134ccc6024c89c216cb3408848e3905ee178d5b12e3bb740e895ee6ee0bd2cf - languageName: node - linkType: hard - "web3-eth-iban@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-iban@npm:1.10.4" @@ -22594,18 +22597,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-iban@npm:^4.0.7": - version: 4.0.7 - resolution: "web3-eth-iban@npm:4.0.7" - dependencies: - web3-errors: "npm:^1.1.3" - web3-types: "npm:^1.3.0" - web3-utils: "npm:^4.0.7" - web3-validator: "npm:^2.0.3" - checksum: 10/9d7521b4d4aef3a0d697905c7859d8e4d7ce82234320beecba9b24d254592a7ccf0354f329289b4e11a816fcbe3eceb842c4c87678f5e8ec622c8351bc1b9170 - languageName: node - linkType: hard - "web3-eth-personal@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-personal@npm:1.10.4" @@ -22620,20 +22611,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-personal@npm:^4.1.0": - version: 4.1.0 - resolution: "web3-eth-personal@npm:4.1.0" - dependencies: - web3-core: "npm:^4.6.0" - web3-eth: "npm:^4.9.0" - web3-rpc-methods: "npm:^1.3.0" - web3-types: "npm:^1.8.0" - web3-utils: "npm:^4.3.1" - web3-validator: "npm:^2.0.6" - checksum: 10/a560b0ef1f28961101c47824aa6fc71722c4e581ef5ffc5b68cf1b7db0fd5804032239f872a167a589b3c0ebe223353b8112b38e247e1f5b5ac48991e12f853c - languageName: node - linkType: hard - "web3-eth@npm:1.10.4": version: 1.10.4 resolution: "web3-eth@npm:1.10.4" @@ -22654,25 +22631,6 @@ __metadata: languageName: node linkType: hard -"web3-eth@npm:^4.11.1, web3-eth@npm:^4.8.0, web3-eth@npm:^4.9.0": - version: 4.11.1 - resolution: "web3-eth@npm:4.11.1" - dependencies: - setimmediate: "npm:^1.0.5" - web3-core: "npm:^4.7.1" - web3-errors: "npm:^1.3.1" - web3-eth-abi: "npm:^4.4.1" - web3-eth-accounts: "npm:^4.3.1" - web3-net: "npm:^4.1.0" - web3-providers-ws: "npm:^4.0.8" - web3-rpc-methods: "npm:^1.3.0" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/b39f5f1559a012ece0017f3976207ffb5358c4ebb2e8518721efcc4975005ed8948814613795d1ceee67eb28f33608cbc89f6b231534241052de231c6477ed17 - languageName: node - linkType: hard - "web3-net@npm:1.10.4": version: 1.10.4 resolution: "web3-net@npm:1.10.4" @@ -22684,18 +22642,6 @@ __metadata: languageName: node linkType: hard -"web3-net@npm:^4.1.0": - version: 4.1.0 - resolution: "web3-net@npm:4.1.0" - dependencies: - web3-core: "npm:^4.4.0" - web3-rpc-methods: "npm:^1.3.0" - web3-types: "npm:^1.6.0" - web3-utils: "npm:^4.3.0" - checksum: 10/2899ed28d9afda9f9faee6424752cb967dabf79128bce25321318e069a41571b9bd9477b480f290fd65f07cd6c0c641def0d72f31a730705112bd14c301f4e5e - languageName: node - linkType: hard - "web3-providers-http@npm:1.10.4": version: 1.10.4 resolution: "web3-providers-http@npm:1.10.4" @@ -22708,18 +22654,6 @@ __metadata: languageName: node linkType: hard -"web3-providers-http@npm:^4.2.0": - version: 4.2.0 - resolution: "web3-providers-http@npm:4.2.0" - dependencies: - cross-fetch: "npm:^4.0.0" - web3-errors: "npm:^1.3.0" - web3-types: "npm:^1.7.0" - web3-utils: "npm:^4.3.1" - checksum: 10/812b05d1e0dd8b6c5005bdcfe3c5fbddfe6cdd082bd2654dfe171ad98c3b7ff85b0bab371c70366d2bace2cf45fbf7d2f087b4cb281dbfa12372b902b8138eeb - languageName: node - linkType: hard - "web3-providers-ipc@npm:1.10.4": version: 1.10.4 resolution: "web3-providers-ipc@npm:1.10.4" @@ -22730,17 +22664,6 @@ __metadata: languageName: node linkType: hard -"web3-providers-ipc@npm:^4.0.7": - version: 4.0.7 - resolution: "web3-providers-ipc@npm:4.0.7" - dependencies: - web3-errors: "npm:^1.1.3" - web3-types: "npm:^1.3.0" - web3-utils: "npm:^4.0.7" - checksum: 10/b953818479f5d9c7b748e10977430fd7e377696f9160ae19b1917c0317e89671c4be824c06723b6fda190258927160fcec0e8e7c1aa87a5f0344008ef7649cda - languageName: node - linkType: hard - "web3-providers-ws@npm:1.10.4": version: 1.10.4 resolution: "web3-providers-ws@npm:1.10.4" @@ -22752,45 +22675,6 @@ __metadata: languageName: node linkType: hard -"web3-providers-ws@npm:^4.0.8": - version: 4.0.8 - resolution: "web3-providers-ws@npm:4.0.8" - dependencies: - "@types/ws": "npm:8.5.3" - isomorphic-ws: "npm:^5.0.0" - web3-errors: "npm:^1.2.0" - web3-types: "npm:^1.7.0" - web3-utils: "npm:^4.3.1" - ws: "npm:^8.17.1" - checksum: 10/9b9fa96fa1fc9455fb1b632de50f542d2589710002ea2cb0cd6a5c1ed9f72960d80ce219ac66b038ea6d0a767056fe653aa258a1c084aa78d5745870cc2703b4 - languageName: node - linkType: hard - -"web3-rpc-methods@npm:^1.3.0": - version: 1.3.0 - resolution: "web3-rpc-methods@npm:1.3.0" - dependencies: - web3-core: "npm:^4.4.0" - web3-types: "npm:^1.6.0" - web3-validator: "npm:^2.0.6" - checksum: 10/8c134b1f2ae1cf94d5c452c53fe699d5951c22c62ea82084559db06722a5f0db2047be4209172ff90432c42f70cf8081fea0ea85a024e4cbcd0e037efd9acfa8 - languageName: node - linkType: hard - -"web3-rpc-providers@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "web3-rpc-providers@npm:1.0.0-rc.4" - dependencies: - web3-errors: "npm:^1.3.1" - web3-providers-http: "npm:^4.2.0" - web3-providers-ws: "npm:^4.0.8" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/a6dff5ce76e6905eb3e8e7175984305b859a35f17ffad9511371e0840097cdccc4d8dd4a4bc893aeb78f93c22034b4c73cac79551a4d7cba204e55590018909b - languageName: node - linkType: hard - "web3-shh@npm:1.10.4": version: 1.10.4 resolution: "web3-shh@npm:1.10.4" @@ -22803,7 +22687,7 @@ __metadata: languageName: node linkType: hard -"web3-types@npm:^1.10.0, web3-types@npm:^1.3.0, web3-types@npm:^1.6.0, web3-types@npm:^1.7.0, web3-types@npm:^1.8.0": +"web3-types@npm:^1.10.0, web3-types@npm:^1.6.0": version: 1.10.0 resolution: "web3-types@npm:1.10.0" checksum: 10/849f05a001896b27082c5b5c46c62b65a28f463366eeec7223802418a61db6d3487ebfb73d1fe6dcad3f0849a76e20706098819cb4e266df4f75ca24617e62a1 @@ -22826,7 +22710,7 @@ __metadata: languageName: node linkType: hard -"web3-utils@npm:^4.0.7, web3-utils@npm:^4.3.0, web3-utils@npm:^4.3.1, web3-utils@npm:^4.3.3": +"web3-utils@npm:^4.3.3": version: 4.3.3 resolution: "web3-utils@npm:4.3.3" dependencies: @@ -22839,7 +22723,7 @@ __metadata: languageName: node linkType: hard -"web3-validator@npm:^2.0.3, web3-validator@npm:^2.0.6": +"web3-validator@npm:^2.0.6": version: 2.0.6 resolution: "web3-validator@npm:2.0.6" dependencies: @@ -22867,31 +22751,6 @@ __metadata: languageName: node linkType: hard -"web3@npm:^4.16.0": - version: 4.16.0 - resolution: "web3@npm:4.16.0" - dependencies: - web3-core: "npm:^4.7.1" - web3-errors: "npm:^1.3.1" - web3-eth: "npm:^4.11.1" - web3-eth-abi: "npm:^4.4.1" - web3-eth-accounts: "npm:^4.3.1" - web3-eth-contract: "npm:^4.7.2" - web3-eth-ens: "npm:^4.4.0" - web3-eth-iban: "npm:^4.0.7" - web3-eth-personal: "npm:^4.1.0" - web3-net: "npm:^4.1.0" - web3-providers-http: "npm:^4.2.0" - web3-providers-ws: "npm:^4.0.8" - web3-rpc-methods: "npm:^1.3.0" - web3-rpc-providers: "npm:^1.0.0-rc.4" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/8d63e70404914d2717d2675ba19350f112b07e50583a0703a68dd326eeb43a5c82b56f1165f4339cd89e697967581e0cd65fdb42ca0f1150fb7a3ce612f1a829 - languageName: node - linkType: hard - "webcrypto-core@npm:^1.8.0": version: 1.8.1 resolution: "webcrypto-core@npm:1.8.1" @@ -23226,7 +23085,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1": +"ws@npm:^8.12.0, ws@npm:^8.13.0": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: From f47ec8a53cb0f8ab736c8c1a4210f074b8811a2a Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 16:53:48 +0530 Subject: [PATCH 22/33] chore(validator): add no claim required log --- validator-cli/src/ArbToEth/claimer.ts | 2 ++ validator-cli/src/utils/botEvents.ts | 1 + validator-cli/src/utils/logger.ts | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 418f1d49..bf642588 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -43,6 +43,7 @@ async function checkAndClaim({ fetchTransactionHandler = getTransactionHandler, now = Date.now(), }: CheckAndClaimParams) { + console.log(epoch); let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); const claimAbleEpoch = Math.floor(now / (1000 * epochPeriod)) - 1; @@ -91,6 +92,7 @@ async function checkAndClaim({ await transactionHandler.makeClaim(savedSnapshot); return transactionHandler; } + emitter.emit(BotEvents.NO_CLAIM_REQUIRED, epoch); } else if (claim != null) { if (claim.honest == ClaimHonestState.CLAIMER) { await transactionHandler.withdrawClaimDeposit(); diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index ae7dc663..ffb67522 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -6,6 +6,7 @@ export enum BotEvents { WAITING = "waiting", NO_CLAIM = "no_claim", VALID_CLAIM = "valid_claim", + NO_CLAIM_REQUIRED = "no_claim_required", // Epoch state NO_NEW_MESSAGES = "no_new_messages", diff --git a/validator-cli/src/utils/logger.ts b/validator-cli/src/utils/logger.ts index 8918188a..ee791304 100644 --- a/validator-cli/src/utils/logger.ts +++ b/validator-cli/src/utils/logger.ts @@ -42,6 +42,10 @@ export const configurableInitialize = (emitter: EventEmitter) => { console.log(`Waiting for next verifiable epoch after ${epoch}`); }); + emitter.on(BotEvents.NO_CLAIM_REQUIRED, (epoch: number) => { + console.log(`No claim is required for epoch ${epoch}`); + }); + // Epoch state logs emitter.on(BotEvents.NO_SNAPSHOT, () => { console.log("No snapshot saved for epoch"); From b2bf8d8b9aa6f2c9793578bd1344b8b1f7d1e549 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 31 Mar 2025 16:54:24 +0530 Subject: [PATCH 23/33] fix(validator): claimable epoch calculation fix --- validator-cli/src/utils/epochHandler.test.ts | 4 ++-- validator-cli/src/utils/epochHandler.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/validator-cli/src/utils/epochHandler.test.ts b/validator-cli/src/utils/epochHandler.test.ts index 92409384..9d4e78af 100644 --- a/validator-cli/src/utils/epochHandler.test.ts +++ b/validator-cli/src/utils/epochHandler.test.ts @@ -11,7 +11,7 @@ describe("epochHandler", () => { const now = (currentTimestamp + mockedEpochPeriod + 1) * 1000; // In ms const startEpoch = Math.floor((currentTimestamp - (mockedSeqDelayLimit + mockedEpochPeriod + startCoolDown)) / mockedEpochPeriod) - - 2; + 1; it("should return the correct epoch range", () => { const mockedFetchBridgeConfig = jest.fn(() => ({ epochPeriod: mockedEpochPeriod, @@ -31,7 +31,7 @@ describe("epochHandler", () => { now, fetchBridgeConfig: mockedFetchBridgeConfig as any, }); - expect(result[result.length - 1]).toEqual(currentEpoch - 1); + expect(result[result.length - 1]).toEqual(currentEpoch); expect(result[0]).toEqual(startEpoch); }); }); diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index 652db831..97c22500 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -41,10 +41,11 @@ const setEpochRange = ({ let veaEpochOutboxClaimableNow = Math.floor(timeLocal / epochPeriod) - 1; // only past epochs are claimable, hence shift by one here - const veaEpochOutboxRange = veaEpochOutboxClaimableNow - veaEpochOutboxWatchLowerBound; - const veaEpochOutboxCheckClaimsRangeArray: number[] = new Array(veaEpochOutboxRange) - .fill(veaEpochOutboxWatchLowerBound) - .map((el, i) => el + i); + const length = veaEpochOutboxClaimableNow - veaEpochOutboxWatchLowerBound; + const veaEpochOutboxCheckClaimsRangeArray: number[] = Array.from( + { length }, + (_, i) => veaEpochOutboxWatchLowerBound + i + 1 + ); return veaEpochOutboxCheckClaimsRangeArray; }; From 13d6e2b2423ddd1ec3eeaef1105e5c2c80892d60 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 2 Apr 2025 18:13:07 +0530 Subject: [PATCH 24/33] feat(validator): subgraph fallback for contract logs --- validator-cli/src/ArbToEth/claimer.ts | 1 - validator-cli/src/utils/claim.ts | 123 +++++++++++++++--------- validator-cli/src/utils/graphQueries.ts | 94 ++++++++++++++++-- validator-cli/src/watcher.ts | 56 ++++++----- 4 files changed, 201 insertions(+), 73 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index bf642588..7f32a334 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -43,7 +43,6 @@ async function checkAndClaim({ fetchTransactionHandler = getTransactionHandler, now = Date.now(), }: CheckAndClaimParams) { - console.log(epoch); let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); const claimAbleEpoch = Math.floor(now / (1000 * epochPeriod)) - 1; diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 3a76e268..965179a6 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -3,6 +3,29 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { ethers } from "ethers"; import { ClaimNotFoundError } from "./errors"; import { getMessageStatus } from "./arbMsgExecutor"; +import { + getClaimForEpoch, + getChallengerForClaim, + getVerificationForClaim, + getSnapshotSentForEpoch, +} from "./graphQueries"; + +enum ClaimHonestState { + NONE = 0, + CLAIMER = 1, + CHALLENGER = 2, +} + +interface ClaimParams { + veaOutbox: any; + veaOutboxProvider: JsonRpcProvider; + epoch: number; + fromBlock: number; + toBlock: number | string; + fetchClaimForEpoch?: typeof getClaimForEpoch; + fetchVerificationForClaim?: typeof getVerificationForClaim; + fetchChallengerForClaim?: typeof getChallengerForClaim; +} /** * @@ -10,19 +33,16 @@ import { getMessageStatus } from "./arbMsgExecutor"; * @param epoch epoch number of the claim to be fetched * @returns claim type of ClaimStruct */ - -enum ClaimHonestState { - NONE = 0, - CLAIMER = 1, - CHALLENGER = 2, -} -const getClaim = async ( - veaOutbox: any, - veaOutboxProvider: JsonRpcProvider, - epoch: number, - fromBlock: number, - toBlock: number | string -): Promise => { +const getClaim = async ({ + veaOutbox, + veaOutboxProvider, + epoch, + fromBlock, + toBlock, + fetchChallengerForClaim = getChallengerForClaim, + fetchClaimForEpoch = getClaimForEpoch, + fetchVerificationForClaim = getVerificationForClaim, +}: ClaimParams): Promise => { let claim: ClaimStruct = { stateRoot: ethers.ZeroHash, claimer: ethers.ZeroAddress, @@ -35,24 +55,36 @@ const getClaim = async ( const claimHash = await veaOutbox.claimHashes(epoch); if (claimHash === ethers.ZeroHash) return null; - const [claimLogs, challengeLogs, verificationLogs] = await Promise.all([ - veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, epoch, null), fromBlock, toBlock), - veaOutbox.queryFilter(veaOutbox.filters.Challenged(epoch, null), fromBlock, toBlock), - veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(epoch), fromBlock, toBlock), - ]); - - if (claimLogs.length === 0) throw new ClaimNotFoundError(epoch); - - claim.stateRoot = claimLogs[0].data; - claim.claimer = `0x${claimLogs[0].topics[1].slice(26)}`; - claim.timestampClaimed = (await veaOutboxProvider.getBlock(claimLogs[0].blockNumber)).timestamp; - - if (verificationLogs.length > 0) { - claim.timestampVerification = (await veaOutboxProvider.getBlock(verificationLogs[0].blockNumber)).timestamp; - claim.blocknumberVerification = verificationLogs[0].blockNumber; - } - if (challengeLogs.length > 0) { - claim.challenger = "0x" + challengeLogs[0].topics[2].substring(26); + try { + const [claimLogs, challengeLogs, verificationLogs] = await Promise.all([ + veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, epoch, null), fromBlock, toBlock), + veaOutbox.queryFilter(veaOutbox.filters.Challenged(epoch, null), fromBlock, toBlock), + veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(epoch), fromBlock, toBlock), + ]); + claim.stateRoot = claimLogs[0].data; + claim.claimer = `0x${claimLogs[0].topics[1].slice(26)}`; + claim.timestampClaimed = (await veaOutboxProvider.getBlock(claimLogs[0].blockNumber)).timestamp; + if (verificationLogs.length > 0) { + claim.blocknumberVerification = verificationLogs[0].blockNumber; + claim.timestampVerification = (await veaOutboxProvider.getBlock(verificationLogs[0].blockNumber)).timestamp; + } + if (challengeLogs.length > 0) claim.challenger = "0x" + challengeLogs[0].topics[2].substring(26); + } catch (error) { + const claimFromGraph = await fetchClaimForEpoch(epoch, await veaOutbox.getAddress()); + const [verificationFromGraph, challengeFromGraph] = await Promise.all([ + fetchVerificationForClaim(claimFromGraph.id), + fetchChallengerForClaim(claimFromGraph.id), + ]); + claim.stateRoot = claimFromGraph.stateroot; + claim.claimer = claimFromGraph.bridger; + claim.timestampClaimed = claimFromGraph.timestamp; + if (verificationFromGraph && verificationFromGraph.startTimestamp) { + claim.timestampVerification = verificationFromGraph.startTimestamp; + const startVerificationTxHash = verificationFromGraph.startTxHash; + const txReceipt = await veaOutboxProvider.getTransactionReceipt(startVerificationTxHash); + claim.blocknumberVerification = txReceipt.blockNumber; + } + if (challengeFromGraph) claim.challenger = challengeFromGraph.challenger; } if (hashClaim(claim) == claimHash) { @@ -100,19 +132,24 @@ const getClaimResolveState = async ( toBlock: number | string, fetchMessageStatus: typeof getMessageStatus = getMessageStatus ): Promise => { - const sentSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSent(epoch, null), fromBlock, toBlock); - - const claimResolveState: ClaimResolveState = { - sendSnapshot: { status: false, txHash: "" }, - execution: { status: 0, txHash: "" }, - }; - - if (sentSnapshotLogs.length === 0) return claimResolveState; - - claimResolveState.sendSnapshot.status = true; - claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; + var claimResolveState: ClaimResolveState; + + try { + const sentSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSent(epoch, null), fromBlock, toBlock); + claimResolveState.sendSnapshot.status = true; + claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; + } catch (error) { + const sentSnapshotFromGraph = await getSnapshotSentForEpoch(epoch, await veaInbox.getAddress()); + console.log(sentSnapshotFromGraph); + if (sentSnapshotFromGraph) { + claimResolveState.sendSnapshot.status = true; + claimResolveState.sendSnapshot.txHash = sentSnapshotFromGraph.txHash; + } else { + return claimResolveState; + } + } - const status = await fetchMessageStatus(sentSnapshotLogs[0].transactionHash, veaInboxProvider, veaOutboxProvider); + const status = await fetchMessageStatus(claimResolveState.sendSnapshot.txHash, veaInboxProvider, veaOutboxProvider); claimResolveState.execution.status = status; return claimResolveState; diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts index 89be2628..a2cf91de 100644 --- a/validator-cli/src/utils/graphQueries.ts +++ b/validator-cli/src/utils/graphQueries.ts @@ -8,6 +8,12 @@ interface ClaimData { timestamp: number; challenged: boolean; txHash: string; + verification: { + timestamp: number; + }; + challenge: { + challenger: string; + }; } /** @@ -34,6 +40,7 @@ const getClaimForEpoch = async (epoch: number, outbox: string): Promise => { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; - - const result = await request( - `${subgraph}`, - `{ + try { + const result = await request( + `${subgraph}`, + `{ claims(first:1, orderBy:timestamp, orderDirection:desc, where: {outbox: "${outbox}"}) { id bridger @@ -58,8 +65,81 @@ const getLastClaimedEpoch = async (outbox: string): Promise => { } }` - ); - return result[`claims`][0]; + ); + return result[`claims`][0]; + } catch (e) { + console.log(e); + throw new ClaimNotFoundError(-1); + } +}; + +type VerificationData = { + startTimestamp: number | null; + startTxHash: string | null; +}; + +const getVerificationForClaim = async (claimId: string): Promise => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + const result = await request( + `${subgraph}`, + `{ + verifications(where: {claim: "${claimId}"}) { + startTimestamp + startTxHash + } + }` + ); + return result[`verifications`][0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +const getChallengerForClaim = async (claimId: string): Promise<{ challenger: string } | undefined> => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + const result = await request( + `${subgraph}`, + `{ + challenges(where: {claim: "${claimId}"}) { + challenger + } + }` + ); + return result[`challenges`][0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +const getSnapshotSentForEpoch = async (epoch: number, veaInbox: any): Promise<{ txHash: string }> => { + try { + const subgraph = process.env.VEAINBOX_SUBGRAPH; + const result = await request( + `${subgraph}`, + `{ + snapshots(where: {epoch: "${epoch}", inbox: "${veaInbox}"}) { + fallback{ + txHash + } + } + }` + ); + return result[`fallback`][0]; + } catch (e) { + console.log(e); + return undefined; + } }; -export { getClaimForEpoch, getLastClaimedEpoch, ClaimData }; +export { + getClaimForEpoch, + getLastClaimedEpoch, + getVerificationForClaim, + getChallengerForClaim, + getSnapshotSentForEpoch, + ClaimData, +}; diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index 699e639b..da793fc0 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -13,6 +13,8 @@ import { MissingEnvError } from "./utils/errors"; import { CheckAndClaimParams } from "./ArbToEth/claimer"; import { ChallengeAndResolveClaimParams } from "./ArbToEth/validator"; +const RPC_BLOCK_LIMIT = 500; // RPC_BLOCK_LIMIT is the limit of blocks that can be queried at once + /** * @file This file contains the logic for watching a bridge and validating/resolving for claims. * @@ -33,41 +35,47 @@ export const watch = async ( const networkConfigs = getNetworkConfig(); emitter.emit(BotEvents.STARTED, path, networkConfigs[0].networks); const transactionHandlers: { [epoch: number]: any } = {}; - const isWatched: { chainId: number; network: string }[] = []; + const toWatch: { [key: string]: number[] } = {}; while (!shutDownSignal.getIsShutdownSignal()) { for (const networkConfig of networkConfigs) { const { chainId, networks } = networkConfig; const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); for (const network of networks) { emitter.emit(BotEvents.WATCHING, chainId, network); + const networkKey = `${chainId}_${network}`; + if (!toWatch[networkKey]) { + toWatch[networkKey] = []; + } const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, privKey, inboxRPC, chainId, network); const veaOutbox = getVeaOutbox(routeConfig[network].veaOutbox.address, privKey, outboxRPC, chainId, network); const veaInboxProvider = new JsonRpcProvider(inboxRPC); const veaOutboxProvider = new JsonRpcProvider(outboxRPC); let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - var epochRange = setEpochRange({ - chainId, - currentTimestamp: veaOutboxLatestBlock.timestamp, - epochPeriod: routeConfig[network].epochPeriod, - }); // If the watcher has already started, only check the latest epoch - if ( - isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network) != null || - network == Network.DEVNET - ) { - if (network == Network.DEVNET) { - epochRange = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; - } else { - epochRange = [epochRange[epochRange.length - 1]]; - } + if (network == Network.DEVNET) { + toWatch[networkKey] = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; + } else if (toWatch[networkKey].length == 0) { + const epochRange = setEpochRange({ + chainId, + currentTimestamp: veaOutboxLatestBlock.timestamp, + epochPeriod: routeConfig[network].epochPeriod, + }); + toWatch[networkKey] = epochRange; } - let i = epochRange.length - 1; + + let i = toWatch[networkKey].length - 1; + const latestEpoch = toWatch[networkKey][i]; while (i >= 0) { - const epoch = epochRange[i]; - let latestEpoch = epochRange[epochRange.length - 1]; + const epoch = toWatch[networkKey][i]; const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); + const latestBlock = await veaOutboxProvider.getBlock("latest"); + var toBlock: number | string = "latest"; + if (latestBlock.number - epochBlock > RPC_BLOCK_LIMIT) { + toBlock = epochBlock + RPC_BLOCK_LIMIT; + } + + const claim = await getClaim({ veaOutbox, veaOutboxProvider, epoch, fromBlock: epochBlock, toBlock }); const checkAndChallengeResolve = getClaimValidator(chainId, network); const checkAndClaim = getClaimer(chainId, network); @@ -107,12 +115,16 @@ export const watch = async ( transactionHandlers[epoch] = updatedTransactions; } else if (epoch != latestEpoch) { delete transactionHandlers[epoch]; - epochRange.splice(i, 1); + toWatch[networkKey].splice(i, 1); } i--; } - if (!isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network)) { - isWatched.push({ chainId, network }); + const currentLatestBlock = await veaOutboxProvider.getBlock("latest"); + const currentLatestEpoch = Math.floor(currentLatestBlock.timestamp / routeConfig[network].epochPeriod); + const toWatchEpochs = toWatch[networkKey]; + const lastEpochInToWatch = toWatchEpochs[toWatchEpochs.length - 1]; + if (currentLatestEpoch > lastEpochInToWatch) { + toWatch[networkKey].push(currentLatestEpoch); } } } From 979394b232b84090a37620dffe9ba4db32055674 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 10:37:35 +0530 Subject: [PATCH 25/33] chore(validator): update tests for subgraph fallback --- validator-cli/src/utils/claim.test.ts | 71 ++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/validator-cli/src/utils/claim.test.ts b/validator-cli/src/utils/claim.test.ts index ed319105..6a018529 100644 --- a/validator-cli/src/utils/claim.test.ts +++ b/validator-cli/src/utils/claim.test.ts @@ -1,7 +1,8 @@ -import { ethers } from "ethers"; +import { ethers, getAddress } from "ethers"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; import { getClaim, hashClaim, getClaimResolveState } from "./claim"; import { ClaimNotFoundError } from "./errors"; +import { mock } from "node:test"; let mockClaim: ClaimStruct; // Pre calculated from the deployed contracts @@ -14,6 +15,10 @@ describe("snapshotClaim", () => { let veaOutbox: any; let veaOutboxProvider: any; const epoch = 1; + let getClaimForEpoch = jest.fn(); + let getVerificationForClaim = jest.fn(); + let getChallengerForClaim = jest.fn(); + let mockClaimParams: any; beforeEach(() => { mockClaim = { stateRoot: "0xeac817ed5c5b3d1c2c548f231b7cf9a0dfd174059f450ec6f0805acf6a16a551", @@ -32,14 +37,25 @@ describe("snapshotClaim", () => { Claimed: jest.fn(), }, claimHashes: jest.fn(), + getAddress: jest.fn(), }; veaOutboxProvider = { getBlock: jest.fn().mockResolvedValueOnce({ timestamp: mockClaim.timestampClaimed, number: 1234 }), }; + mockClaimParams = { + veaOutbox, + veaOutboxProvider, + epoch, + fromBlock: mockFromBlock, + toBlock: mockBlockTag, + fetchClaimForEpoch: getClaim, + fetchVerificationForClaim: getVerificationForClaim, + fetchChallengerForClaim: getChallengerForClaim, + }; }); it("should return a valid claim", async () => { - veaOutbox.claimHashes.mockResolvedValueOnce(hashedMockClaim); + veaOutbox.claimHashes = jest.fn().mockResolvedValueOnce(hashedMockClaim); veaOutbox.queryFilter .mockImplementationOnce(() => Promise.resolve([ @@ -52,8 +68,8 @@ describe("snapshotClaim", () => { ) // For Claimed .mockImplementationOnce(() => []) // For Challenged .mockImplementationOnce(() => Promise.resolve([])); // For VerificationStarted - - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, mockFromBlock, mockBlockTag); + mockClaimParams.veaOutbox = veaOutbox; + const claim = await getClaim(mockClaimParams); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); @@ -81,8 +97,8 @@ describe("snapshotClaim", () => { ]) ) // For Challenged .mockImplementationOnce(() => Promise.resolve([])); // For VerificationStartedß - - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, mockFromBlock, mockBlockTag); + mockClaimParams.veaOutbox = veaOutbox; + const claim = await getClaim(mockClaimParams); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); }); @@ -110,17 +126,44 @@ describe("snapshotClaim", () => { }, ]) ); // For VerificationStarted + mockClaimParams.veaOutbox = veaOutbox; + mockClaimParams.veaOutboxProvider = veaOutboxProvider; - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, mockFromBlock, mockBlockTag); + const claim = await getClaim(mockClaimParams); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); }); + it("should return a claim with fallback on subgraphs", async () => { + veaOutbox.claimHashes.mockResolvedValueOnce(hashedMockClaim); + + veaOutbox.queryFilter.mockImplementationOnce(() => { + throw new Error("Logs not available"); + }); + + const claimFromGraph = { + id: "1", + stateroot: mockClaim.stateRoot, + bridger: mockClaim.claimer, + timestamp: mockClaim.timestampClaimed, + }; + + const verificationFromGraph = null; + const challengeFromGraph = null; + + mockClaimParams.veaOutbox = veaOutbox; + mockClaimParams.fetchClaimForEpoch = jest.fn().mockResolvedValueOnce(claimFromGraph); + mockClaimParams.fetchVerificationForClaim = jest.fn().mockResolvedValueOnce(verificationFromGraph); + mockClaimParams.fetchChallengerForClaim = jest.fn().mockResolvedValueOnce(challengeFromGraph); + const claim = await getClaim(mockClaimParams); + expect(claim).toBeDefined(); + expect(claim).toEqual(mockClaim); + }); it("should return null if no claim is found", async () => { veaOutbox.claimHashes.mockResolvedValueOnce(ethers.ZeroHash); - - const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, mockFromBlock, mockBlockTag); + mockClaimParams.veaOutbox = veaOutbox; + const claim = await getClaim(mockClaimParams); expect(claim).toBeNull(); expect(veaOutbox.queryFilter).toHaveBeenCalledTimes(0); }); @@ -131,9 +174,12 @@ describe("snapshotClaim", () => { .mockImplementationOnce(() => Promise.resolve([])) .mockImplementationOnce(() => Promise.resolve([])) .mockImplementationOnce(() => Promise.resolve([])); - + mockClaimParams.fetchClaimForEpoch = jest.fn().mockResolvedValueOnce(null); + mockClaimParams.fetchVerificationForClaim = jest.fn().mockResolvedValueOnce(null); + mockClaimParams.fetchChallengerForClaim = jest.fn().mockResolvedValueOnce(null); + mockClaimParams.veaOutbox = veaOutbox; await expect(async () => { - await getClaim(veaOutbox, veaOutboxProvider, epoch, mockFromBlock, mockBlockTag); + await getClaim(mockClaimParams); }).rejects.toThrow(new ClaimNotFoundError(epoch)); }); @@ -152,9 +198,10 @@ describe("snapshotClaim", () => { ) // For Claimed .mockImplementationOnce(() => []) // For Challenged .mockImplementationOnce(() => Promise.resolve([])); // For VerificationStarted + mockClaimParams.veaOutbox = veaOutbox; await expect(async () => { - await getClaim(veaOutbox, veaOutboxProvider, epoch, mockFromBlock, mockBlockTag); + await getClaim(mockClaimParams); }).rejects.toThrow(new ClaimNotFoundError(epoch)); }); }); From de4b1b61158fc9d2225aa1e94e59e5e08ebce109 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 10:39:56 +0530 Subject: [PATCH 26/33] chore(validator): refactor for subgraph fallback --- validator-cli/src/ArbToEth/claimer.ts | 2 +- validator-cli/src/utils/claim.ts | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 7f32a334..00859572 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -86,7 +86,7 @@ async function checkAndClaim({ fetchLatestClaimedEpoch(veaOutbox.target), ]); newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; - lastClaimChallenged = claimData.challenged && savedSnapshot == outboxStateRoot; + lastClaimChallenged = claimData?.challenged && savedSnapshot == outboxStateRoot; if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { await transactionHandler.makeClaim(savedSnapshot); return transactionHandler; diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 965179a6..3f1909d5 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -71,6 +71,7 @@ const getClaim = async ({ if (challengeLogs.length > 0) claim.challenger = "0x" + challengeLogs[0].topics[2].substring(26); } catch (error) { const claimFromGraph = await fetchClaimForEpoch(epoch, await veaOutbox.getAddress()); + if (!claimFromGraph) throw new ClaimNotFoundError(epoch); const [verificationFromGraph, challengeFromGraph] = await Promise.all([ fetchVerificationForClaim(claimFromGraph.id), fetchChallengerForClaim(claimFromGraph.id), @@ -86,7 +87,6 @@ const getClaim = async ({ } if (challengeFromGraph) claim.challenger = challengeFromGraph.challenger; } - if (hashClaim(claim) == claimHash) { return claim; } @@ -132,15 +132,27 @@ const getClaimResolveState = async ( toBlock: number | string, fetchMessageStatus: typeof getMessageStatus = getMessageStatus ): Promise => { - var claimResolveState: ClaimResolveState; + var claimResolveState: ClaimResolveState = { + sendSnapshot: { + status: false, + txHash: "", + }, + execution: { + status: 0, + txHash: "", + }, + }; try { const sentSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSent(epoch, null), fromBlock, toBlock); - claimResolveState.sendSnapshot.status = true; - claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; + if (sentSnapshotLogs.length > 0) { + claimResolveState.sendSnapshot.status = true; + claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; + } else { + return claimResolveState; + } } catch (error) { const sentSnapshotFromGraph = await getSnapshotSentForEpoch(epoch, await veaInbox.getAddress()); - console.log(sentSnapshotFromGraph); if (sentSnapshotFromGraph) { claimResolveState.sendSnapshot.status = true; claimResolveState.sendSnapshot.txHash = sentSnapshotFromGraph.txHash; From ee90f378de041ce71efe52cfaa44ae4616a2a157 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 10:40:37 +0530 Subject: [PATCH 27/33] fix(validator): snapshot sent query fix --- validator-cli/src/utils/graphQueries.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts index a2cf91de..fe048b4e 100644 --- a/validator-cli/src/utils/graphQueries.ts +++ b/validator-cli/src/utils/graphQueries.ts @@ -115,20 +115,28 @@ const getChallengerForClaim = async (claimId: string): Promise<{ challenger: str } }; +type SenSnapshotResponse = { + snapshots: { + fallback: { txHash: string }[]; + }[]; +}; + const getSnapshotSentForEpoch = async (epoch: number, veaInbox: any): Promise<{ txHash: string }> => { try { const subgraph = process.env.VEAINBOX_SUBGRAPH; - const result = await request( + const veaInboxAddress = veaInbox.toLowerCase(); + + const result: SenSnapshotResponse = await request( `${subgraph}`, `{ - snapshots(where: {epoch: "${epoch}", inbox: "${veaInbox}"}) { + snapshots(where: {epoch: ${epoch}, inbox_: { id: "${veaInboxAddress}" }}) { fallback{ txHash } } }` ); - return result[`fallback`][0]; + return result.snapshots[0].fallback[0]; } catch (e) { console.log(e); return undefined; From c3d2a2347e48be3234308420ecc3a3bc056b0af3 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 10:41:24 +0530 Subject: [PATCH 28/33] chore(validator): update pm2 config --- validator-cli/ecosystem.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/validator-cli/ecosystem.config.js b/validator-cli/ecosystem.config.js index 8b485a61..9a238256 100644 --- a/validator-cli/ecosystem.config.js +++ b/validator-cli/ecosystem.config.js @@ -6,8 +6,8 @@ module.exports = { interpreter: "../node_modules/.bin/ts-node", interpreter_args: "--project tsconfig.json -r tsconfig-paths/register", log_date_format: "YYYY-MM-DD HH:mm Z", - watch: false, - autorestart: false, + watch: true, + autorestart: true, env: { NODE_ENV: "development", TS_NODE_PROJECT: "./tsconfig.json", From 4aa595a2b84367f297ea3e8307abcd883d66fe7c Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 10:45:20 +0530 Subject: [PATCH 29/33] chore(validator): remove legacy devnet code --- .../src/devnet/arbToChiado/happyPath.ts | 29 ------ .../src/devnet/arbToSepolia/happyPath.ts | 29 ------ validator-cli/src/utils/devnet.ts | 90 ------------------- 3 files changed, 148 deletions(-) delete mode 100644 validator-cli/src/devnet/arbToChiado/happyPath.ts delete mode 100644 validator-cli/src/devnet/arbToSepolia/happyPath.ts delete mode 100644 validator-cli/src/utils/devnet.ts diff --git a/validator-cli/src/devnet/arbToChiado/happyPath.ts b/validator-cli/src/devnet/arbToChiado/happyPath.ts deleted file mode 100644 index daf0ed9a..00000000 --- a/validator-cli/src/devnet/arbToChiado/happyPath.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { happyPath, initialize } from "../../utils/devnet"; - -require("dotenv").config(); - -(async () => { - let [veaInboxArbSepoliaToSepolia, epochPeriod, lastSavedCount, veaOutboxSepolia, deposit] = await initialize( - process.env.VEAOUTBOX_ARBSEPOLIA_TO_CHIADO_ADDRESS, - process.env.VEAINBOX_ARBSEPOLIA_TO_CHIADO_ADDRESS, - process.env.RPC_CHIADO - ); - - while (1) { - lastSavedCount = await happyPath( - veaInboxArbSepoliaToSepolia, - epochPeriod, - lastSavedCount, - veaOutboxSepolia, - deposit - ); - const currentTS = Math.floor(Date.now() / 1000); - const delayAmount = (epochPeriod - (currentTS % epochPeriod)) * 1000 + 30000; - console.log("waiting for the next epoch. . .", Math.floor(delayAmount / 1000), "seconds"); - await delay(delayAmount); - } -})(); - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/validator-cli/src/devnet/arbToSepolia/happyPath.ts b/validator-cli/src/devnet/arbToSepolia/happyPath.ts deleted file mode 100644 index 41097faa..00000000 --- a/validator-cli/src/devnet/arbToSepolia/happyPath.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { happyPath, initialize } from "../../utils/devnet"; - -require("dotenv").config(); - -(async () => { - let [veaInboxArbSepoliaToSepolia, epochPeriod, lastSavedCount, veaOutboxSepolia, deposit] = await initialize( - process.env.VEAOUTBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS, - process.env.VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS, - process.env.RPC_SEPOLIA - ); - - while (1) { - lastSavedCount = await happyPath( - veaInboxArbSepoliaToSepolia, - epochPeriod, - lastSavedCount, - veaOutboxSepolia, - deposit - ); - const currentTS = Math.floor(Date.now() / 1000); - const delayAmount = (epochPeriod - (currentTS % epochPeriod)) * 1000 + 30000; - console.log("waiting for the next epoch. . .", Math.floor(delayAmount / 1000), "seconds"); - await delay(delayAmount); - } -})(); - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/validator-cli/src/utils/devnet.ts b/validator-cli/src/utils/devnet.ts deleted file mode 100644 index a08f13de..00000000 --- a/validator-cli/src/utils/devnet.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ethers } from "ethers"; -import { getVeaInboxArbToEth, getVeaOutboxArbToEthDevnet } from "../utils/ethers"; -import { VeaInboxArbToEth, VeaOutboxArbToEthDevnet } from "@kleros/vea-contracts/typechain-types"; -import { JsonRpcProvider } from "@ethersproject/providers"; - -async function initialize( - veaOutboxAddress: string, - veaInboxAddress: string, - outboxRPCUrl: string -): Promise<[VeaInboxArbToEth, number, bigint, VeaOutboxArbToEthDevnet, bigint]> { - const outboxProvider = new JsonRpcProvider(outboxRPCUrl); - const veaOutbox = getVeaOutboxArbToEthDevnet(veaOutboxAddress, process.env.PRIVATE_KEY, outboxRPCUrl); - - const arbSepoliaProvider = new JsonRpcProvider(process.env.RPC_ARB_SEPOLIA); - const veaInbox = getVeaInboxArbToEth(veaInboxAddress, process.env.PRIVATE_KEY, process.env.RPC_ARB_SEPOLIA); - - const deposit = await veaOutbox.deposit(); - const epochPeriod = Number(await veaOutbox.epochPeriod()); - let currentTS = Math.floor(Date.now() / 1000); - let claimableEpoch = Math.floor(currentTS / epochPeriod); - - if (currentTS % epochPeriod < 60) { - console.log("Epoch is almost over. Waiting 1 min for next epoch..."); - await delay((currentTS % epochPeriod) * 1000); - claimableEpoch++; - } - - // only search back 2 weeks - // not really correct since l2 blocks are different, but just an estimate - const searchBlock = Math.max(0, (await arbSepoliaProvider.getBlockNumber()) - Math.floor(1209600 / 12)); - - const logs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSaved(null), searchBlock); - - let lastSavedCount = - logs.length > 0 - ? ethers.AbiCoder.defaultAbiCoder().decode(["bytes32", "uint256", "uint64"], logs[logs.length - 1].data)[2] - : BigInt(0); - return [veaInbox, epochPeriod, lastSavedCount, veaOutbox, deposit]; -} - -async function happyPath( - veaInbox: VeaInboxArbToEth, - epochPeriod: number, - lastSavedCount: bigint, - veaOutbox: VeaOutboxArbToEthDevnet, - deposit: bigint -): Promise { - let currentTS = Math.floor(Date.now() / 1000); - let claimableEpoch = Math.floor(currentTS / epochPeriod); - let newCount = lastSavedCount; - const snapshot = await veaInbox.snapshots(claimableEpoch); - - if (snapshot == "0x0000000000000000000000000000000000000000000000000000000000000000") { - // check if snapshot should be taken - const inboxCount: bigint = await veaInbox.count(); - if (inboxCount > lastSavedCount) { - // should take snapshot - console.log("inbox updated: taking snapshot. . ."); - const txn = await veaInbox.saveSnapshot(); - const receipt = await txn.wait(); - - newCount = BigInt(receipt.logs[0].data); - - const snapshot = await veaInbox.snapshots(claimableEpoch); - console.log(`Snapshot Txn: ${txn.hash}`); - console.log("snapshot count: ", receipt.logs[0].data); - lastSavedCount = inboxCount; - const txnOutbox = await veaOutbox.devnetAdvanceState(claimableEpoch, snapshot, { value: Number(deposit) }); - console.log(`DevnetAdvanceState Txn: ${txnOutbox.hash}`); - } else { - console.log("inbox not updated: not taking snapshot. . ."); - } - } else { - console.log("snapshot already taken. . ."); - const latestVerifiedEpoch = await veaOutbox.latestVerifiedEpoch(); - if (latestVerifiedEpoch < claimableEpoch) { - console.log("advancing devnet state. . ."); - const txnOutbox = await veaOutbox.devnetAdvanceState(claimableEpoch, snapshot, { value: Number(deposit) }); - console.log(`DevnetAdvanceState Txn: ${txnOutbox.hash}`); - } - } - - return newCount; -} - -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -export { happyPath, initialize }; From 28e609533f01af3c35164c1de7158d469fc19896 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 10:55:01 +0530 Subject: [PATCH 30/33] chore(validator): remove unsused vars --- validator-cli/.env.dist | 22 ++++------------------ validator-cli/src/utils/botConfig.ts | 7 +------ 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/validator-cli/.env.dist b/validator-cli/.env.dist index c4fed13a..0fa990da 100644 --- a/validator-cli/.env.dist +++ b/validator-cli/.env.dist @@ -4,31 +4,17 @@ PRIVATE_KEY= NETWORKS=devnet,testnet -# Devnet Owner -DEVNET_OWNER=0x5f4eC3Df9Cf2f0f1fDfCfCfCfCfCfCfCfCfCfCfC - # RPCs RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc RPC_ETH= RPC_GNOSIS=https://rpc.chiadochain.net -# Testnet or Mainnet Addresses -# VEA Arbitrum to Ethereum -VEAINBOX_ARB_TO_ETH_ADDRESS=0xE12daFE59Bc3A996362d54b37DFd2BA9279cAd06 -VEAOUTBOX_ARB_TO_ETH_ADDRESS=0x209BFdC6B7c66b63A8382196Ba3d06619d0F12c9 -# VEA Arbitrum to GNOSIS -VEAINBOX_ARB_TO_GNOSIS_ADDRESS=0x854374483572FFcD4d0225290346279d0718240b -VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS=0x2f1788F7B74e01c4C85578748290467A5f063B0b -VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193 + GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d VEAOUTBOX_CHAINS=11155111,421611 -VEAOUTBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/outbox-arb-sep/version/latest - -# Devnet Addresses -VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410 -VEAOUTBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410 +# vescan subgraph endpoints +VEAINBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/inbox-arb-sep/version/latest +VEAOUTBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/outbox-arb-sep/version/latest -TRANSACTION_BATCHER_CONTRACT_ADDRESS_SEPOLIA=0xe7953da7751063d0a41ba727c32c762d3523ade8 -TRANSACTION_BATCHER_CONTRACT_ADDRESS_CHIADO=0xcC0a08D4BCC5f91ee9a1587608f7a2975EA75d73 \ No newline at end of file diff --git a/validator-cli/src/utils/botConfig.ts b/validator-cli/src/utils/botConfig.ts index cee3ede9..c26a1854 100644 --- a/validator-cli/src/utils/botConfig.ts +++ b/validator-cli/src/utils/botConfig.ts @@ -40,7 +40,6 @@ export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathP interface NetworkConfig { chainId: number; networks: Network[]; - devnetOwner?: string; } /** @@ -49,18 +48,14 @@ interface NetworkConfig { */ export function getNetworkConfig(): NetworkConfig[] { const chainIds = process.env.VEAOUTBOX_CHAINS ? process.env.VEAOUTBOX_CHAINS.split(",") : []; - const devnetOwner = process.env.DEVNET_OWNER; const rawNetwork = process.env.NETWORKS ? process.env.NETWORKS.split(",") : []; const networks = validateNetworks(rawNetwork); - if (networks.includes(Network.DEVNET) && !devnetOwner) { - throw new DevnetOwnerNotSetError(); - } + const networkConfig: NetworkConfig[] = []; for (const chainId of chainIds) { networkConfig.push({ chainId: Number(chainId), networks, - devnetOwner, }); } return networkConfig; From 23937af6c95271b18cf801f02b619b8a4e79cd63 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 12:18:16 +0530 Subject: [PATCH 31/33] fix(validator): add default cases --- validator-cli/src/utils/ethers.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 9792916d..ccd08c78 100644 --- a/validator-cli/src/utils/ethers.ts +++ b/validator-cli/src/utils/ethers.ts @@ -31,6 +31,8 @@ function getVeaInbox(veaInboxAddress: string, privateKey: string, rpcUrl: string return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, rpcUrl)); case 10200: return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, rpcUrl)); + default: + throw new NotDefinedError("VeaInbox"); } } @@ -42,6 +44,8 @@ function getVeaOutbox(veaOutboxAddress: string, privateKey: string, rpcUrl: stri return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); case Network.TESTNET: return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + default: + throw new InvalidNetworkError(`${network}(veaOutbox)`); } case 10200: @@ -50,7 +54,11 @@ function getVeaOutbox(veaOutboxAddress: string, privateKey: string, rpcUrl: stri return VeaOutboxArbToGnosisDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); case Network.TESTNET: return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + default: + throw new InvalidNetworkError(`${network}(veaOutbox)`); } + default: + throw new NotDefinedError("VeaOutbox"); } } @@ -105,6 +113,8 @@ const getTransactionHandler = (chainId: number, network: Network) => { return ArbToEthDevnetTransactionHandler; case Network.TESTNET: return ArbToEthTransactionHandler; + default: + throw new InvalidNetworkError(`${network}(transactionHandler)`); } default: throw new NotDefinedError("Transaction Handler"); From c9dbe57efd6be9c082aeedb29be2e726e3eef2c8 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 13:15:24 +0530 Subject: [PATCH 32/33] chore(validator): refactor into helper ftns --- validator-cli/src/ArbToEth/claimer.ts | 117 ++++++++------ validator-cli/src/watcher.ts | 215 +++++++++++++++----------- 2 files changed, 200 insertions(+), 132 deletions(-) diff --git a/validator-cli/src/ArbToEth/claimer.ts b/validator-cli/src/ArbToEth/claimer.ts index 00859572..37f9f884 100644 --- a/validator-cli/src/ArbToEth/claimer.ts +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -44,7 +44,6 @@ async function checkAndClaim({ now = Date.now(), }: CheckAndClaimParams) { let outboxStateRoot = await veaOutbox.stateRoot(); - const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); const claimAbleEpoch = Math.floor(now / (1000 * epochPeriod)) - 1; if (!transactionHandler) { const TransactionHandler = fetchTransactionHandler(chainId, network); @@ -61,57 +60,87 @@ async function checkAndClaim({ } else { transactionHandler.claim = claim; } - var savedSnapshot; - var claimData; - var newMessagesToBridge: boolean; - var lastClaimChallenged: boolean; if (network == Network.DEVNET) { - const devnetTransactionHandler = transactionHandler as ArbToEthDevnetTransactionHandler; - if (claim == null) { - [savedSnapshot, claimData] = await Promise.all([ - veaInbox.snapshots(epoch), - fetchLatestClaimedEpoch(veaOutbox.target), - ]); - - newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; - if (newMessagesToBridge && savedSnapshot != ethers.ZeroHash) { - await devnetTransactionHandler.devnetAdvanceState(savedSnapshot); - return devnetTransactionHandler; - } - } + return makeClaimDevnet( + epoch, + claim, + outboxStateRoot, + transactionHandler as ArbToEthDevnetTransactionHandler, + veaInbox, + emitter + ); } else if (claim == null && epoch == claimAbleEpoch) { - [savedSnapshot, claimData] = await Promise.all([ - veaInbox.snapshots(epoch), - fetchLatestClaimedEpoch(veaOutbox.target), - ]); - newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; - lastClaimChallenged = claimData?.challenged && savedSnapshot == outboxStateRoot; - if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { - await transactionHandler.makeClaim(savedSnapshot); - return transactionHandler; - } - emitter.emit(BotEvents.NO_CLAIM_REQUIRED, epoch); + return makeClaim(epoch, transactionHandler, outboxStateRoot, veaInbox, veaOutbox, fetchLatestClaimedEpoch); } else if (claim != null) { - if (claim.honest == ClaimHonestState.CLAIMER) { - await transactionHandler.withdrawClaimDeposit(); - return transactionHandler; - } else if (claim.honest == ClaimHonestState.NONE) { - if (claim.challenger != ethers.ZeroAddress) { - emitter.emit(BotEvents.CLAIM_CHALLENGED, epoch); - return transactionHandler; - } - if (claim.timestampVerification == 0) { - await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); - } else { - await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp); - } - return transactionHandler; - } + return verifyClaim(transactionHandler, claim, veaOutboxProvider); } else { emitter.emit(BotEvents.CLAIM_EPOCH_PASSED, epoch); } return null; } +async function makeClaimDevnet( + epoch: number, + claim: ClaimStruct | null, + outboxStateRoot: string, + transactionHandler: ArbToEthDevnetTransactionHandler, + veaInbox: any, + emitter: EventEmitter +): Promise { + if (claim == null) { + const [savedSnapshot] = await Promise.all([veaInbox.snapshots(epoch)]); + + const newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + if (newMessagesToBridge && savedSnapshot != ethers.ZeroHash) { + await transactionHandler.devnetAdvanceState(savedSnapshot); + return transactionHandler; + } + } + emitter.emit(BotEvents.NO_CLAIM_REQUIRED, epoch); + return null; +} + +async function makeClaim( + epoch: number, + transactionHandler: ArbToEthTransactionHandler, + outboxStateRoot: string, + veaInbox: any, + veaOutbox: any, + fetchLatestClaimedEpoch: typeof getLastClaimedEpoch = getLastClaimedEpoch +): Promise { + const [savedSnapshot, claimData] = await Promise.all([ + veaInbox.snapshots(epoch), + fetchLatestClaimedEpoch(veaOutbox.target), + ]); + const newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + const lastClaimChallenged = claimData?.challenged && savedSnapshot == outboxStateRoot; + if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { + await transactionHandler.makeClaim(savedSnapshot); + return transactionHandler; + } +} + +async function verifyClaim( + transactionHandler: ArbToEthTransactionHandler, + claim: ClaimStruct, + veaOutboxProvider: JsonRpcProvider +) { + if (claim.honest == ClaimHonestState.CLAIMER) { + await transactionHandler.withdrawClaimDeposit(); + return transactionHandler; + } else if (claim.honest == ClaimHonestState.NONE) { + const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); + if (claim.challenger != ethers.ZeroAddress) { + return transactionHandler; + } + if (claim.timestampVerification == 0) { + await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); + } else { + await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp); + } + return transactionHandler; + } +} + export { checkAndClaim, CheckAndClaimParams }; diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index da793fc0..7984c85c 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -7,7 +7,7 @@ import { defaultEmitter } from "./utils/emitter"; import { BotEvents } from "./utils/botEvents"; import { initialize as initializeLogger } from "./utils/logger"; import { ShutdownSignal } from "./utils/shutdown"; -import { getBotPath, BotPaths, getNetworkConfig } from "./utils/botConfig"; +import { getBotPath, BotPaths, getNetworkConfig, NetworkConfig } from "./utils/botConfig"; import { getClaim } from "./utils/claim"; import { MissingEnvError } from "./utils/errors"; import { CheckAndClaimParams } from "./ArbToEth/claimer"; @@ -16,7 +16,7 @@ import { ChallengeAndResolveClaimParams } from "./ArbToEth/validator"; const RPC_BLOCK_LIMIT = 500; // RPC_BLOCK_LIMIT is the limit of blocks that can be queried at once /** - * @file This file contains the logic for watching a bridge and validating/resolving for claims. + * @file This file contains the logic for watching bridge and validating/resolving for claims. * * @param shutDownSignal - The signal to shut down the watcher * @param emitter - The emitter to emit events @@ -38,99 +38,138 @@ export const watch = async ( const toWatch: { [key: string]: number[] } = {}; while (!shutDownSignal.getIsShutdownSignal()) { for (const networkConfig of networkConfigs) { - const { chainId, networks } = networkConfig; - const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); - for (const network of networks) { - emitter.emit(BotEvents.WATCHING, chainId, network); - const networkKey = `${chainId}_${network}`; - if (!toWatch[networkKey]) { - toWatch[networkKey] = []; - } - const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, privKey, inboxRPC, chainId, network); - const veaOutbox = getVeaOutbox(routeConfig[network].veaOutbox.address, privKey, outboxRPC, chainId, network); - const veaInboxProvider = new JsonRpcProvider(inboxRPC); - const veaOutboxProvider = new JsonRpcProvider(outboxRPC); - let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); + await processNetwork(path, networkConfig, transactionHandlers, toWatch, emitter); + } + await wait(1000 * 10); + } +}; - // If the watcher has already started, only check the latest epoch - if (network == Network.DEVNET) { - toWatch[networkKey] = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; - } else if (toWatch[networkKey].length == 0) { - const epochRange = setEpochRange({ - chainId, - currentTimestamp: veaOutboxLatestBlock.timestamp, - epochPeriod: routeConfig[network].epochPeriod, - }); - toWatch[networkKey] = epochRange; - } +async function processNetwork( + path: number, + networkConfig: NetworkConfig, + transactionHandlers: { [epoch: number]: any }, + toWatch: { [key: string]: number[] }, + emitter: typeof defaultEmitter +): Promise { + const { chainId, networks } = networkConfig; + const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); + for (const network of networks) { + emitter.emit(BotEvents.WATCHING, chainId, network); + const networkKey = `${chainId}_${network}`; + if (!toWatch[networkKey]) { + toWatch[networkKey] = []; + } - let i = toWatch[networkKey].length - 1; - const latestEpoch = toWatch[networkKey][i]; - while (i >= 0) { - const epoch = toWatch[networkKey][i]; - const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); - const latestBlock = await veaOutboxProvider.getBlock("latest"); - var toBlock: number | string = "latest"; - if (latestBlock.number - epochBlock > RPC_BLOCK_LIMIT) { - toBlock = epochBlock + RPC_BLOCK_LIMIT; - } + const veaOutboxProvider = new JsonRpcProvider(outboxRPC); + let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - const claim = await getClaim({ veaOutbox, veaOutboxProvider, epoch, fromBlock: epochBlock, toBlock }); + // If the watcher has already started, only check the latest epoch + if (network == Network.DEVNET) { + toWatch[networkKey] = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; + } else if (toWatch[networkKey].length == 0) { + const epochRange = setEpochRange({ + chainId, + currentTimestamp: veaOutboxLatestBlock.timestamp, + epochPeriod: routeConfig[network].epochPeriod, + }); + toWatch[networkKey] = epochRange; + } - const checkAndChallengeResolve = getClaimValidator(chainId, network); - const checkAndClaim = getClaimer(chainId, network); - let updatedTransactions; - if (path > BotPaths.CLAIMER && claim != null) { - const checkAndChallengeResolveDeps: ChallengeAndResolveClaimParams = { - claim, - epoch, - epochPeriod: routeConfig[network].epochPeriod, - veaInbox, - veaInboxProvider, - veaOutboxProvider, - veaOutbox, - transactionHandler: transactionHandlers[epoch], - emitter, - }; - updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); - } - if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { - const checkAndClaimParams: CheckAndClaimParams = { - network, - chainId, - claim, - epoch, - epochPeriod: routeConfig[network].epochPeriod, - veaInbox, - veaInboxProvider, - veaOutboxProvider, - veaOutbox, - transactionHandler: transactionHandlers[epoch], - emitter, - }; - updatedTransactions = await checkAndClaim(checkAndClaimParams); - } + await processEpochsForNetwork( + chainId, + path, + networkKey, + network, + routeConfig, + inboxRPC, + outboxRPC, + toWatch, + transactionHandlers, + emitter + ); + const currentLatestBlock = await veaOutboxProvider.getBlock("latest"); + const currentLatestEpoch = Math.floor(currentLatestBlock.timestamp / routeConfig[network].epochPeriod); + const toWatchEpochs = toWatch[networkKey]; + const lastEpochInToWatch = toWatchEpochs[toWatchEpochs.length - 1]; + if (currentLatestEpoch > lastEpochInToWatch) { + toWatch[networkKey].push(currentLatestEpoch); + } + } +} - if (updatedTransactions) { - transactionHandlers[epoch] = updatedTransactions; - } else if (epoch != latestEpoch) { - delete transactionHandlers[epoch]; - toWatch[networkKey].splice(i, 1); - } - i--; - } - const currentLatestBlock = await veaOutboxProvider.getBlock("latest"); - const currentLatestEpoch = Math.floor(currentLatestBlock.timestamp / routeConfig[network].epochPeriod); - const toWatchEpochs = toWatch[networkKey]; - const lastEpochInToWatch = toWatchEpochs[toWatchEpochs.length - 1]; - if (currentLatestEpoch > lastEpochInToWatch) { - toWatch[networkKey].push(currentLatestEpoch); - } - } +async function processEpochsForNetwork( + chainId: number, + path: number, + networkKey: string, + network: Network, + routeConfig: any, + inboxRPC: string, + outboxRPC: string, + toWatch: { [key: string]: number[] }, + transactionHandlers: { [epoch: number]: any }, + emitter: typeof defaultEmitter +) { + const privKey = process.env.PRIVATE_KEY; + const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, privKey, inboxRPC, chainId, network); + const veaOutbox = getVeaOutbox(routeConfig[network].veaOutbox.address, privKey, outboxRPC, chainId, network); + const veaInboxProvider = new JsonRpcProvider(inboxRPC); + const veaOutboxProvider = new JsonRpcProvider(outboxRPC); + let i = toWatch[networkKey].length - 1; + const latestEpoch = toWatch[networkKey][i]; + while (i >= 0) { + const epoch = toWatch[networkKey][i]; + const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); + const latestBlock = await veaOutboxProvider.getBlock("latest"); + let toBlock: number | string = "latest"; + if (latestBlock.number - epochBlock > RPC_BLOCK_LIMIT) { + toBlock = epochBlock + RPC_BLOCK_LIMIT; } - await wait(1000 * 10); + + const claim = await getClaim({ veaOutbox, veaOutboxProvider, epoch, fromBlock: epochBlock, toBlock }); + + const checkAndChallengeResolve = getClaimValidator(chainId, network); + const checkAndClaim = getClaimer(chainId, network); + let updatedTransactions; + if (path > BotPaths.CLAIMER && claim != null) { + const checkAndChallengeResolveDeps: ChallengeAndResolveClaimParams = { + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; + updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); + } + if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { + const checkAndClaimParams: CheckAndClaimParams = { + network, + chainId, + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; + updatedTransactions = await checkAndClaim(checkAndClaimParams); + } + + if (updatedTransactions) { + transactionHandlers[epoch] = updatedTransactions; + } else if (epoch != latestEpoch) { + delete transactionHandlers[epoch]; + toWatch[networkKey].splice(i, 1); + } + i--; } -}; +} const wait = (ms: number): Promise => new Promise((resolve: () => void) => setTimeout(resolve, ms)); From 30b174dcff0d0b79f26b490baa33c5956ac48729 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 7 Apr 2025 13:19:00 +0530 Subject: [PATCH 33/33] fix(validator): sonar review fixes --- .../src/ArbToEth/transactionHandler.test.ts | 12 ++++-------- validator-cli/src/consts/bridgeRoutes.ts | 2 +- validator-cli/src/utils/botConfig.ts | 2 +- validator-cli/src/utils/claim.test.ts | 4 +--- validator-cli/src/utils/claim.ts | 8 ++++---- validator-cli/src/utils/epochHandler.test.ts | 8 +------- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index 68ab533c..713b580b 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -152,7 +152,7 @@ describe("ArbToEthTransactionHandler", () => { const { deposit } = getBridgeConfig(chainId); beforeEach(() => { const mockClaim = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; - (mockClaim as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + mockClaim.estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["claim(uint256,bytes32)"] = mockClaim; transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); @@ -186,12 +186,11 @@ describe("ArbToEthTransactionHandler", () => { describe("startVerification", () => { let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); const { routeConfig, sequencerDelayLimit } = getBridgeConfig(chainId); const epochPeriod = routeConfig[Network.TESTNET].epochPeriod; let startVerificationFlipTime: number; const mockStartVerification = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; - (mockStartVerification as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + mockStartVerification.estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); beforeEach(() => { veaOutbox["startVerification(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockStartVerification; @@ -241,10 +240,9 @@ describe("ArbToEthTransactionHandler", () => { describe("verifySnapshot", () => { let verificationFlipTime: number; let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); beforeEach(() => { const mockVerifySnapshot = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; - (mockVerifySnapshot as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + mockVerifySnapshot.estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockVerifySnapshot; veaOutbox.verifySnapshot.mockResolvedValue({ hash: "0x1234" }); transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); @@ -291,10 +289,9 @@ describe("ArbToEthTransactionHandler", () => { describe("withdrawClaimDeposit", () => { let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); beforeEach(() => { const mockWithdrawClaimDeposit = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; - (mockWithdrawClaimDeposit as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + mockWithdrawClaimDeposit.estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["withdrawClaimDeposit(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockWithdrawClaimDeposit; transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); @@ -474,7 +471,6 @@ describe("ArbToEthTransactionHandler", () => { describe("resolveChallengedClaim", () => { let mockMessageExecutor: any; let transactionHandler: ArbToEthTransactionHandler; - const mockEmitter = new MockEmitter(); beforeEach(() => { mockMessageExecutor = jest.fn(); transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index b682888e..a8b4153e 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -8,7 +8,7 @@ import veaOutboxArbToEthTestnet from "@kleros/vea-contracts/deployments/sepolia/ import veaInboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisDevnet.json"; import veaOutboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisDevnet.json"; -import veaInboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/VeaOutboxArbToEthTestnet.json"; +import veaInboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisTestnet.json"; import veaOutboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json"; import veaRouterArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/RouterArbToGnosisTestnet.json"; interface Bridge { diff --git a/validator-cli/src/utils/botConfig.ts b/validator-cli/src/utils/botConfig.ts index c26a1854..df90650e 100644 --- a/validator-cli/src/utils/botConfig.ts +++ b/validator-cli/src/utils/botConfig.ts @@ -37,7 +37,7 @@ export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathP return path ? pathMapping[path] : defaultPath; } -interface NetworkConfig { +export interface NetworkConfig { chainId: number; networks: Network[]; } diff --git a/validator-cli/src/utils/claim.test.ts b/validator-cli/src/utils/claim.test.ts index 6a018529..660e5553 100644 --- a/validator-cli/src/utils/claim.test.ts +++ b/validator-cli/src/utils/claim.test.ts @@ -1,8 +1,7 @@ -import { ethers, getAddress } from "ethers"; +import { ethers } from "ethers"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; import { getClaim, hashClaim, getClaimResolveState } from "./claim"; import { ClaimNotFoundError } from "./errors"; -import { mock } from "node:test"; let mockClaim: ClaimStruct; // Pre calculated from the deployed contracts @@ -15,7 +14,6 @@ describe("snapshotClaim", () => { let veaOutbox: any; let veaOutboxProvider: any; const epoch = 1; - let getClaimForEpoch = jest.fn(); let getVerificationForClaim = jest.fn(); let getChallengerForClaim = jest.fn(); let mockClaimParams: any; diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 3f1909d5..a13cbe96 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -69,7 +69,7 @@ const getClaim = async ({ claim.timestampVerification = (await veaOutboxProvider.getBlock(verificationLogs[0].blockNumber)).timestamp; } if (challengeLogs.length > 0) claim.challenger = "0x" + challengeLogs[0].topics[2].substring(26); - } catch (error) { + } catch { const claimFromGraph = await fetchClaimForEpoch(epoch, await veaOutbox.getAddress()); if (!claimFromGraph) throw new ClaimNotFoundError(epoch); const [verificationFromGraph, challengeFromGraph] = await Promise.all([ @@ -79,7 +79,7 @@ const getClaim = async ({ claim.stateRoot = claimFromGraph.stateroot; claim.claimer = claimFromGraph.bridger; claim.timestampClaimed = claimFromGraph.timestamp; - if (verificationFromGraph && verificationFromGraph.startTimestamp) { + if (verificationFromGraph?.startTimestamp) { claim.timestampVerification = verificationFromGraph.startTimestamp; const startVerificationTxHash = verificationFromGraph.startTxHash; const txReceipt = await veaOutboxProvider.getTransactionReceipt(startVerificationTxHash); @@ -132,7 +132,7 @@ const getClaimResolveState = async ( toBlock: number | string, fetchMessageStatus: typeof getMessageStatus = getMessageStatus ): Promise => { - var claimResolveState: ClaimResolveState = { + let claimResolveState: ClaimResolveState = { sendSnapshot: { status: false, txHash: "", @@ -151,7 +151,7 @@ const getClaimResolveState = async ( } else { return claimResolveState; } - } catch (error) { + } catch { const sentSnapshotFromGraph = await getSnapshotSentForEpoch(epoch, await veaInbox.getAddress()); if (sentSnapshotFromGraph) { claimResolveState.sendSnapshot.status = true; diff --git a/validator-cli/src/utils/epochHandler.test.ts b/validator-cli/src/utils/epochHandler.test.ts index 9d4e78af..88432caa 100644 --- a/validator-cli/src/utils/epochHandler.test.ts +++ b/validator-cli/src/utils/epochHandler.test.ts @@ -17,13 +17,7 @@ describe("epochHandler", () => { epochPeriod: mockedEpochPeriod, sequencerDelayLimit: mockedSeqDelayLimit, })); - const mockParams: EpochRangeParams = { - chainId: 1, - currentTimestamp, - epochPeriod: mockedEpochPeriod, - now, - fetchBridgeConfig: mockedFetchBridgeConfig as any, - }; + const result = setEpochRange({ chainId: 1, currentTimestamp,