diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index 29a81d39..677ed6cf 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -35,6 +35,7 @@ describe("ArbToEthTransactionHandler", () => { challenger: "0x1234", }; }); + describe("constructor", () => { it("should create a new TransactionHandler without claim", () => { const transactionHandler = new ArbToEthTransactionHandler( @@ -84,14 +85,14 @@ describe("ArbToEthTransactionHandler", () => { veaInboxProvider.getBlock.mockResolvedValue({ number: finalityBlock }); }); - it("should return false if transaction is not final", async () => { + it("should return 2 if transaction is not final", async () => { jest.spyOn(mockEmitter, "emit"); veaInboxProvider.getTransactionReceipt.mockResolvedValue({ blockNumber: finalityBlock - (transactionHandler.requiredConfirmations - 1), }); const trnxHash = "0x123456"; const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); - expect(status).toBeTruthy(); + expect(status).toEqual(2); expect(mockEmitter.emit).toHaveBeenCalledWith( BotEvents.TXN_NOT_FINAL, trnxHash, @@ -99,23 +100,23 @@ describe("ArbToEthTransactionHandler", () => { ); }); - it("should return true if transaction is pending", async () => { + 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); - expect(status).toBeTruthy(); + expect(status).toEqual(1); expect(mockEmitter.emit).toHaveBeenCalledWith(BotEvents.TXN_PENDING, trnxHash); }); - it("should return false if transaction is final", async () => { + it("should return 3 if transaction is final", async () => { jest.spyOn(mockEmitter, "emit"); veaInboxProvider.getTransactionReceipt.mockResolvedValue({ blockNumber: finalityBlock - transactionHandler.requiredConfirmations, }); const trnxHash = "0x123456"; const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); - expect(status).toBeFalsy(); + expect(status).toEqual(3); expect(mockEmitter.emit).toHaveBeenCalledWith( BotEvents.TXN_FINAL, trnxHash, @@ -123,10 +124,10 @@ describe("ArbToEthTransactionHandler", () => { ); }); - it("should return false if transaction hash is null", async () => { + it("should return 0 if transaction hash is null", async () => { const trnxHash = null; const status = await transactionHandler.checkTransactionStatus(trnxHash, ContractType.INBOX); - expect(status).toBeFalsy(); + expect(status).toEqual(0); }); }); @@ -153,7 +154,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should not challenge claim if txn is pending", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(true); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); transactionHandler.transactions.challengeTxn = "0x1234"; await transactionHandler.challengeClaim(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( @@ -164,7 +165,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should challenge claim", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(false); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); const mockChallenge = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; (mockChallenge as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockChallenge; @@ -193,7 +194,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should withdraw deposit", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(false); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); veaOutbox.withdrawChallengeDeposit.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.withdrawChallengeDeposit(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.OUTBOX); @@ -201,7 +202,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should not withdraw deposit if txn is pending", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(true); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); transactionHandler.transactions.withdrawChallengeDepositTxn = "0x1234"; await transactionHandler.withdrawChallengeDeposit(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( @@ -238,7 +239,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should send snapshot", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(false); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); veaInbox.sendSnapshot.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.sendSnapshot(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith(null, ContractType.INBOX); @@ -246,7 +247,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should not send snapshot if txn is pending", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(true); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); transactionHandler.transactions.sendSnapshotTxn = "0x1234"; await transactionHandler.sendSnapshot(); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( @@ -280,7 +281,7 @@ describe("ArbToEthTransactionHandler", () => { ); }); it("should resolve challenged claim", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(false); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); transactionHandler.transactions.sendSnapshotTxn = "0x1234"; mockMessageExecutor.mockResolvedValue({ hash: "0x1234" }); await transactionHandler.resolveChallengedClaim( @@ -295,7 +296,7 @@ describe("ArbToEthTransactionHandler", () => { }); it("should not resolve challenged claim if txn is pending", async () => { - jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(true); + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); transactionHandler.transactions.sendSnapshotTxn = "0x1234"; await transactionHandler.resolveChallengedClaim(mockMessageExecutor); expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index cb23bf45..f4fb453a 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -73,7 +73,7 @@ export class ArbToEthTransactionHandler { * * @returns False if transaction is pending || not final || not made, else True. */ - public async checkTransactionStatus(trnxHash: string | null, contract: ContractType): Promise { + public async checkTransactionStatus(trnxHash: string | null, contract: ContractType): Promise { let provider: JsonRpcProvider; if (contract === ContractType.INBOX) { provider = this.veaInboxProvider; @@ -82,14 +82,15 @@ export class ArbToEthTransactionHandler { } if (trnxHash == null) { - return false; + return 0; } const receipt = await provider.getTransactionReceipt(trnxHash); if (!receipt) { + // TODO: Add transaction pending timeout- redo transaction. this.emitter.emit(BotEvents.TXN_PENDING, trnxHash); - return true; + return 1; } const currentBlock = await provider.getBlock("latest"); @@ -97,10 +98,10 @@ export class ArbToEthTransactionHandler { if (confirmations >= this.requiredConfirmations) { this.emitter.emit(BotEvents.TXN_FINAL, trnxHash, confirmations); - return false; + return 3; } else { this.emitter.emit(BotEvents.TXN_NOT_FINAL, trnxHash, confirmations); - return true; + return 2; } } @@ -113,7 +114,7 @@ export class ArbToEthTransactionHandler { if (!this.claim) { throw new ClaimNotSetError(); } - if (await this.checkTransactionStatus(this.transactions.challengeTxn, ContractType.OUTBOX)) { + if ((await this.checkTransactionStatus(this.transactions.challengeTxn, ContractType.OUTBOX)) > 0) { return; } const { deposit } = getBridgeConfig(this.chainId); @@ -151,7 +152,7 @@ export class ArbToEthTransactionHandler { if (!this.claim) { throw new ClaimNotSetError(); } - if (await this.checkTransactionStatus(this.transactions.withdrawChallengeDepositTxn, ContractType.OUTBOX)) { + if ((await this.checkTransactionStatus(this.transactions.withdrawChallengeDepositTxn, ContractType.OUTBOX)) > 0) { return; } const withdrawDepositTxn = await this.veaOutbox.withdrawChallengeDeposit(this.epoch, this.claim); @@ -167,7 +168,7 @@ export class ArbToEthTransactionHandler { if (!this.claim) { throw new ClaimNotSetError(); } - if (await this.checkTransactionStatus(this.transactions.sendSnapshotTxn, ContractType.INBOX)) { + if ((await this.checkTransactionStatus(this.transactions.sendSnapshotTxn, ContractType.INBOX)) > 0) { return; } const sendSnapshotTxn = await this.veaInbox.sendSnapshot(this.epoch, this.claim); @@ -180,7 +181,7 @@ export class ArbToEthTransactionHandler { */ public async resolveChallengedClaim(sendSnapshotTxn: string, executeMsg: typeof messageExecutor = messageExecutor) { this.emitter.emit(BotEvents.EXECUTING_SNAPSHOT, this.epoch); - if (await this.checkTransactionStatus(this.transactions.sendSnapshotTxn, ContractType.OUTBOX)) { + if ((await this.checkTransactionStatus(this.transactions.sendSnapshotTxn, ContractType.OUTBOX)) > 0) { return; } const msgExecuteTrnx = await executeMsg(sendSnapshotTxn, this.veaInboxProvider, this.veaOutboxProvider); diff --git a/validator-cli/src/consts/bridgeRoutes.ts b/validator-cli/src/consts/bridgeRoutes.ts index b2f6eb00..b61e4696 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -11,7 +11,7 @@ interface Bridge { inboxAddress: string; outboxAddress: string; routerAddress?: string; - roueterProvider?: string; + routerProvider?: string; } const bridges: { [chainId: number]: Bridge } = { @@ -34,7 +34,7 @@ const bridges: { [chainId: number]: Bridge } = { sequencerDelayLimit: 86400, inboxRPC: process.env.RPC_ARB, outboxRPC: process.env.RPC_GNOSIS, - roueterProvider: process.env.RPC_ETH, + 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, diff --git a/validator-cli/src/utils/arbMsgExecutor.ts b/validator-cli/src/utils/arbMsgExecutor.ts index 0e9a7e47..64e7840b 100644 --- a/validator-cli/src/utils/arbMsgExecutor.ts +++ b/validator-cli/src/utils/arbMsgExecutor.ts @@ -9,7 +9,16 @@ import { JsonRpcProvider, TransactionReceipt } from "@ethersproject/providers"; import { Signer } from "@ethersproject/abstract-signer"; import { ContractTransaction } from "@ethersproject/contracts"; -// Execute the child-to-parent (arbitrum-to-ethereum) message, for reference see: https://docs.arbitrum.io/sdk/reference/message/ChildToParentMessage +/** + * Execute the child-to-parent (arbitrum-to-ethereum) message, + * for reference see: https://docs.arbitrum.io/sdk/reference/message/ChildToParentMessage + * + * @param trnxHash Hash of the transaction + * @param childJsonRpc L2 provider + * @param parentJsonRpc L1 provider + * @returns Execution transaction for the message + * + * */ async function messageExecutor( trnxHash: string, childJsonRpc: JsonRpcProvider, @@ -37,6 +46,14 @@ async function messageExecutor( return res; } +/** + * + * @param trnxHash Hash of the transaction + * @param childJsonRpc L2 provider + * @param parentJsonRpc L1 provider + * @returns status of the message: 0 - not ready, 1 - ready + * + */ async function getMessageStatus( trnxHash: string, childJsonRpc: JsonRpcProvider, diff --git a/validator-cli/src/utils/arbToEthState.ts b/validator-cli/src/utils/arbToEthState.ts index 2e3774d5..ed4cfca5 100644 --- a/validator-cli/src/utils/arbToEthState.ts +++ b/validator-cli/src/utils/arbToEthState.ts @@ -10,12 +10,23 @@ import { SequencerInbox__factory } from "@arbitrum/sdk/dist/lib/abi/factories/Se const slotsPerEpochEth = 32; const secondsPerSlotEth = 12; +/** + * This function checks the finality of the blocks on Arbitrum and Ethereum. + * It returns the latest/finalized block on Arbitrum(found on Ethereum) and Ethereum and a flag indicating if there is a finality issue on Ethereum. + * + * @param EthProvider Ethereum provider + * @param ArbProvider Arbitrum provider + * @param veaEpoch epoch number of the claim to be fetched + * @param veaEpochPeriod epoch period of the claim to be fetched + * + * @returns [Arbitrum block, Ethereum block, finalityIssueFlag] + * */ const getBlocksAndCheckFinality = async ( EthProvider: JsonRpcProvider, ArbProvider: JsonRpcProvider, veaEpoch: number, veaEpochPeriod: number -): Promise<[Block, Block, Boolean] | undefined> => { +): Promise<[Block, Block, boolean] | undefined> => { const currentEpoch = Math.floor(Date.now() / 1000 / veaEpochPeriod); const l2Network = await getArbitrumNetwork(ArbProvider); @@ -134,6 +145,20 @@ const getBlocksAndCheckFinality = async ( return [blockArbitrum, blockFinalizedEth, finalityIssueFlagEth]; }; +/** + * + * This function finds the corresponding L1(Eth) block for a given L2(Arb) block. + * + * @param L2Provider Arbitrum provider + * @param sequencer Arbitrum sequencerInbox + * @param L2Block L2 block + * @param fromBlockEth from block number on Eth + * @param fromArbBlock from block number on Arb + * @param fallbackLatest fallback to latest L2 block if the L2 block is not found on L1 + * + * @returns [L1Block, L2BlockNumberFallback] + */ + const ArbBlockToL1Block = async ( L2Provider: JsonRpcProvider, sequencer: SequencerInbox, @@ -182,6 +207,16 @@ const ArbBlockToL1Block = async ( return [L1Block, L2BlockNumberFallback]; }; +/** + * This function finds the latest L2 batch and block number that has a corresponding batch on L1. + * + * @param nodeInterface Arbitrum NodeInterface + * @param fromArbBlock from block number on Arb + * @param latestBlockNumber latest block number on Arb + * + * @returns [latest L2 batch number, latest L2 block number] + */ + const findLatestL2BatchAndBlock = async ( nodeInterface: NodeInterface, fromArbBlock: number, diff --git a/validator-cli/src/utils/claim.test.ts b/validator-cli/src/utils/claim.test.ts index 4a22ec2e..ed319105 100644 --- a/validator-cli/src/utils/claim.test.ts +++ b/validator-cli/src/utils/claim.test.ts @@ -1,8 +1,7 @@ import { ethers } from "ethers"; import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; -import { getClaim, hashClaim } from "./claim"; +import { getClaim, hashClaim, getClaimResolveState } from "./claim"; import { ClaimNotFoundError } from "./errors"; -import { getBlock } from "web3/lib/commonjs/eth.exports"; let mockClaim: ClaimStruct; // Pre calculated from the deployed contracts @@ -185,4 +184,62 @@ describe("snapshotClaim", () => { expect(hash).not.toEqual(hashedMockClaim); }); }); + + describe("getClaimResolveState", () => { + let veaInbox: any; + let veaInboxProvider: any; + let veaOutboxProvider: any; + const epoch = 1; + const blockNumberOutboxLowerBound = 1234; + const toBlock = "latest"; + beforeEach(() => { + mockClaim = { + stateRoot: "0xeac817ed5c5b3d1c2c548f231b7cf9a0dfd174059f450ec6f0805acf6a16a551", + claimer: "0xFa00D29d378EDC57AA1006946F0fc6230a5E3288", + timestampClaimed: 1730276784, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: ethers.ZeroAddress, + }; + veaInbox = { + queryFilter: jest.fn(), + filters: { + SnapshotSent: jest.fn(), + }, + }; + }); + + it("should return pending state for both", async () => { + veaInbox.queryFilter.mockResolvedValueOnce([]); + const claimResolveState = await getClaimResolveState( + veaInbox, + veaInboxProvider, + veaOutboxProvider, + epoch, + blockNumberOutboxLowerBound, + toBlock + ); + expect(claimResolveState).toBeDefined(); + expect(claimResolveState.sendSnapshot.status).toBeFalsy(); + expect(claimResolveState.execution.status).toBe(0); + }); + + it("should return pending state for execution", async () => { + veaInbox.queryFilter.mockResolvedValueOnce([{ transactionHash: "0x1234" }]); + const mockGetMessageStatus = jest.fn().mockResolvedValueOnce(0); + const claimResolveState = await getClaimResolveState( + veaInbox, + veaInboxProvider, + veaOutboxProvider, + epoch, + blockNumberOutboxLowerBound, + toBlock, + mockGetMessageStatus + ); + expect(claimResolveState).toBeDefined(); + expect(claimResolveState.sendSnapshot.status).toBeTruthy(); + expect(claimResolveState.execution.status).toBe(0); + }); + }); }); diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 1cb8653f..b24efa04 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -80,7 +80,8 @@ const getClaimResolveState = async ( veaOutboxProvider: JsonRpcProvider, epoch: number, fromBlock: number, - toBlock: number | string + toBlock: number | string, + fetchMessageStatus: typeof getMessageStatus = getMessageStatus ): Promise => { const sentSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSent(epoch, null), fromBlock, toBlock); @@ -95,7 +96,7 @@ const getClaimResolveState = async ( claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash; } - const status = await getMessageStatus(sentSnapshotLogs[0].transactionHash, veaInboxProvider, veaOutboxProvider); + const status = await fetchMessageStatus(sentSnapshotLogs[0].transactionHash, veaInboxProvider, veaOutboxProvider); claimResolveState.execution.status = status; return claimResolveState; diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index d6842a40..3cb79b52 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -1,5 +1,16 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; +/** + * Sets the epoch range to check for claims. + * + * @param currentTimestamp - The current timestamp + * @param chainId - The chain ID + * @param now - The current time in milliseconds (optional, defaults to Date.now()) + * @param fetchBridgeConfig - The function to fetch the bridge configuration (optional, defaults to getBridgeConfig) + * + * @returns The epoch range to check for claims + */ + const setEpochRange = ( currentTimestamp: number, chainId: number, diff --git a/validator-cli/src/utils/errors.ts b/validator-cli/src/utils/errors.ts index c50753b3..c87a65a4 100644 --- a/validator-cli/src/utils/errors.ts +++ b/validator-cli/src/utils/errors.ts @@ -22,4 +22,12 @@ class ContractNotSupportedError extends Error { } } -export { ClaimNotFoundError, ClaimNotSetError, ContractNotSupportedError }; +class TransactionHandlerNotDefinedError extends Error { + constructor() { + super(); + this.name = "TransactionHandlerNotDefinedError"; + this.message = "TransactionHandler is not defined"; + } +} + +export { ClaimNotFoundError, ClaimNotSetError, ContractNotSupportedError, TransactionHandlerNotDefinedError }; diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 45ada19b..29187eb2 100644 --- a/validator-cli/src/utils/ethers.ts +++ b/validator-cli/src/utils/ethers.ts @@ -11,6 +11,7 @@ import { } from "@kleros/vea-contracts/typechain-types"; import { challengeAndResolveClaim as challengeAndResolveClaimArbToEth } from "../ArbToEth/validator"; import { ArbToEthTransactionHandler } from "../ArbToEth/transactionHandler"; +import { TransactionHandlerNotDefinedError } from "./errors"; function getWallet(privateKey: string, web3ProviderURL: string) { return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL)); @@ -68,6 +69,8 @@ const getTransactionHandler = (chainId: number) => { switch (chainId) { case 11155111: return ArbToEthTransactionHandler; + default: + throw new TransactionHandlerNotDefinedError(); } }; export { diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index ce562d71..9de4dcb0 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -6,7 +6,15 @@ import { getClaimValidator } 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 { ShutdownSignal } from "./utils/shutdown"; + +/** + * @file This file contains the logic for watching a bridge and validating/resolving for claims. + * + * @param shutDownSignal - The signal to shut down the watcher + * @param emitter - The emitter to emit events + * + */ export const watch = async ( shutDownSignal: ShutdownSignal = new ShutdownSignal(),