diff --git a/validator-cli/README.md b/validator-cli/README.md index 3c9fe8cf..b9aaf670 100644 --- a/validator-cli/README.md +++ b/validator-cli/README.md @@ -1,4 +1,4 @@ -# bots +# Validator bot A collection of bots for the Vea challenger and bridger ecosystem. @@ -8,4 +8,29 @@ A collection of bots for the Vea challenger and bridger ecosystem. `pm2 start` -Runs watcher every minute, and challenges any false claims on the fast bridge receiver. +By default, the watcher performs two core functions: + +- Bridger: Submits stored snapshots to the fast bridge receiver. +- Challenger: Challenges any detected invalid claims. + +# flags + +`--saveSnapshot` + +Enables snapshot saving on the inbox when the bot observes a valid state. + +`--path=challenger | bridger | both` + +- challenger: Only challenge invalid claims +- bridger: Only submit snapshots +- both: Default mode, acts as both challenger and bridger + +# Example usage + +Run as both challenger and bridger with snapshots enabled: + +`pm2 start -- --saveSnapshot` + +Run only as challenger: + +`pm2 start dist/watcher.js -- --path=challenger` diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index 6b86a445..c1bb7ede 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -11,7 +11,7 @@ import veaOutboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/chiado 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 { +export interface Bridge { chain: string; minChallengePeriod: number; sequencerDelayLimit: number; @@ -30,7 +30,7 @@ type RouteConfigs = { deposit: bigint; }; -export enum Network { +enum Network { DEVNET = "devnet", TESTNET = "testnet", } @@ -39,7 +39,7 @@ const arbToEthConfigs: { [key in Network]: RouteConfigs } = { [Network.DEVNET]: { veaInbox: veaInboxArbToEthDevnet, veaOutbox: veaOutboxArbToEthDevnet, - epochPeriod: 1800, + epochPeriod: 300, deposit: BigInt("1000000000000000000"), }, [Network.TESTNET]: { @@ -54,7 +54,7 @@ const arbToGnosisConfigs: { [key in Network]: RouteConfigs } = { [Network.DEVNET]: { veaInbox: veaInboxArbToGnosisDevnet, veaOutbox: veaOutboxArbToGnosisDevnet, - epochPeriod: 1800, + epochPeriod: 300, deposit: BigInt("100000000000000000"), }, [Network.TESTNET]: { @@ -66,7 +66,7 @@ const arbToGnosisConfigs: { [key in Network]: RouteConfigs } = { }, }; -export const bridges: { [chainId: number]: Bridge } = { +const bridges: { [chainId: number]: Bridge } = { 11155111: { chain: "sepolia", minChallengePeriod: 10800, @@ -87,10 +87,16 @@ export const bridges: { [chainId: number]: Bridge } = { }, }; +// For the remaining time in an epoch the bot should save snapshots +const snapshotSavingPeriod = { + [Network.DEVNET]: 90, // 1m 30s + [Network.TESTNET]: 600, // 10 mins +}; + const getBridgeConfig = (chainId: number): Bridge => { const bridge = bridges[chainId]; if (!bridge) throw new Error(`Bridge not found for chain`); return bridges[chainId]; }; -export { getBridgeConfig, Bridge }; +export { bridges, getBridgeConfig, Network, snapshotSavingPeriod }; diff --git a/validator-cli/src/helpers/snapshot.test.ts b/validator-cli/src/helpers/snapshot.test.ts index a78f52a8..4164b015 100644 --- a/validator-cli/src/helpers/snapshot.test.ts +++ b/validator-cli/src/helpers/snapshot.test.ts @@ -1,9 +1,10 @@ -import { Network } from "../consts/bridgeRoutes"; +import { Network, snapshotSavingPeriod } from "../consts/bridgeRoutes"; import { isSnapshotNeeded, saveSnapshot } from "./snapshot"; import { MockEmitter } from "../utils/emitter"; describe("snapshot", () => { let veaInbox: any; + let veaOutbox: any; let count: number = 1; const chainId = 11155111; let fetchLastSavedMessage: jest.Mock; @@ -14,8 +15,12 @@ describe("snapshot", () => { filters: { SnapshotSaved: jest.fn(), }, + snapshots: jest.fn(), getAddress: jest.fn().mockResolvedValue("0x1"), }; + veaOutbox = { + stateRoot: jest.fn(), + }; }); describe("isSnapshotNeeded", () => { it("should return false and updated count when there are no new messages and count is -1 ", () => { @@ -27,9 +32,10 @@ describe("snapshot", () => { const params = { chainId, veaInbox, + veaOutbox, count, fetchLastSavedMessage, - }; + } as any; expect(isSnapshotNeeded(params)).resolves.toEqual({ snapshotNeeded: false, latestCount: currentCount, @@ -45,9 +51,10 @@ describe("snapshot", () => { const params = { chainId, veaInbox, + veaOutbox, count, fetchLastSavedMessage, - }; + } as any; expect(isSnapshotNeeded(params)).resolves.toEqual({ snapshotNeeded: false, latestCount: count, @@ -62,9 +69,10 @@ describe("snapshot", () => { const params = { chainId, veaInbox, + veaOutbox, count, fetchLastSavedMessage, - }; + } as any; expect(isSnapshotNeeded(params)).resolves.toEqual({ snapshotNeeded: false, latestCount: currentCount, @@ -79,9 +87,10 @@ describe("snapshot", () => { const params = { chainId, veaInbox, + veaOutbox, count, fetchLastSavedMessage, - }; + } as any; expect(isSnapshotNeeded(params)).resolves.toEqual({ snapshotNeeded: true, latestCount: currentCount, @@ -96,9 +105,30 @@ describe("snapshot", () => { const params = { chainId, veaInbox, + veaOutbox, count, fetchLastSavedMessage, - }; + } as any; + expect(isSnapshotNeeded(params)).resolves.toEqual({ + snapshotNeeded: true, + latestCount: currentCount, + }); + }); + it.only("should return true if claim was missed in previous epoch", async () => { + count = 1; + let currentCount = 3; + veaInbox.count.mockResolvedValue(currentCount); + fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3"); + veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed")); + veaOutbox.stateRoot.mockResolvedValue("0xabcde"); + veaInbox.snapshots.mockResolvedValue("0x0"); + const params = { + chainId, + veaInbox, + veaOutbox, + count, + fetchLastSavedMessage, + } as any; expect(isSnapshotNeeded(params)).resolves.toEqual({ snapshotNeeded: true, latestCount: currentCount, @@ -119,6 +149,7 @@ describe("snapshot", () => { const res = await saveSnapshot({ chainId, veaInbox, + veaOutbox, network, epochPeriod, count, @@ -146,6 +177,7 @@ describe("snapshot", () => { const res = await saveSnapshot({ chainId, veaInbox, + veaOutbox, network, epochPeriod, count, @@ -172,6 +204,7 @@ describe("snapshot", () => { const res = await saveSnapshot({ chainId, veaInbox, + veaOutbox, network, epochPeriod, count: -1, @@ -184,7 +217,8 @@ describe("snapshot", () => { expect(res).toEqual({ transactionHandler, latestCount: currentCount }); }); - it("should save snapshot if snapshot is needed at anytime for devnet", async () => { + it("should save snapshot in time limit for devnet", async () => { + const savingPeriod = snapshotSavingPeriod[Network.DEVNET]; const currentCount = 6; count = -1; veaInbox.count.mockResolvedValue(currentCount); @@ -192,13 +226,14 @@ describe("snapshot", () => { snapshotNeeded: true, latestCount: currentCount, }); - const now = 1801; // 600 seconds after the epoch started + const now = epochPeriod + epochPeriod - savingPeriod; // 60 seconds before the second epoch ends const transactionHandler = { saveSnapshot: jest.fn(), }; const res = await saveSnapshot({ chainId, veaInbox, + veaOutbox, network: Network.DEVNET, epochPeriod, count, diff --git a/validator-cli/src/helpers/snapshot.ts b/validator-cli/src/helpers/snapshot.ts index a37172b9..16708c52 100644 --- a/validator-cli/src/helpers/snapshot.ts +++ b/validator-cli/src/helpers/snapshot.ts @@ -1,11 +1,13 @@ -import { Network } from "../consts/bridgeRoutes"; +import { ZeroHash } from "ethers"; +import { Network, snapshotSavingPeriod } from "../consts/bridgeRoutes"; import { getLastMessageSaved } from "../utils/graphQueries"; import { BotEvents } from "../utils/botEvents"; import { defaultEmitter } from "../utils/emitter"; - interface SnapshotCheckParams { + epochPeriod: number; chainId: number; veaInbox: any; + veaOutbox: any; count: number; fetchLastSavedMessage?: typeof getLastMessageSaved; } @@ -13,6 +15,7 @@ interface SnapshotCheckParams { export interface SaveSnapshotParams { chainId: number; veaInbox: any; + veaOutbox: any; network: Network; epochPeriod: number; count: number; @@ -25,6 +28,7 @@ export interface SaveSnapshotParams { export const saveSnapshot = async ({ chainId, veaInbox, + veaOutbox, network, epochPeriod, count, @@ -33,18 +37,18 @@ export const saveSnapshot = async ({ toSaveSnapshot = isSnapshotNeeded, now = Math.floor(Date.now() / 1000), }: SaveSnapshotParams): Promise => { - if (network != Network.DEVNET) { - const timeElapsed = now % epochPeriod; - const timeLeftForEpoch = epochPeriod - timeElapsed; - // Saving snapshots in last 10 minutes of the epoch on testnet - if (timeLeftForEpoch > 600) { - emitter.emit(BotEvents.SNAPSHOT_WAITING, timeLeftForEpoch); - return { transactionHandler, latestCount: count }; - } + const timeElapsed = now % epochPeriod; + const timeLeftForEpoch = epochPeriod - timeElapsed; + + if (timeLeftForEpoch > snapshotSavingPeriod[network]) { + emitter.emit(BotEvents.SNAPSHOT_WAITING, timeLeftForEpoch); + return { transactionHandler, latestCount: count }; } const { snapshotNeeded, latestCount } = await toSaveSnapshot({ + epochPeriod, chainId, veaInbox, + veaOutbox, count, }); if (!snapshotNeeded) return { transactionHandler, latestCount }; @@ -53,28 +57,42 @@ export const saveSnapshot = async ({ }; export const isSnapshotNeeded = async ({ + epochPeriod, chainId, veaInbox, + veaOutbox, count, fetchLastSavedMessage = getLastMessageSaved, }: SnapshotCheckParams): Promise<{ snapshotNeeded: boolean; latestCount: number }> => { const currentCount = Number(await veaInbox.count()); + if (count == currentCount) { return { snapshotNeeded: false, latestCount: currentCount }; } let lastSavedCount: number; + let lastSavedSnapshot: string; try { const saveSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSaved()); lastSavedCount = Number(saveSnapshotLogs[saveSnapshotLogs.length - 1].args[2]); + lastSavedSnapshot = saveSnapshotLogs[saveSnapshotLogs.length - 1].args[1]; } catch { const veaInboxAddress = await veaInbox.getAddress(); - const lastSavedMessageId = await fetchLastSavedMessage(veaInboxAddress, chainId); + const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage( + veaInboxAddress, + chainId + ); const messageIndex = extractMessageIndex(lastSavedMessageId); + lastSavedSnapshot = lastSavedStateRoot; // adding 1 to the message index to get the last saved count - lastSavedCount = messageIndex + 1; + lastSavedCount = messageIndex; } + const epochNow = Math.floor(Date.now() / (1000 * epochPeriod)); + const currentSnapshot = await veaInbox.snapshots(epochNow); + const currentStateRoot = await veaOutbox.stateRoot(); if (currentCount > lastSavedCount) { return { snapshotNeeded: true, latestCount: currentCount }; + } else if (currentSnapshot == ZeroHash && lastSavedSnapshot != currentStateRoot) { + return { snapshotNeeded: true, latestCount: currentCount }; } return { snapshotNeeded: false, latestCount: currentCount }; }; diff --git a/validator-cli/src/helpers/validator.ts b/validator-cli/src/helpers/validator.ts index 1c96b3da..8b0b9c14 100644 --- a/validator-cli/src/helpers/validator.ts +++ b/validator-cli/src/helpers/validator.ts @@ -74,6 +74,15 @@ export async function challengeAndResolveClaim({ } else { transactionHandler.claim = claim; } + // If claim is already resolved, nothing to do + if (claim.honest !== 0) { + emitter.emit(BotEvents.CLAIM_ALREADY_RESOLVED, epoch); + if (claim.honest === 2) { + await transactionHandler.withdrawChallengeDeposit(); + return transactionHandler; + } + return null; + } const { challenged, toRelay } = await challengeAndCheckRelay({ veaInbox, @@ -94,6 +103,7 @@ export async function challengeAndResolveClaim({ claim, veaInbox, veaInboxProvider, + veaOutbox, queryRpc, ethBlockTag, transactionHandler, @@ -141,6 +151,7 @@ interface ResolveFlowParams { claim: ClaimStruct; veaInbox: any; veaInboxProvider: JsonRpcProvider; + veaOutbox: any; queryRpc: JsonRpcProvider; ethBlockTag: "latest" | "finalized"; transactionHandler: ITransactionHandler; @@ -154,6 +165,7 @@ async function handleResolveFlow({ claim, veaInbox, veaInboxProvider, + veaOutbox, queryRpc, ethBlockTag, transactionHandler, @@ -165,6 +177,7 @@ async function handleResolveFlow({ chainId, veaInbox, veaInboxProvider, + veaOutbox, veaOutboxProvider: queryRpc, epoch, fromBlock: blockNumberOutboxLowerBound, @@ -175,7 +188,6 @@ async function handleResolveFlow({ await transactionHandler.sendSnapshot(); return; } - const execStatus = claimResolveState.execution.status; if (execStatus === 1) { await transactionHandler.resolveChallengedClaim(claimResolveState.sendSnapshot.txHash); diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index 850c4cf9..5f82cdcb 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -32,6 +32,7 @@ export enum BotEvents { WITHDRAWING_CHALLENGE_DEPOSIT = "withdrawing_challenge_deposit", WITHDRAWING_CLAIM_DEPOSIT = "withdrawing_claim_deposit", WAITING_ARB_TIMEOUT = "waiting_arb_timeout", + CLAIM_ALREADY_RESOLVED = "claim_already_resolved", // Devnet state ADV_DEVNET = "advance_devnet", diff --git a/validator-cli/src/utils/claim.test.ts b/validator-cli/src/utils/claim.test.ts index 0f490590..34bb075d 100644 --- a/validator-cli/src/utils/claim.test.ts +++ b/validator-cli/src/utils/claim.test.ts @@ -1,4 +1,4 @@ -import { ethers } from "ethers"; +import { ethers, getAddress } from "ethers"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; import { getClaim, hashClaim, getClaimResolveState, ClaimResolveStateParams } from "./claim"; import { ClaimNotFoundError } from "./errors"; @@ -232,8 +232,8 @@ describe("snapshotClaim", () => { describe("getClaimResolveState", () => { let veaInbox: any; - let veaInboxProvider: any; - let veaOutboxProvider: any; + let veaOutbox: any; + let fetchSentSnapshotData: any; const epoch = 1; const blockNumberOutboxLowerBound = 1234; const toBlock = "latest"; @@ -253,10 +253,17 @@ describe("snapshotClaim", () => { filters: { SnapshotSent: jest.fn(), }, + getAddress: jest.fn(), }; + veaOutbox = { + claimHashes: jest.fn().mockResolvedValueOnce(hashedMockClaim), + getAddress: jest.fn(), + }; + fetchSentSnapshotData = jest.fn().mockResolvedValueOnce(hashedMockClaim); mockClaimResolveStateParams = { chainId: 11155111, veaInbox, + veaOutbox, veaInboxProvider: { getBlock: jest.fn().mockResolvedValueOnce({ timestamp: mockClaim.timestampClaimed, number: 1234 }), } as any, @@ -267,6 +274,7 @@ describe("snapshotClaim", () => { fromBlock: blockNumberOutboxLowerBound, toBlock, fetchMessageStatus: jest.fn(), + fetchSentSnapshotData, }; }); @@ -287,5 +295,15 @@ describe("snapshotClaim", () => { expect(claimResolveState.sendSnapshot.status).toBeTruthy(); expect(claimResolveState.execution.status).toBe(0); }); + + it("should return false state if incorrect snapshot sent", async () => { + veaInbox.queryFilter.mockResolvedValueOnce([{ transactionHash: "0x1234" }]); + fetchSentSnapshotData = jest.fn().mockResolvedValueOnce("0xincorrecthash"); + mockClaimResolveStateParams.fetchSentSnapshotData = fetchSentSnapshotData; + const claimResolveState = await getClaimResolveState(mockClaimResolveStateParams); + expect(claimResolveState).toBeDefined(); + expect(claimResolveState.sendSnapshot.status).toBeFalsy(); + expect(claimResolveState.execution.status).toBe(0); + }); }); }); diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 9263e121..b75dc132 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -1,6 +1,7 @@ import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; +import { VeaInboxArbToEth__factory } from "@kleros/vea-contracts/typechain-types"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { ethers } from "ethers"; +import { ethers, Interface } from "ethers"; import { ClaimNotFoundError } from "./errors"; import { getMessageStatus } from "./arbMsgExecutor"; import { @@ -56,7 +57,6 @@ const getClaim = async ({ }; const claimHash = await veaOutbox.claimHashes(epoch); if (claimHash === ethers.ZeroHash) return null; - try { const [claimLogs, challengeLogs, verificationLogs] = await Promise.all([ veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, epoch, null), fromBlock, toBlock), @@ -118,17 +118,20 @@ export interface ClaimResolveStateParams { chainId: number; veaInbox: any; veaInboxProvider: JsonRpcProvider; + veaOutbox: any; veaOutboxProvider: JsonRpcProvider; epoch: number; fromBlock: number; toBlock: number | string; fetchMessageStatus?: typeof getMessageStatus; + fetchSentSnapshotData?: typeof getSentSnapshotData; } /** - * Fetches the claim resolve state. + * Fetches the claim resolve state. Verifies claimHash from sent snapshot logs with Outbox claimHash. To call if claim is not yet resolved else an extra snapshot will be sent. * @param veaInbox VeaInbox contract instance * @param veaInboxProvider VeaInbox provider + * @param veaOutbox VeaOutbox contract instance * @param veaOutboxProvider VeaOutbox provider * @param epoch epoch number of the claim to be fetched * @param fromBlock from block number @@ -140,11 +143,13 @@ const getClaimResolveState = async ({ chainId, veaInbox, veaInboxProvider, + veaOutbox, veaOutboxProvider, epoch, fromBlock, toBlock, - fetchMessageStatus, + fetchMessageStatus = getMessageStatus, + fetchSentSnapshotData = getSentSnapshotData, }: ClaimResolveStateParams): Promise => { let claimResolveState: ClaimResolveState = { sendSnapshot: { @@ -156,20 +161,44 @@ const getClaimResolveState = async ({ txHash: "", }, }; - try { const sentSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSent(epoch, null), fromBlock, toBlock); if (sentSnapshotLogs.length > 0) { - claimResolveState.sendSnapshot.status = true; - claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; + sentSnapshotLogs.sort((a, b) => + a.blockNumber !== b.blockNumber ? b.blockNumber - a.blockNumber : b.logIndex - a.logIndex + ); + // Add logic to check if the sent message has the actual claimHash or not + const expectedClaimHash = await fetchSentSnapshotData( + sentSnapshotLogs[0].transactionHash, + veaInboxProvider, + veaInbox.interface + ); + const claimHash = await veaOutbox.claimHashes(epoch); + + if (claimHash === expectedClaimHash) { + claimResolveState.sendSnapshot.status = true; + claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; + } else { + return claimResolveState; + } } else { return claimResolveState; } } catch { const sentSnapshotFromGraph = await getSnapshotSentForEpoch(epoch, await veaInbox.getAddress(), chainId); if (sentSnapshotFromGraph) { - claimResolveState.sendSnapshot.status = true; - claimResolveState.sendSnapshot.txHash = sentSnapshotFromGraph.txHash; + const expectedClaimHash = await fetchSentSnapshotData( + sentSnapshotFromGraph.txHash, + veaInboxProvider, + veaInbox.interface + ); + const claimHash = await veaOutbox.claimHashes(epoch); + if (claimHash === expectedClaimHash) { + claimResolveState.sendSnapshot.status = true; + claimResolveState.sendSnapshot.txHash = sentSnapshotFromGraph.txHash; + } else { + return claimResolveState; + } } else { return claimResolveState; } @@ -204,4 +233,16 @@ const hashClaim = (claim: ClaimStruct) => { ); }; +const getSentSnapshotData = async (txHash: string, provider: JsonRpcProvider, inboxInterface: any): Promise => { + const tx = await provider.getTransaction(txHash); + if (!tx) return null; + + // Parse the transaction calldata to identify function + args + const parsed = inboxInterface.parseTransaction({ data: tx.data }); + const args = parsed.args; + const claimTuple = args[1] as ClaimStruct; + const expectedClaimHash = hashClaim(claimTuple); + return expectedClaimHash; +}; + export { getClaim, hashClaim, getClaimResolveState, ClaimHonestState }; diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts index eebe4473..553fa0b4 100644 --- a/validator-cli/src/utils/graphQueries.ts +++ b/validator-cli/src/utils/graphQueries.ts @@ -162,7 +162,7 @@ const getSnapshotSentForEpoch = async ( `${subgraph}`, `{ snapshots(where: {epoch: ${epoch}, inbox_: { id: "${veaInbox}" }}) { - fallback{ + fallback(orderBy: timestamp, orderDirection: desc){ txHash } } @@ -177,6 +177,7 @@ const getSnapshotSentForEpoch = async ( type SnapshotSavedResponse = { snapshots: { + stateRoot: string; messages: { id: string; }[]; @@ -188,12 +189,13 @@ type SnapshotSavedResponse = { * @param veaInbox * @returns message id */ -const getLastMessageSaved = async (veaInbox: string, chainId: number): Promise => { +const getLastMessageSaved = async (veaInbox: string, chainId: number): Promise<{ id: string; stateRoot: string }> => { const subgraph = getInboxSubgraphUrl(chainId); const result: SnapshotSavedResponse = await request( `${subgraph}`, `{ snapshots(first:2, orderBy:timestamp,orderDirection:desc, where:{inbox:"${veaInbox}"}) { + stateRoot messages(first: 1,orderBy:timestamp,orderDirection:desc){ id } @@ -201,7 +203,7 @@ const getLastMessageSaved = async (veaInbox: string, chainId: number): Promise { emitter.on(BotEvents.CHALLENGER_WON_CLAIM, () => { console.log("Challenger won claim"); }); + emitter.on(BotEvents.CLAIM_ALREADY_RESOLVED, (epoch: number) => { + console.log(`Claim for epoch ${epoch} is already resolved.`); + }); }; diff --git a/validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts b/validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts index 7a74bdcd..09b25405 100644 --- a/validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts +++ b/validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts @@ -25,10 +25,9 @@ export class ArbToEthTransactionHandler extends BaseTransactionHandler { this.emitter.emit(BotEvents.CLAIMING, this.epoch); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.claimTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) { - return; - } + + const toSubmit = await this.toSubmitTransaction(this.transactions.claimTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; const { routeConfig } = getBridgeConfig(this.chainId); const { deposit } = routeConfig[this.network]; @@ -51,10 +50,8 @@ export class ArbToEthTransactionHandler extends BaseTransactionHandler { this.emitter.emit(BotEvents.EXECUTING_SNAPSHOT, this.epoch); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.executeSnapshotTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) { - return; - } + const toSubmit = await this.toSubmitTransaction(this.transactions.executeSnapshotTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; const result = await execFn(sendSnapshotHash, this.veaInboxProvider, this.veaOutboxProvider); this.emitter.emit(BotEvents.TXN_MADE, result.hash, this.epoch, "Execute Snapshot"); @@ -142,10 +135,8 @@ export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler public async devnetAdvanceState(stateRoot: string): Promise { this.emitter.emit(BotEvents.ADV_DEVNET, this.epoch); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.devnetAdvanceStateTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) { - return; - } + const toSubmit = await this.toSubmitTransaction(this.transactions.devnetAdvanceStateTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; const { routeConfig } = getBridgeConfig(this.chainId); const { deposit } = routeConfig[Network.DEVNET]; const tx = await this.veaOutboxDevnet.devnetAdvanceState(this.epoch, stateRoot, { diff --git a/validator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts b/validator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts index fd85b913..556c2339 100644 --- a/validator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts +++ b/validator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts @@ -41,8 +41,8 @@ export class ArbToGnosisTransactionHandler extends BaseTransactionHandler { this.emitter.emit(BotEvents.CLAIMING, this.epoch); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.claimTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + const toSubmit = await this.toSubmitTransaction(this.transactions.claimTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; // Approves WETH for the claim if not already approved await this.approveWeth(); @@ -57,8 +57,8 @@ export class ArbToGnosisTransactionHandler extends BaseTransactionHandler { this.emitter.emit(BotEvents.ADV_DEVNET, this.epoch); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.devnetAdvanceStateTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + const toSubmit = await this.toSubmitTransaction(this.transactions.devnetAdvanceStateTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; await this.approveWeth(); const { routeConfig } = getBridgeConfig(this.chainId); const { deposit } = routeConfig[Network.DEVNET]; diff --git a/validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts b/validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts index a2c5af69..643d0092 100644 --- a/validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts +++ b/validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts @@ -189,13 +189,23 @@ export abstract class BaseTransactionHandler implements ITransact return TransactionStatus.NOT_FINAL; } + public async toSubmitTransaction( + trnx: Transaction | null, + contract: ContractType, + currentTime: number + ): Promise { + const status = await this.checkTransactionStatus(trnx, contract, currentTime); + if (status === TransactionStatus.PENDING || status === TransactionStatus.NOT_FINAL) return false; + return true; + } + public async startVerification(currentTimestamp: number) { this.emitter.emit(BotEvents.STARTING_VERIFICATION, this.epoch); if (!this.claim) throw new ClaimNotSetError(); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.startVerificationTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + const toSubmit = await this.toSubmitTransaction(this.transactions.startVerificationTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; const cfg = getBridgeConfig(this.chainId); const timeOver = @@ -222,8 +232,8 @@ export abstract class BaseTransactionHandler implements ITransact if (!this.claim) throw new ClaimNotSetError(); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.verifySnapshotTxn, ContractType.OUTBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + const toSubmit = await this.toSubmitTransaction(this.transactions.verifySnapshotTxn, ContractType.OUTBOX, now); + if (!toSubmit) return; const cfg = getBridgeConfig(this.chainId); const timeLeft = currentTimestamp - Number(this.claim.timestampVerification) - cfg.minChallengePeriod; @@ -246,12 +256,12 @@ export abstract class BaseTransactionHandler implements ITransact if (!this.claim) throw new ClaimNotSetError(); const now = Date.now(); - const status = await this.checkTransactionStatus( + const toSubmit = await this.toSubmitTransaction( this.transactions.withdrawClaimDepositTxn, ContractType.OUTBOX, now ); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + if (!toSubmit) return; const tx = await (this.veaOutbox as any).withdrawClaimDeposit(this.epoch, this.claim); this.emitter.emit(BotEvents.TXN_MADE, tx.hash, this.epoch, "Withdraw Claim Deposit"); @@ -266,12 +276,12 @@ export abstract class BaseTransactionHandler implements ITransact if (!this.claim) throw new ClaimNotSetError(); const now = Date.now(); - const status = await this.checkTransactionStatus( + const toSubmit = await this.toSubmitTransaction( this.transactions.withdrawChallengeDepositTxn, ContractType.OUTBOX, now ); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + if (!toSubmit) return; const tx = await (this.veaOutbox as any).withdrawChallengeDeposit(this.epoch, this.claim); this.emitter.emit(BotEvents.TXN_MADE, tx.hash, this.epoch, "Withdraw Challenge Deposit"); @@ -285,8 +295,8 @@ export abstract class BaseTransactionHandler implements ITransact this.emitter.emit(BotEvents.SAVING_SNAPSHOT, this.epoch); const now = Date.now(); - const status = await this.checkTransactionStatus(this.transactions.saveSnapshotTxn, ContractType.INBOX, now); - if (status !== TransactionStatus.NOT_MADE && status !== TransactionStatus.EXPIRED) return; + const toSubmit = await this.toSubmitTransaction(this.transactions.saveSnapshotTxn, ContractType.INBOX, now); + if (!toSubmit) return; const tx = await (this.veaInbox as any).saveSnapshot(); this.emitter.emit(BotEvents.TXN_MADE, tx.hash, this.epoch, "Save Snapshot"); diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index da98878b..d7f7df88 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -14,7 +14,7 @@ import { ChallengeAndResolveClaimParams, challengeAndResolveClaim } from "./help import { saveSnapshot, SaveSnapshotParams } from "./helpers/snapshot"; import { getTransactionHandler } from "./utils/transactionHandlers"; -const RPC_BLOCK_LIMIT = 1000; // RPC_BLOCK_LIMIT is the limit of blocks that can be queried at once +const RPC_BLOCK_LIMIT = 100; // RPC_BLOCK_LIMIT is the limit of blocks that can be queried at once /** * @file This file contains the logic for watching bridge and validating/resolving for claims. @@ -157,6 +157,7 @@ async function processEpochsForNetwork({ const { updatedTransactionHandler, latestCount } = await saveSnapshot({ chainId, veaInbox, + veaOutbox, network, epochPeriod: routeConfig[network].epochPeriod, count: toWatch[networkKey].count, @@ -178,7 +179,6 @@ async function processEpochsForNetwork({ toBlock = epochBlock + RPC_BLOCK_LIMIT; } const claim = await getClaim({ chainId, veaOutbox, veaOutboxProvider, epoch, fromBlock: epochBlock, toBlock }); - let updatedTransactions; if (path > BotPaths.CLAIMER && claim != null) { const checkAndChallengeResolveDeps: ChallengeAndResolveClaimParams = {