diff --git a/validator-cli/.env.dist b/validator-cli/.env.dist index c152544b..0fa990da 100644 --- a/validator-cli/.env.dist +++ b/validator-cli/.env.dist @@ -1,31 +1,20 @@ PRIVATE_KEY= -# Devnet RPCs -RPC_CHIADO=https://rpc.chiadochain.net -RPC_ARB_SEPOLIA=https://sepolia-rollup.arbitrum.io/rpc -RPC_SEPOLIA= +# Networks: devnet, testnet, mainnet +NETWORKS=devnet,testnet -# Testnet or Mainnet RPCs + +# 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_CHAIN_ID=421611 +GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d -# Devnet Addresses -VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410 -VEAOUTBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410 +VEAOUTBOX_CHAINS=11155111,421611 +# 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/ecosystem.config.js b/validator-cli/ecosystem.config.js index 795f27f0..9a238256 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", + 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", - }, - }, - { - name: "start-sepolia-devnet", - script: "yarn", - args: "start-sepolia-devnet", - interpreter: "/bin/bash", - 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", }, }, ], 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/ArbToEth/claimer.test.ts b/validator-cli/src/ArbToEth/claimer.test.ts new file mode 100644 index 00000000..b5763ae2 --- /dev/null +++ b/validator-cli/src/ArbToEth/claimer.test.ts @@ -0,0 +1,234 @@ +import { ethers } from "ethers"; +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; + let veaOutboxProvider: any; + let emitter: any; + let mockClaim: any; + let mockGetLatestClaimedEpoch: 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", + 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: 110 }), + }; + emitter = { + emit: jest.fn(), + }; + + 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, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + 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", () => { + 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(); + }), + devnetAdvanceState: jest.fn().mockImplementation(() => { + mockTransactionHandler.transactions.devnetAdvanceStateTxn = mockTransactions.devnetAdvanceStateTxn; + return Promise.resolve(); + }), + transactions: { + claimTxn: "0x0", + withdrawClaimDepositTxn: "0x0", + startVerificationTxn: "0x0", + 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(); + }); + it("should return null if no snapshot is saved on the inbox for a claimable epoch", async () => { + 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 () => { + 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(); + }); + describe("devnet", () => { + beforeEach(() => { + mockDeps.network = Network.DEVNET; + }); + 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); + }); + }); + 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 new file mode 100644 index 00000000..37f9f884 --- /dev/null +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -0,0 +1,146 @@ +import { EventEmitter } from "events"; +import { ethers } from "ethers"; +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"; +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; + veaInbox: any; + veaInboxProvider: JsonRpcProvider; + veaOutbox: any; + veaOutboxProvider: JsonRpcProvider; + transactionHandler: ArbToEthTransactionHandler | null; + emitter: EventEmitter; + fetchClaim?: typeof getClaim; + fetchLatestClaimedEpoch?: typeof getLastClaimedEpoch; + fetchTransactionHandler?: typeof getTransactionHandler; + now?: number; +} + +async function checkAndClaim({ + chainId, + network, + claim, + epoch, + epochPeriod, + veaInbox, + veaInboxProvider, + veaOutbox, + veaOutboxProvider, + transactionHandler, + emitter, + fetchLatestClaimedEpoch = getLastClaimedEpoch, + fetchTransactionHandler = getTransactionHandler, + now = Date.now(), +}: CheckAndClaimParams) { + let outboxStateRoot = await veaOutbox.stateRoot(); + const claimAbleEpoch = Math.floor(now / (1000 * epochPeriod)) - 1; + if (!transactionHandler) { + const TransactionHandler = fetchTransactionHandler(chainId, network); + transactionHandler = new TransactionHandler({ + network, + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim, + }); + } else { + transactionHandler.claim = claim; + } + + if (network == Network.DEVNET) { + return makeClaimDevnet( + epoch, + claim, + outboxStateRoot, + transactionHandler as ArbToEthDevnetTransactionHandler, + veaInbox, + emitter + ); + } else if (claim == null && epoch == claimAbleEpoch) { + return makeClaim(epoch, transactionHandler, outboxStateRoot, veaInbox, veaOutbox, fetchLatestClaimedEpoch); + } else if (claim != null) { + 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/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index 12b7fb6f..713b580b 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -1,26 +1,41 @@ -import { ArbToEthTransactionHandler, ContractType } from "./transactionHandler"; +import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth"; +import { + ArbToEthTransactionHandler, + ContractType, + Transaction, + MAX_PENDING_CONFIRMATIONS, + 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, Network } from "../consts/bridgeRoutes"; describe("ArbToEthTransactionHandler", () => { + const chainId = 11155111; let epoch: number = 100; let veaInbox: any; let veaOutbox: any; let veaInboxProvider: any; let veaOutboxProvider: any; let claim: ClaimStruct = null; - + let transactionHandlerParams: TransactionHandlerConstructor; + const mockEmitter = new MockEmitter(); beforeEach(() => { veaInboxProvider = { getTransactionReceipt: jest.fn(), 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(), @@ -34,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); @@ -52,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); @@ -72,77 +84,258 @@ 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 }); }); 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, 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); }); }); - describe("challengeClaim", () => { + // 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.estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["claim(uint256,bytes32)"] = mockClaim; + + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); + 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({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); + }); + + 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 { routeConfig, sequencerDelayLimit } = getBridgeConfig(chainId); + const epochPeriod = routeConfig[Network.TESTNET].epochPeriod; + let startVerificationFlipTime: number; + const mockStartVerification = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + mockStartVerification.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(transactionHandlerParams); + 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({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); + }); + + 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; + beforeEach(() => { + const mockVerifySnapshot = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + 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); + verificationFlipTime = Number(claim.timestampVerification) + 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({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); + }); + + 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; beforeEach(() => { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter + const mockWithdrawClaimDeposit = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + mockWithdrawClaimDeposit.estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["withdrawClaimDeposit(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = + mockWithdrawClaimDeposit; + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); + 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.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 = { hash: "0x1234", broadcastedTimestamp: 1000 }; + await transactionHandler.withdrawClaimDeposit(); + expect(transactionHandler.checkTransactionStatus).toHaveBeenCalledWith( + transactionHandler.transactions.withdrawClaimDepositTxn, + ContractType.OUTBOX, + expect.any(Number) ); + }); + + 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; + beforeEach(() => { + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); transactionHandler.claim = claim; }); @@ -155,13 +348,16 @@ 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.estimateGas).not.toHaveBeenCalled(); + expect( + veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] + ).not.toHaveBeenCalled(); }); it("should challenge claim", async () => { @@ -170,25 +366,24 @@ 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"); }); - describe("withdrawDeposit", () => { + 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; }); @@ -197,17 +392,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) ); }); @@ -219,22 +422,14 @@ 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); }); }); 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; }); @@ -242,17 +437,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(); }); @@ -268,36 +471,32 @@ describe("ArbToEthTransactionHandler", () => { describe("resolveChallengedClaim", () => { let mockMessageExecutor: any; let transactionHandler: 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); - 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 571b3684..4635ca7d 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -1,33 +1,59 @@ -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"; 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. * 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). * executeSnapshot() - Execute a sent snapshot to resolve dispute in VeaOutbox (ETH). */ -type Transactions = { - challengeTxn: string | null; - withdrawChallengeDepositTxn: string | null; - sendSnapshotTxn: string | null; - executeSnapshotTxn: string | null; +export interface TransactionHandlerConstructor { + network: Network; + epoch: number; + veaInbox: VeaInboxArbToEth; + veaOutbox: VeaOutboxArbToEth; + veaInboxProvider: JsonRpcProvider; + veaOutboxProvider: JsonRpcProvider; + emitter: typeof defaultEmitter; + claim: ClaimStruct | null; +} + +export type Transaction = { + hash: string; + broadcastedTimestamp: number; }; -enum TransactionStatus { +export type Transactions = { + claimTxn: Transaction | null; + withdrawClaimDepositTxn: Transaction | null; + startVerificationTxn: Transaction | null; + verifySnapshotTxn: Transaction | null; + challengeTxn: Transaction | null; + withdrawChallengeDepositTxn: Transaction | null; + sendSnapshotTxn: Transaction | null; + executeSnapshotTxn: Transaction | null; + devnetAdvanceStateTxn?: Transaction | null; +}; + +export enum TransactionStatus { NOT_MADE = 0, PENDING = 1, NOT_FINAL = 2, FINAL = 3, + EXPIRED = 4, } export enum ContractType { @@ -35,34 +61,42 @@ 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 network: Network; public veaInbox: VeaInboxArbToEth; - public veaOutbox: VeaOutboxArbToEth; + public veaOutbox: VeaOutboxArbToEth | VeaOutboxArbToEthDevnet; public veaInboxProvider: JsonRpcProvider; public veaOutboxProvider: JsonRpcProvider; public epoch: number; public emitter: typeof defaultEmitter; public transactions: Transactions = { + claimTxn: null, + withdrawClaimDepositTxn: null, + startVerificationTxn: null, + verifySnapshotTxn: null, challengeTxn: null, withdrawChallengeDepositTxn: null, sendSnapshotTxn: null, executeSnapshotTxn: null, }; - constructor( - epoch: number, - veaInbox: VeaInboxArbToEth, - veaOutbox: VeaOutboxArbToEth, - veaInboxProvider: JsonRpcProvider, - veaOutboxProvider: JsonRpcProvider, - emitter: typeof defaultEmitter = defaultEmitter, - claim: ClaimStruct | null = null - ) { + constructor({ + network, + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim, + }: TransactionHandlerConstructor) { + this.network = network; this.epoch = epoch; this.veaInbox = veaInbox; this.veaOutbox = veaOutbox; @@ -80,36 +114,179 @@ 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, MAX_PENDING_CONFIRMATIONS - confirmations); 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); + 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(CHAIN_ID); + + 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, claimTransaction.hash, this.epoch, "Claim"); + this.transactions.claimTxn = { + hash: claimTransaction.hash, + broadcastedTimestamp: currentTime, + }; + } + + /** + * Start verification for this.epoch in VeaOutbox(ETH). + */ + public async startVerification(currentTimestamp: number) { + this.emitter.emit(BotEvents.STARTING_VERIFICATION, this.epoch); + if (this.claim == null) { + throw new ClaimNotSetError(); + } + 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(CHAIN_ID); + const timeOver = + currentTimestamp - + Number(this.claim.timestampClaimed) - + bridgeConfig.sequencerDelayLimit - + bridgeConfig.routeConfig[this.network].epochPeriod; + + if (timeOver < 0) { + 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, startVerifTrx.hash, this.epoch, "Start Verification"); + this.transactions.startVerificationTxn = { + hash: startVerifTrx.hash, + broadcastedTimestamp: currentTime, + }; + } + + /** + * Verify snapshot for this.epoch in VeaOutbox(ETH). + */ + public async verifySnapshot(currentTimestamp: number) { + this.emitter.emit(BotEvents.VERIFYING_SNAPSHOT, this.epoch); + if (this.claim == null) { + throw new ClaimNotSetError(); + } + 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(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, this.epoch, -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, claimTransaction.hash, this.epoch, "Verify Snapshot"); + this.transactions.verifySnapshotTxn = { + hash: claimTransaction.hash, + broadcastedTimestamp: currentTime, + }; + } + + /** + * Withdraw the claim deposit. + * + */ + public async withdrawClaimDeposit() { + this.emitter.emit(BotEvents.WITHDRAWING_CLAIM_DEPOSIT, this.epoch); + if (this.claim == null) { + throw new ClaimNotSetError(); + } + 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[ + "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, withdrawTxn.hash, this.epoch, "Withdraw Deposit"); + this.transactions.withdrawClaimDepositTxn = { + hash: withdrawTxn.hash, + broadcastedTimestamp: currentTime, + }; + } + /** * Challenge claim for this.epoch in VeaOutbox(ETH). * @@ -119,11 +296,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 }); @@ -146,7 +328,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, + }; } /** @@ -154,20 +339,25 @@ export class ArbToEthTransactionHandler { * */ public async withdrawChallengeDeposit() { - this.emitter.emit(BotEvents.WITHDRAWING); + this.emitter.emit(BotEvents.WITHDRAWING_CHALLENGE_DEPOSIT); 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, + }; } /** @@ -178,13 +368,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, + }; } /** @@ -192,15 +390,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, + }; } } diff --git a/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts new file mode 100644 index 00000000..f352464d --- /dev/null +++ b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts @@ -0,0 +1,71 @@ +import { VeaOutboxArbToEthDevnet } from "@kleros/vea-contracts/typechain-types"; +import { + ArbToEthTransactionHandler, + ContractType, + TransactionStatus, + Transactions, + Transaction, + TransactionHandlerConstructor, +} from "./transactionHandler"; +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 = { + 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 = 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 deposit = getBridgeConfig(CHAIN_ID).deposit; + const startVerifTrx = await this.veaOutboxDevnet.devnetAdvanceState(this.epoch, stateRoot, { + value: deposit, + }); + 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/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..881ed6e0 100644 --- a/validator-cli/src/ArbToEth/validator.ts +++ b/validator-cli/src/ArbToEth/validator.ts @@ -6,11 +6,14 @@ 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 const secondsPerSlotEth = 12; export interface ChallengeAndResolveClaimParams { + claim: ClaimStruct; epoch: number; epochPeriod: number; veaInbox: any; @@ -25,6 +28,7 @@ export interface ChallengeAndResolveClaimParams { } export async function challengeAndResolveClaim({ + claim, epoch, epochPeriod, veaInbox, @@ -33,10 +37,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,21 +60,17 @@ 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( + 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 b61e4696..a8b4153e 100644 --- a/validator-cli/src/consts/bridgeRoutes.ts +++ b/validator-cli/src/consts/bridgeRoutes.ts @@ -1,47 +1,91 @@ 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/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 { chain: string; - epochPeriod: number; deposit: bigint; minChallengePeriod: number; sequencerDelayLimit: number; inboxRPC: string; outboxRPC: string; - inboxAddress: string; - outboxAddress: string; - routerAddress?: string; - routerProvider?: string; + routerRPC?: string; + routeConfig: { [key in Network]: RouteConfigs }; } +type RouteConfigs = { + veaInbox: any; + veaOutbox: any; + veaRouter?: any; + epochPeriod: number; +}; + +export enum Network { + DEVNET = "devnet", + TESTNET = "testnet", +} + +const arbToEthConfigs: { [key in Network]: RouteConfigs } = { + [Network.DEVNET]: { + veaInbox: veaInboxArbToEthDevnet, + veaOutbox: veaOutboxArbToEthDevnet, + epochPeriod: 1800, + }, + [Network.TESTNET]: { + veaInbox: veaInboxArbToEthTestnet, + veaOutbox: veaOutboxArbToEthTestnet, + epochPeriod: 7200, + }, +}; + +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, - inboxAddress: process.env.VEAINBOX_ARB_TO_ETH_ADDRESS, - outboxAddress: process.env.VEAOUTBOX_ARB_TO_ETH_ADDRESS, + routeConfig: arbToEthConfigs, }, 10200: { chain: "chiado", - epochPeriod: 3600, deposit: BigInt("1000000000000000000"), minChallengePeriod: 10800, 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, + routeConfig: arbToGnosisConfigs, }, }; -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/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/botConfig.test.ts b/validator-cli/src/utils/botConfig.test.ts new file mode 100644 index 00000000..175b357d --- /dev/null +++ b/validator-cli/src/utils/botConfig.test.ts @@ -0,0 +1,25 @@ +import { getBotPath, BotPaths } from "./botConfig"; +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/botConfig.ts b/validator-cli/src/utils/botConfig.ts new file mode 100644 index 00000000..df90650e --- /dev/null +++ b/validator-cli/src/utils/botConfig.ts @@ -0,0 +1,72 @@ +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; +} + +export interface NetworkConfig { + chainId: number; + networks: Network[]; +} + +/** + * 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 rawNetwork = process.env.NETWORKS ? process.env.NETWORKS.split(",") : []; + const networks = validateNetworks(rawNetwork); + + const networkConfig: NetworkConfig[] = []; + for (const chainId of chainIds) { + networkConfig.push({ + chainId: Number(chainId), + networks, + }); + } + 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 fef5ed5b..ffb67522 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -1,29 +1,42 @@ export enum BotEvents { // Bridger state STARTED = "started", + WATCHING = "watching", CHECKING = "checking", WAITING = "waiting", NO_CLAIM = "no_claim", VALID_CLAIM = "valid_claim", + NO_CLAIM_REQUIRED = "no_claim_required", // 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", + CLAIM_CHALLENGED = "claim_challenged", 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", + // Devnet state + ADV_DEVNET = "advance_devnet", + // Transaction state TXN_MADE = "txn_made", TXN_PENDING = "txn_pending", 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/claim.test.ts b/validator-cli/src/utils/claim.test.ts index ed319105..660e5553 100644 --- a/validator-cli/src/utils/claim.test.ts +++ b/validator-cli/src/utils/claim.test.ts @@ -14,6 +14,9 @@ describe("snapshotClaim", () => { let veaOutbox: any; let veaOutboxProvider: any; const epoch = 1; + let getVerificationForClaim = jest.fn(); + let getChallengerForClaim = jest.fn(); + let mockClaimParams: any; beforeEach(() => { mockClaim = { stateRoot: "0xeac817ed5c5b3d1c2c548f231b7cf9a0dfd174059f450ec6f0805acf6a16a551", @@ -32,14 +35,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 +66,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 +95,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 +124,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 +172,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 +196,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)); }); }); diff --git a/validator-cli/src/utils/claim.ts b/validator-cli/src/utils/claim.ts index 6b7e2262..a13cbe96 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,26 +55,38 @@ 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 { + 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), + ]); + claim.stateRoot = claimFromGraph.stateroot; + claim.claimer = claimFromGraph.bridger; + claim.timestampClaimed = claimFromGraph.timestamp; + if (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) { return claim; } @@ -80,6 +112,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, @@ -89,19 +132,36 @@ 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: "" }, + let 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; + 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; + } else { + return claimResolveState; + } + } catch { + const sentSnapshotFromGraph = await getSnapshotSentForEpoch(epoch, await veaInbox.getAddress()); + 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; @@ -130,4 +190,4 @@ const hashClaim = (claim: ClaimStruct) => { ); }; -export { getClaim, hashClaim, getClaimResolveState }; +export { getClaim, hashClaim, getClaimResolveState, ClaimHonestState }; 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 }; diff --git a/validator-cli/src/utils/epochHandler.test.ts b/validator-cli/src/utils/epochHandler.test.ts index 23cb1ed9..88432caa 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", () => { @@ -11,26 +11,30 @@ 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, sequencerDelayLimit: mockedSeqDelayLimit, })); - const result = setEpochRange(currentEpoch * mockedEpochPeriod, 1, now, mockedFetchBridgeConfig as any); - expect(result[result.length - 1]).toEqual(currentEpoch - 1); + + const result = setEpochRange({ + chainId: 1, + currentTimestamp, + epochPeriod: mockedEpochPeriod, + now, + fetchBridgeConfig: mockedFetchBridgeConfig as any, + }); + expect(result[result.length - 1]).toEqual(currentEpoch); expect(result[0]).toEqual(startEpoch); }); }); 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); }); }); diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index 0d57128a..97c22500 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -1,5 +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. * @@ -11,13 +20,14 @@ import { getBridgeConfig } from "../consts/bridgeRoutes"; * @returns The epoch range to check for claims */ -const setEpochRange = ( - currentTimestamp: number, - chainId: number, - now: number = Date.now(), - fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig -): Array => { - const { sequencerDelayLimit, epochPeriod } = fetchBridgeConfig(chainId); +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 // When Sequencer is malicious, even when L1 is finalized, L2 state might be unknown for up to sequencerDelayLimit + epochPeriod. @@ -30,13 +40,12 @@ 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) - .fill(veaEpochOutboxWatchLowerBound) - .map((el, i) => el + i); - + const length = veaEpochOutboxClaimableNow - veaEpochOutboxWatchLowerBound; + const veaEpochOutboxCheckClaimsRangeArray: number[] = Array.from( + { length }, + (_, i) => veaEpochOutboxWatchLowerBound + i + 1 + ); return veaEpochOutboxCheckClaimsRangeArray; }; @@ -52,13 +61,17 @@ const setEpochRange = ( * @example * currentEpoch = checkForNewEpoch(currentEpoch, 7200); */ -const getLatestChallengeableEpoch = ( - chainId: number, - now: number = Date.now(), - fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig -): number => { - const { epochPeriod } = fetchBridgeConfig(chainId); +const getLatestChallengeableEpoch = (epochPeriod: number, now: number = Date.now()): number => { 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 - 1000); + 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, EpochRangeParams }; diff --git a/validator-cli/src/utils/errors.ts b/validator-cli/src/utils/errors.ts index 7d4256e5..0299afe3 100644 --- a/validator-cli/src/utils/errors.ts +++ b/validator-cli/src/utils/errors.ts @@ -1,3 +1,5 @@ +import { BotPaths } from "./botConfig"; +import { Network } from "../consts/bridgeRoutes"; class ClaimNotFoundError extends Error { constructor(epoch: number) { super(); @@ -14,12 +16,52 @@ 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`; + } +} + +class InvalidBotPathError extends Error { + constructor() { + super(); + this.name = "InvalidBotPath"; + this.message = `Invalid path provided, Use one of: ${Object.keys(BotPaths).join("), ")}`; + } +} + +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(", ")}`; + } +} + +class MissingEnvError extends Error { + constructor(envVar: string) { + super(); + this.name = "MissingEnvError"; + this.message = `Missing environment variable: ${envVar}`; } } -export { ClaimNotFoundError, ClaimNotSetError, TransactionHandlerNotDefinedError }; +export { + ClaimNotFoundError, + ClaimNotSetError, + NotDefinedError, + InvalidBotPathError, + DevnetOwnerNotSetError, + InvalidNetworkError, + MissingEnvError, +}; diff --git a/validator-cli/src/utils/ethers.ts b/validator-cli/src/utils/ethers.ts index 29187eb2..ccd08c78 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, @@ -10,67 +11,113 @@ 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"; +import { ArbToEthDevnetTransactionHandler } from "../ArbToEth/transactionHandlerDevnet"; +import { NotDefinedError, InvalidNetworkError } 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)); + default: + throw new NotDefinedError("VeaInbox"); } } -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)); + default: + throw new InvalidNetworkError(`${network}(veaOutbox)`); + } + 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)); + default: + throw new InvalidNetworkError(`${network}(veaOutbox)`); + } + default: + throw new NotDefinedError("VeaOutbox"); } } -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) => { +const getClaimValidator = (chainId: number, network: Network) => { switch (chainId) { case 11155111: return challengeAndResolveClaimArbToEth; + default: + throw new NotDefinedError("Claim Validator"); } }; +const getClaimer = (chainId: number, network: Network): typeof checkAndClaim => { + switch (chainId) { + case 11155111: + switch (network) { + case Network.DEVNET: + + case Network.TESTNET: + return checkAndClaim; -const getTransactionHandler = (chainId: number) => { + default: + throw new InvalidNetworkError(`${network}(claimer)`); + } + default: + throw new NotDefinedError("Claimer"); + } +}; +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 InvalidNetworkError(`${network}(transactionHandler)`); + } default: - throw new TransactionHandlerNotDefinedError(); + throw new NotDefinedError("Transaction Handler"); } }; export { @@ -82,6 +129,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..fe048b4e --- /dev/null +++ b/validator-cli/src/utils/graphQueries.ts @@ -0,0 +1,153 @@ +import request from "graphql-request"; +import { ClaimNotFoundError } from "./errors"; + +interface ClaimData { + id: string; + bridger: string; + stateroot: string; + timestamp: number; + challenged: boolean; + txHash: string; + verification: { + timestamp: number; + }; + challenge: { + challenger: string; + }; +} + +/** + * Fetches the claim data for a given epoch (used for claimer - happy path) + * @param epoch + * @returns ClaimData + * */ +const getClaimForEpoch = async (epoch: number, outbox: string): Promise => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + + const result = await request( + `${subgraph}`, + `{ + claims(where: {epoch: ${epoch}, outbox: "${outbox}"}) { + id + bridger + stateroot + timestamp + txHash + challenged + } + }` + ); + return result[`claims`][0]; + } catch (e) { + console.log(e); + throw new ClaimNotFoundError(epoch); + } +}; + +/** + * Fetches the last claimed epoch (used for claimer - happy path) + * @returns ClaimData + */ +const getLastClaimedEpoch = async (outbox: string): Promise => { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + try { + const result = await request( + `${subgraph}`, + `{ + claims(first:1, orderBy:timestamp, orderDirection:desc, where: {outbox: "${outbox}"}) { + id + bridger + stateroot + timestamp + challenged + txHash + } + + }` + ); + 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; + } +}; + +type SenSnapshotResponse = { + snapshots: { + fallback: { txHash: string }[]; + }[]; +}; + +const getSnapshotSentForEpoch = async (epoch: number, veaInbox: any): Promise<{ txHash: string }> => { + try { + const subgraph = process.env.VEAINBOX_SUBGRAPH; + const veaInboxAddress = veaInbox.toLowerCase(); + + const result: SenSnapshotResponse = await request( + `${subgraph}`, + `{ + snapshots(where: {epoch: ${epoch}, inbox_: { id: "${veaInboxAddress}" }}) { + fallback{ + txHash + } + } + }` + ); + return result.snapshots[0].fallback[0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +export { + getClaimForEpoch, + getLastClaimedEpoch, + getVerificationForClaim, + getChallengerForClaim, + getSnapshotSentForEpoch, + ClaimData, +}; diff --git a/validator-cli/src/utils/logger.ts b/validator-cli/src/utils/logger.ts index 7b858b2e..ee791304 100644 --- a/validator-cli/src/utils/logger.ts +++ b/validator-cli/src/utils/logger.ts @@ -1,5 +1,7 @@ import { EventEmitter } from "node:events"; import { BotEvents } from "./botEvents"; +import { BotPaths } from "./botConfig"; +import { Network } from "../consts/bridgeRoutes"; /** * Listens to relevant events of an EventEmitter instance and issues log lines @@ -18,8 +20,18 @@ 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, (path: BotPaths, networks: Network[]) => { + let pathString = "claimer and challenger"; + if (path === BotPaths.CLAIMER) { + pathString = "claimer"; + } else if (path === BotPaths.CHALLENGER) { + pathString = "challenger"; + } + 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) => { @@ -30,16 +42,17 @@ export const configurableInitialize = (emitter: EventEmitter) => { console.log(`Waiting for next verifiable epoch after ${epoch}`); }); - emitter.on(BotEvents.NO_SNAPSHOT, () => { - console.log("No snapshot saved for epoch"); + emitter.on(BotEvents.NO_CLAIM_REQUIRED, (epoch: number) => { + console.log(`No claim is required for epoch ${epoch}`); }); - emitter.on(BotEvents.EPOCH_PASSED, (epoch: number) => { - console.log(`Epoch ${epoch} has passed`); + // Epoch state logs + emitter.on(BotEvents.NO_SNAPSHOT, () => { + console.log("No snapshot saved for epoch"); }); - 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 @@ -60,31 +73,52 @@ 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 - // 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}`); }); - + 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}`); }); + // 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 +130,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..7984c85c 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -1,15 +1,22 @@ 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 { 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"; import { BotEvents } from "./utils/botEvents"; import { initialize as initializeLogger } from "./utils/logger"; import { ShutdownSignal } from "./utils/shutdown"; +import { getBotPath, BotPaths, getNetworkConfig, NetworkConfig } from "./utils/botConfig"; +import { getClaim } from "./utils/claim"; +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. + * @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 @@ -21,29 +28,113 @@ export const watch = async ( emitter: typeof defaultEmitter = defaultEmitter ) => { initializeLogger(emitter); - emitter.emit(BotEvents.STARTED); - const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); - 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 TransactionHandler = getTransactionHandler(chainId); + const privKey = process.env.PRIVATE_KEY; + if (!privKey) throw new MissingEnvError("PRIVATE_KEY"); + const cliCommand = process.argv; + const path = getBotPath({ cliCommand }); + const networkConfigs = getNetworkConfig(); + emitter.emit(BotEvents.STARTED, path, networkConfigs[0].networks); + const transactionHandlers: { [epoch: number]: any } = {}; + const toWatch: { [key: string]: number[] } = {}; + while (!shutDownSignal.getIsShutdownSignal()) { + for (const networkConfig of networkConfigs) { + await processNetwork(path, networkConfig, transactionHandlers, toWatch, emitter); + } + await wait(1000 * 10); + } +}; - let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - const transactionHandlers: { [epoch: number]: InstanceType } = {}; - const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); +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 latestEpoch = getLatestChallengeableEpoch(chainId); - while (!shutDownSignal.getIsShutdownSignal()) { - let i = 0; - while (i < epochRange.length) { - const epoch = epochRange[i]; - emitter.emit(BotEvents.CHECKING, epoch); - const checkAndChallengeResolveDeps = { + const veaOutboxProvider = new JsonRpcProvider(outboxRPC); + let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); + + // 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; + } + + 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); + } + } +} + +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; + } + + 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: veaBridge.epochPeriod, + epochPeriod: routeConfig[network].epochPeriod, veaInbox, veaInboxProvider, veaOutboxProvider, @@ -51,28 +142,36 @@ export const watch = async ( transactionHandler: transactionHandlers[epoch], emitter, }; - const updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); - if (updatedTransactions) { - transactionHandlers[epoch] = updatedTransactions; - } else { - delete transactionHandlers[epoch]; - epochRange.splice(i, 1); - i--; - } - i++; + updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); } - const newEpoch = getLatestChallengeableEpoch(chainId); - if (newEpoch > latestEpoch) { - epochRange.push(newEpoch); - latestEpoch = newEpoch; - } else { - emitter.emit(BotEvents.WAITING, latestEpoch); + 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 wait(1000 * 10); + + if (updatedTransactions) { + transactionHandlers[epoch] = updatedTransactions; + } else if (epoch != latestEpoch) { + delete transactionHandlers[epoch]; + toWatch[networkKey].splice(i, 1); + } + i--; } -}; +} -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/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; diff --git a/validator-cli/tsconfig.json b/validator-cli/tsconfig.json index dbe3a5ab..7922328e 100644 --- a/validator-cli/tsconfig.json +++ b/validator-cli/tsconfig.json @@ -1,5 +1,14 @@ { "include": [ "src" - ] + ], + "compilerOptions": { + "baseUrl": ".", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "outDir": "dist" + } } diff --git a/yarn.lock b/yarn.lock index c5e1d882..25e17b80 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" @@ -1200,15 +1193,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/util@npm:^8.1.0": version: 8.1.0 resolution: "@ethereumjs/util@npm:8.1.0" @@ -3663,12 +3647,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 @@ -5354,27 +5336,6 @@ __metadata: languageName: node linkType: hard -"@pm2/agent@npm:~2.0.0": - version: 2.0.4 - resolution: "@pm2/agent@npm:2.0.4" - 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.0.0-1" - fclone: "npm:~1.0.11" - nssocket: "npm:0.6.0" - pm2-axon: "npm:~4.0.1" - pm2-axon-rpc: "npm:~0.7.0" - proxy-agent: "npm:~6.3.0" - semver: "npm:~7.5.0" - ws: "npm:~7.5.10" - checksum: 10/e7d5a48637ce78850bdb4c60dc0e2c763c0ce0eace716f16c6f50a7431b616acea5ae0901d4cf31f281a5bac4d59de92729ea52eee4a6b23178155294ae40532 - languageName: node - linkType: hard - "@pm2/agent@npm:~2.1.1": version: 2.1.1 resolution: "@pm2/agent@npm:2.1.1" @@ -5395,22 +5356,6 @@ __metadata: languageName: node linkType: hard -"@pm2/io@npm:~6.0.1": - version: 6.0.1 - resolution: "@pm2/io@npm:6.0.1" - 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/b0763b7204bb4609d3b09411c5d2b7184de0a93747f033dfbbb07e7fd158c7ffe0cfd15e5728a131b93d38c21f9e34f361e2bc2bdddf1bd48638e401e6c98587 - languageName: node - linkType: hard - "@pm2/io@npm:~6.1.0": version: 6.1.0 resolution: "@pm2/io@npm:6.1.0" @@ -6588,15 +6533,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" @@ -9233,15 +9169,6 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.2": - version: 1.2.2 - resolution: "crc-32@npm:1.2.2" - bin: - crc32: bin/crc32.njs - checksum: 10/824f696a5baaf617809aa9cd033313c8f94f12d15ebffa69f10202480396be44aef9831d900ab291638a8022ed91c360696dd5b1ba691eb3f34e60be8835b7c3 - languageName: node - linkType: hard - "create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": version: 1.2.0 resolution: "create-hash@npm:1.2.0" @@ -9309,15 +9236,6 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:^4.0.0": - version: 4.0.0 - resolution: "cross-fetch@npm:4.0.0" - dependencies: - node-fetch: "npm:^2.6.12" - checksum: 10/e231a71926644ef122d334a3a4e73d9ba3ba4b480a8a277fb9badc434c1ba905b3d60c8034e18b348361a09afbec40ba9371036801ba2b675a7b84588f9f55d8 - languageName: node - linkType: hard - "cross-spawn@npm:7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -9557,7 +9475,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:~1.11.13, dayjs@npm:~1.11.5": +"dayjs@npm:~1.11.13": version: 1.11.13 resolution: "dayjs@npm:1.11.13" checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 @@ -11018,13 +10936,6 @@ __metadata: languageName: node linkType: hard -"eventemitter2@npm:~0.4.14": - version: 0.4.14 - resolution: "eventemitter2@npm:0.4.14" - checksum: 10/5dc7b4903700f603226b69c7c33b55ce0dda85c1d57aba99ede5afa8ce97e22099102dbbcc6917d7fd754328e579e71dea1949a510ec9eaf712bb41358d914ac - languageName: node - linkType: hard - "eventemitter3@npm:5.0.1, eventemitter3@npm:^5.0.1": version: 5.0.1 resolution: "eventemitter3@npm:5.0.1" @@ -11192,7 +11103,7 @@ __metadata: languageName: node linkType: hard -"fast-json-patch@npm:^3.0.0-1, fast-json-patch@npm:^3.1.0": +"fast-json-patch@npm:^3.1.0": version: 3.1.1 resolution: "fast-json-patch@npm:3.1.1" checksum: 10/3e56304e1c95ad1862a50e5b3f557a74c65c0ff2ba5b15caab983b43e70e86ddbc5bc887e9f7064f0aacfd0f0435a29ab2f000fe463379e72b906486345e6671 @@ -11637,15 +11548,6 @@ __metadata: languageName: node linkType: hard -"function-batch@npm:^1.1.2": - version: 1.1.2 - resolution: "function-batch@npm:1.1.2" - dependencies: - lodash: "npm:^4.0.8" - checksum: 10/61515786250cf953094594c36560e288f63ad973e23879822bb3072efcc4fcb9204132e76af20208cc7bb8c1e941f656a6b9fd47a2397754966ba25a5c0aba26 - languageName: node - linkType: hard - "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" @@ -12784,7 +12686,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.3, https-proxy-agent@npm:^7.0.6": +"https-proxy-agent@npm:^7.0.1, 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: @@ -14669,13 +14571,6 @@ __metadata: languageName: node linkType: hard -"lazy@npm:~1.0.11": - version: 1.0.11 - resolution: "lazy@npm:1.0.11" - checksum: 10/12ebb4db919a7cab3d780fdc1eada2985cd07f7cef0df18b8f40f23ceb9dcb2684d924d768f8c1cf603c641df7305f5b77ca9039716e235043e4f25bdba3e886 - languageName: node - linkType: hard - "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -15197,7 +15092,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.0.8, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.0": +"lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 @@ -16292,16 +16187,6 @@ __metadata: languageName: node linkType: hard -"nssocket@npm:0.6.0": - version: 0.6.0 - resolution: "nssocket@npm:0.6.0" - dependencies: - eventemitter2: "npm:~0.4.14" - lazy: "npm:~1.0.11" - checksum: 10/9559f50385b2f586721a6053e3d9a9f0cb9fc28d14d0284725c073362c2f96b438895b3164ccae1837795acccedcc34615a96e9c3590a4db493d3221f323f81e - languageName: node - linkType: hard - "nth-check@npm:^2.0.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" @@ -17118,52 +17003,6 @@ __metadata: languageName: node linkType: hard -"pm2@npm:^5.2.2": - version: 5.4.3 - resolution: "pm2@npm:5.4.3" - dependencies: - "@pm2/agent": "npm:~2.0.0" - "@pm2/io": "npm:~6.0.1" - "@pm2/js-api": "npm:~0.8.0" - "@pm2/pm2-version-check": "npm:latest" - async: "npm:~3.2.0" - 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.5" - debug: "npm:^4.3.1" - 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.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/58b08c23285058d8dd0d44a2ba2f84fe183e7b7c323f8efb70ef293067e190c5ab4b22a4f58e7676b4df8635bbcc66f0921970ebc7962df56f7bc105ca63d835 - languageName: node - linkType: hard - "pm2@npm:^6.0.5": version: 6.0.5 resolution: "pm2@npm:6.0.5" @@ -17413,22 +17252,6 @@ __metadata: languageName: node linkType: hard -"proxy-agent@npm:~6.3.0": - version: 6.3.1 - resolution: "proxy-agent@npm:6.3.1" - dependencies: - agent-base: "npm:^7.0.2" - debug: "npm:^4.3.4" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.2" - 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/547e6ebd7359cc37608cfb7ba58c97faaa33f29fcff25c2933552917bec234cfbbd8bade0f8acccab1bd0aae489082dce5ee63f644f05f824890084a70919dea - languageName: node - linkType: hard - "proxy-agent@npm:~6.4.0": version: 6.4.0 resolution: "proxy-agent@npm:6.4.0" @@ -18589,7 +18412,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.2, semver@npm:^7.6.3": +"semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.2, semver@npm:^7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -20799,38 +20622,7 @@ __metadata: languageName: node linkType: hard -"web3-batched-send@npm:^1.0.3": - version: 1.0.3 - resolution: "web3-batched-send@npm:1.0.3" - dependencies: - function-batch: "npm:^1.1.2" - peerDependencies: - web3: ^1.0.0-beta.46 - checksum: 10/1fa368e0f4b52269a878beb265d116104b0c5cdf2a13d020665f3e49953ea488e65a21d5dbbc24f63995a26ebb8b773c8a78ce08c40ed8815410687258a50cbb - 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: @@ -20839,7 +20631,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: @@ -20852,174 +20644,7 @@ __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:^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:^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:^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:^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:^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:^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:^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:^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:^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-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 @@ -21042,7 +20667,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: @@ -21055,7 +20680,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: @@ -21068,31 +20693,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" @@ -21402,7 +21002,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: