diff --git a/validator-cli/.env.dist b/validator-cli/.env.dist index c152544b..c4fed13a 100644 --- a/validator-cli/.env.dist +++ b/validator-cli/.env.dist @@ -1,11 +1,13 @@ 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 + +# Devnet Owner +DEVNET_OWNER=0x5f4eC3Df9Cf2f0f1fDfCfCfCfCfCfCfCfCfCfCfC + +# RPCs RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc RPC_ETH= RPC_GNOSIS=https://rpc.chiadochain.net @@ -20,7 +22,8 @@ VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS=0x2f1788F7B74e01c4C85578748290467A5f063B0b VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193 GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d -VEAOUTBOX_CHAIN_ID=421611 +VEAOUTBOX_CHAINS=11155111,421611 +VEAOUTBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/outbox-arb-sep/version/latest # Devnet Addresses VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410 diff --git a/validator-cli/ecosystem.config.js b/validator-cli/ecosystem.config.js index 795f27f0..8b485a61 100644 --- a/validator-cli/ecosystem.config.js +++ b/validator-cli/ecosystem.config.js @@ -1,27 +1,16 @@ module.exports = { apps: [ { - name: "chiado-devnet", - script: "yarn", - args: "start-chiado-devnet", - interpreter: "/bin/bash", - log_date_format: "YYYY-MM-DD HH:mm Z", - watch: false, - autorestart: false, - env: { - NODE_ENV: "development", - }, - }, - { - name: "start-sepolia-devnet", - script: "yarn", - args: "start-sepolia-devnet", - interpreter: "/bin/bash", + name: "validator-cli", + script: "./src/watcher.ts", + interpreter: "../node_modules/.bin/ts-node", + interpreter_args: "--project tsconfig.json -r tsconfig-paths/register", log_date_format: "YYYY-MM-DD HH:mm Z", watch: false, autorestart: false, env: { NODE_ENV: "development", + TS_NODE_PROJECT: "./tsconfig.json", }, }, ], diff --git a/validator-cli/package.json b/validator-cli/package.json index c0334b5d..ee8481de 100644 --- a/validator-cli/package.json +++ b/validator-cli/package.json @@ -22,10 +22,8 @@ "@kleros/vea-contracts": "workspace:^", "@typechain/ethers-v6": "^0.5.1", "dotenv": "^16.4.5", - "pm2": "^5.2.2", - "typescript": "^4.9.5", - "web3": "^4.16.0", - "web3-batched-send": "^1.0.3" + "pm2": "^6.0.5", + "typescript": "^4.9.5" }, "devDependencies": { "@types/jest": "^29.5.14", diff --git a/validator-cli/src/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..bf642588 --- /dev/null +++ b/validator-cli/src/ArbToEth/claimer.ts @@ -0,0 +1,118 @@ +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) { + console.log(epoch); + let outboxStateRoot = await veaOutbox.stateRoot(); + const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); + const claimAbleEpoch = Math.floor(now / (1000 * epochPeriod)) - 1; + if (!transactionHandler) { + const TransactionHandler = fetchTransactionHandler(chainId, network); + transactionHandler = new TransactionHandler({ + network, + epoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + claim, + }); + } else { + transactionHandler.claim = claim; + } + var savedSnapshot; + var claimData; + var newMessagesToBridge: boolean; + var lastClaimChallenged: boolean; + + if (network == Network.DEVNET) { + const devnetTransactionHandler = transactionHandler as ArbToEthDevnetTransactionHandler; + if (claim == null) { + [savedSnapshot, claimData] = await Promise.all([ + veaInbox.snapshots(epoch), + fetchLatestClaimedEpoch(veaOutbox.target), + ]); + + newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + if (newMessagesToBridge && savedSnapshot != ethers.ZeroHash) { + await devnetTransactionHandler.devnetAdvanceState(savedSnapshot); + return devnetTransactionHandler; + } + } + } else if (claim == null && epoch == claimAbleEpoch) { + [savedSnapshot, claimData] = await Promise.all([ + veaInbox.snapshots(epoch), + fetchLatestClaimedEpoch(veaOutbox.target), + ]); + newMessagesToBridge = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash; + lastClaimChallenged = claimData.challenged && savedSnapshot == outboxStateRoot; + if ((newMessagesToBridge || lastClaimChallenged) && savedSnapshot != ethers.ZeroHash) { + await transactionHandler.makeClaim(savedSnapshot); + return transactionHandler; + } + emitter.emit(BotEvents.NO_CLAIM_REQUIRED, epoch); + } else if (claim != null) { + if (claim.honest == ClaimHonestState.CLAIMER) { + await transactionHandler.withdrawClaimDeposit(); + return transactionHandler; + } else if (claim.honest == ClaimHonestState.NONE) { + if (claim.challenger != ethers.ZeroAddress) { + emitter.emit(BotEvents.CLAIM_CHALLENGED, epoch); + return transactionHandler; + } + if (claim.timestampVerification == 0) { + await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); + } else { + await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp); + } + return transactionHandler; + } + } else { + emitter.emit(BotEvents.CLAIM_EPOCH_PASSED, epoch); + } + return null; +} + +export { checkAndClaim, CheckAndClaimParams }; diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index 12b7fb6f..68ab533c 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,261 @@ 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 as any).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 mockEmitter = new MockEmitter(); + const { routeConfig, sequencerDelayLimit } = getBridgeConfig(chainId); + const epochPeriod = routeConfig[Network.TESTNET].epochPeriod; + let startVerificationFlipTime: number; + const mockStartVerification = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockStartVerification as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + 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; + const mockEmitter = new MockEmitter(); + beforeEach(() => { + const mockVerifySnapshot = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockVerifySnapshot as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["verifySnapshot(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = mockVerifySnapshot; + veaOutbox.verifySnapshot.mockResolvedValue({ hash: "0x1234" }); + transactionHandler = new ArbToEthTransactionHandler(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; const mockEmitter = new MockEmitter(); beforeEach(() => { - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter + const mockWithdrawClaimDeposit = jest.fn().mockResolvedValue({ hash: "0x1234" }) as any; + (mockWithdrawClaimDeposit as any).estimateGas = jest.fn().mockResolvedValue(BigInt(100000)); + veaOutbox["withdrawClaimDeposit(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"] = + mockWithdrawClaimDeposit; + transactionHandler = new ArbToEthTransactionHandler(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 +351,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 +369,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 +395,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 +425,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 +440,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(); }); @@ -271,33 +477,30 @@ describe("ArbToEthTransactionHandler", () => { const mockEmitter = new MockEmitter(); beforeEach(() => { mockMessageExecutor = jest.fn(); - transactionHandler = new ArbToEthTransactionHandler( - epoch, - veaInbox, - veaOutbox, - veaInboxProvider, - veaOutboxProvider, - mockEmitter - ); + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); }); it("should resolve challenged claim", async () => { jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); - 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..b682888e 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/sepolia/VeaOutboxArbToEthTestnet.json"; +import veaOutboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json"; +import veaRouterArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/RouterArbToGnosisTestnet.json"; interface Bridge { chain: string; - epochPeriod: number; 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/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..cee3ede9 --- /dev/null +++ b/validator-cli/src/utils/botConfig.ts @@ -0,0 +1,77 @@ +import { InvalidBotPathError, DevnetOwnerNotSetError, InvalidNetworkError } from "./errors"; +import { Network } from "../consts/bridgeRoutes"; +require("dotenv").config(); + +export enum BotPaths { + CLAIMER = 0, // happy path + CHALLENGER = 1, // unhappy path + BOTH = 2, // both happy and unhappy path +} + +interface BotPathParams { + cliCommand: string[]; + defaultPath?: BotPaths; +} + +/** + * Get the bot path from the command line arguments + * @param defaultPath - default path to use if not specified in the command line arguments + * @returns BotPaths - the bot path (BotPaths) + */ +export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathParams): number { + const args = cliCommand.slice(2); + const pathFlag = args.find((arg) => arg.startsWith("--path=")); + + const path = pathFlag ? pathFlag.split("=")[1] : null; + + const pathMapping: Record = { + claimer: BotPaths.CLAIMER, + challenger: BotPaths.CHALLENGER, + both: BotPaths.BOTH, + }; + + if (path && !(path in pathMapping)) { + throw new InvalidBotPathError(); + } + + return path ? pathMapping[path] : defaultPath; +} + +interface NetworkConfig { + chainId: number; + networks: Network[]; + devnetOwner?: string; +} + +/** + * Get the network configuration: chainId, networks, and devnet owner + * @returns NetworkConfig[] - the network configuration + */ +export function getNetworkConfig(): NetworkConfig[] { + const chainIds = process.env.VEAOUTBOX_CHAINS ? process.env.VEAOUTBOX_CHAINS.split(",") : []; + const devnetOwner = process.env.DEVNET_OWNER; + const rawNetwork = process.env.NETWORKS ? process.env.NETWORKS.split(",") : []; + const networks = validateNetworks(rawNetwork); + if (networks.includes(Network.DEVNET) && !devnetOwner) { + throw new DevnetOwnerNotSetError(); + } + const networkConfig: NetworkConfig[] = []; + for (const chainId of chainIds) { + networkConfig.push({ + chainId: Number(chainId), + networks, + devnetOwner, + }); + } + return networkConfig; +} + +function validateNetworks(networks: string[]): Network[] { + const validNetworks = Object.values(Network); + for (const network of networks) { + if (!validNetworks.includes(network as Network)) { + throw new InvalidNetworkError(network); + } + } + return networks as unknown as Network[]; +} diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index 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.ts b/validator-cli/src/utils/claim.ts index 6b7e2262..3a76e268 100644 --- a/validator-cli/src/utils/claim.ts +++ b/validator-cli/src/utils/claim.ts @@ -80,6 +80,17 @@ type ClaimResolveState = { }; }; +/** + * Fetches the claim resolve state. + * @param veaInbox VeaInbox contract instance + * @param veaInboxProvider VeaInbox provider + * @param veaOutboxProvider VeaOutbox provider + * @param epoch epoch number of the claim to be fetched + * @param fromBlock from block number + * @param toBlock to block number + * @param fetchMessageStatus function to fetch message status + * @returns ClaimResolveState + **/ const getClaimResolveState = async ( veaInbox: any, veaInboxProvider: JsonRpcProvider, @@ -130,4 +141,4 @@ const hashClaim = (claim: ClaimStruct) => { ); }; -export { getClaim, hashClaim, getClaimResolveState }; +export { getClaim, hashClaim, getClaimResolveState, ClaimHonestState }; diff --git a/validator-cli/src/utils/epochHandler.test.ts b/validator-cli/src/utils/epochHandler.test.ts index 23cb1ed9..9d4e78af 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,36 @@ 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 mockParams: EpochRangeParams = { + chainId: 1, + currentTimestamp, + epochPeriod: mockedEpochPeriod, + now, + fetchBridgeConfig: mockedFetchBridgeConfig as any, + }; + const result = setEpochRange({ + chainId: 1, + currentTimestamp, + epochPeriod: mockedEpochPeriod, + now, + fetchBridgeConfig: mockedFetchBridgeConfig as any, + }); + expect(result[result.length - 1]).toEqual(currentEpoch); 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..9792916d 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,103 @@ 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)); } } -function getVeaOutbox(veaOutboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { +function getVeaOutbox(veaOutboxAddress: string, privateKey: string, rpcUrl: string, chainId: number, network: Network) { switch (chainId) { case 11155111: - return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + switch (network) { + case Network.DEVNET: + return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + case Network.TESTNET: + return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + } + case 10200: - return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + switch (network) { + case Network.DEVNET: + return VeaOutboxArbToGnosisDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + case Network.TESTNET: + return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); + } } } -function getVeaRouter(veaRouterAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { +function getVeaRouter(veaRouterAddress: string, privateKey: string, rpcUrl: string, chainId: number) { switch (chainId) { case 10200: - return RouterArbToGnosis__factory.connect(veaRouterAddress, getWallet(privateKey, web3ProviderURL)); + return RouterArbToGnosis__factory.connect(veaRouterAddress, getWallet(privateKey, rpcUrl)); } } -function getWETH(WETH: string, privateKey: string, web3ProviderURL: string) { - return IWETH__factory.connect(WETH, getWallet(privateKey, web3ProviderURL)); +function getWETH(WETH: string, privateKey: string, rpcUrl: string) { + return IWETH__factory.connect(WETH, getWallet(privateKey, rpcUrl)); } -function getVeaOutboxArbToEthDevnet(veaOutboxAddress: string, privateKey: string, web3ProviderURL: string) { - return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); +function getVeaOutboxArbToEthDevnet(veaOutboxAddress: string, privateKey: string, rpcUrl: string) { + return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, rpcUrl)); } -function getAMB(ambAddress: string, privateKey: string, web3ProviderURL: string) { - return IAMB__factory.connect(ambAddress, getWallet(privateKey, web3ProviderURL)); +function getAMB(ambAddress: string, privateKey: string, rpcUrl: string) { + return IAMB__factory.connect(ambAddress, getWallet(privateKey, rpcUrl)); } -const getClaimValidator = (chainId: number) => { +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: -const getTransactionHandler = (chainId: number) => { + case Network.TESTNET: + return checkAndClaim; + + 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 TransactionHandlerNotDefinedError(); + throw new NotDefinedError("Transaction Handler"); } }; export { @@ -82,6 +119,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..89be2628 --- /dev/null +++ b/validator-cli/src/utils/graphQueries.ts @@ -0,0 +1,65 @@ +import request from "graphql-request"; +import { ClaimNotFoundError } from "./errors"; + +interface ClaimData { + id: string; + bridger: string; + stateroot: string; + timestamp: number; + challenged: boolean; + txHash: string; +} + +/** + * Fetches the claim data for a given epoch (used for claimer - happy path) + * @param epoch + * @returns ClaimData + * */ +const getClaimForEpoch = async (epoch: number, 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) { + 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; + + 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]; +}; + +export { getClaimForEpoch, getLastClaimedEpoch, 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..699e639b 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -1,12 +1,17 @@ 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 } from "./utils/botConfig"; +import { getClaim } from "./utils/claim"; +import { MissingEnvError } from "./utils/errors"; +import { CheckAndClaimParams } from "./ArbToEth/claimer"; +import { ChallengeAndResolveClaimParams } from "./ArbToEth/validator"; /** * @file This file contains the logic for watching a bridge and validating/resolving for claims. @@ -21,58 +26,101 @@ 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 isWatched: { chainId: number; network: string }[] = []; + while (!shutDownSignal.getIsShutdownSignal()) { + for (const networkConfig of networkConfigs) { + const { chainId, networks } = networkConfig; + const { routeConfig, inboxRPC, outboxRPC } = getBridgeConfig(chainId); + for (const network of networks) { + emitter.emit(BotEvents.WATCHING, chainId, network); + const veaInbox = getVeaInbox(routeConfig[network].veaInbox.address, privKey, inboxRPC, chainId, network); + const veaOutbox = getVeaOutbox(routeConfig[network].veaOutbox.address, privKey, outboxRPC, chainId, network); + const veaInboxProvider = new JsonRpcProvider(inboxRPC); + const veaOutboxProvider = new JsonRpcProvider(outboxRPC); + let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); + var epochRange = setEpochRange({ + chainId, + currentTimestamp: veaOutboxLatestBlock.timestamp, + epochPeriod: routeConfig[network].epochPeriod, + }); - let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); - const transactionHandlers: { [epoch: number]: InstanceType } = {}; - const epochRange = setEpochRange(veaOutboxLatestBlock.timestamp, chainId); + // If the watcher has already started, only check the latest epoch + if ( + isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network) != null || + network == Network.DEVNET + ) { + if (network == Network.DEVNET) { + epochRange = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; + } else { + epochRange = [epochRange[epochRange.length - 1]]; + } + } + let i = epochRange.length - 1; + while (i >= 0) { + const epoch = epochRange[i]; + let latestEpoch = epochRange[epochRange.length - 1]; + const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); + const claim = await getClaim(veaOutbox, veaOutboxProvider, epoch, epochBlock, "latest"); - 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 = { - epoch, - epochPeriod: veaBridge.epochPeriod, - veaInbox, - veaInboxProvider, - veaOutboxProvider, - veaOutbox, - transactionHandler: transactionHandlers[epoch], - emitter, - }; - const updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); - if (updatedTransactions) { - transactionHandlers[epoch] = updatedTransactions; - } else { - delete transactionHandlers[epoch]; - epochRange.splice(i, 1); - i--; + const checkAndChallengeResolve = getClaimValidator(chainId, network); + const checkAndClaim = getClaimer(chainId, network); + let updatedTransactions; + if (path > BotPaths.CLAIMER && claim != null) { + const checkAndChallengeResolveDeps: ChallengeAndResolveClaimParams = { + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; + updatedTransactions = await checkAndChallengeResolve(checkAndChallengeResolveDeps); + } + if (path == BotPaths.CLAIMER || path == BotPaths.BOTH) { + const checkAndClaimParams: CheckAndClaimParams = { + network, + chainId, + claim, + epoch, + epochPeriod: routeConfig[network].epochPeriod, + veaInbox, + veaInboxProvider, + veaOutboxProvider, + veaOutbox, + transactionHandler: transactionHandlers[epoch], + emitter, + }; + updatedTransactions = await checkAndClaim(checkAndClaimParams); + } + + if (updatedTransactions) { + transactionHandlers[epoch] = updatedTransactions; + } else if (epoch != latestEpoch) { + delete transactionHandlers[epoch]; + epochRange.splice(i, 1); + } + i--; + } + if (!isWatched.find((watcher) => watcher.chainId == chainId && watcher.network == network)) { + isWatched.push({ chainId, network }); + } } - i++; - } - const newEpoch = getLatestChallengeableEpoch(chainId); - if (newEpoch > latestEpoch) { - epochRange.push(newEpoch); - latestEpoch = newEpoch; - } else { - emitter.emit(BotEvents.WAITING, latestEpoch); } await wait(1000 * 10); } }; -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 fcbb7202..5f935794 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,13 +19,6 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:^1.8.8": - version: 1.11.0 - resolution: "@adraffy/ens-normalize@npm:1.11.0" - checksum: 10/abef75f21470ea43dd6071168e092d2d13e38067e349e76186c78838ae174a46c3e18ca50921d05bea6ec3203074147c9e271f8cb6531d1c2c0e146f3199ddcb - languageName: node - linkType: hard - "@ampproject/remapping@npm:^2.2.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -1210,15 +1203,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/rlp@npm:^5.0.2": - version: 5.0.2 - resolution: "@ethereumjs/rlp@npm:5.0.2" - bin: - rlp: bin/rlp.cjs - checksum: 10/2af80d98faf7f64dfb6d739c2df7da7350ff5ad52426c3219897e843ee441215db0ffa346873200a6be6d11142edb9536e66acd62436b5005fa935baaf7eb6bd - languageName: node - linkType: hard - "@ethereumjs/tx@npm:3.5.2": version: 3.5.2 resolution: "@ethereumjs/tx@npm:3.5.2" @@ -3898,12 +3882,10 @@ __metadata: "@types/jest": "npm:^29.5.14" dotenv: "npm:^16.4.5" jest: "npm:^29.7.0" - pm2: "npm:^5.2.2" + pm2: "npm:^6.0.5" ts-jest: "npm:^29.2.5" ts-node: "npm:^10.9.2" typescript: "npm:^4.9.5" - web3: "npm:^4.16.0" - web3-batched-send: "npm:^1.0.3" languageName: unknown linkType: soft @@ -5609,6 +5591,26 @@ __metadata: languageName: node linkType: hard +"@pm2/agent@npm:~2.1.1": + version: 2.1.1 + resolution: "@pm2/agent@npm:2.1.1" + dependencies: + async: "npm:~3.2.0" + chalk: "npm:~3.0.0" + dayjs: "npm:~1.8.24" + debug: "npm:~4.3.1" + eventemitter2: "npm:~5.0.1" + fast-json-patch: "npm:^3.1.0" + fclone: "npm:~1.0.11" + pm2-axon: "npm:~4.0.1" + pm2-axon-rpc: "npm:~0.7.0" + proxy-agent: "npm:~6.4.0" + semver: "npm:~7.5.0" + ws: "npm:~7.5.10" + checksum: 10/8adc4e087bb69c609e98d1895cf4185483f14d8a6ea25cc3b62e9c13c6aa974582709fe51909ee3580976306ab14f84546b8e0156fd19c562dcf4548297671f8 + languageName: node + linkType: hard + "@pm2/io@npm:~6.0.1": version: 6.0.1 resolution: "@pm2/io@npm:6.0.1" @@ -5625,6 +5627,22 @@ __metadata: languageName: node linkType: hard +"@pm2/io@npm:~6.1.0": + version: 6.1.0 + resolution: "@pm2/io@npm:6.1.0" + dependencies: + async: "npm:~2.6.1" + debug: "npm:~4.3.1" + eventemitter2: "npm:^6.3.1" + require-in-the-middle: "npm:^5.0.0" + semver: "npm:~7.5.4" + shimmer: "npm:^1.2.0" + signal-exit: "npm:^3.0.3" + tslib: "npm:1.9.3" + checksum: 10/c95a1b4cf0b16877a503ed4eddcb94353db7f0dcfb39f59cac70cbad5c530a05a0d337602d38a7d38ffc185b142ec99f2ddf714a48ab9e1ec62f39a5760c9d74 + languageName: node + linkType: hard + "@pm2/js-api@npm:~0.8.0": version: 0.8.0 resolution: "@pm2/js-api@npm:0.8.0" @@ -6841,15 +6859,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" @@ -7799,7 +7808,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^3.2.0, async@npm:^3.2.3, async@npm:~3.2.0": +"async@npm:^3.2.0, async@npm:^3.2.3, async@npm:~3.2.0, async@npm:~3.2.6": version: 3.2.6 resolution: "async@npm:3.2.6" checksum: 10/cb6e0561a3c01c4b56a799cc8bab6ea5fef45f069ab32500b6e19508db270ef2dffa55e5aed5865c5526e9907b1f8be61b27530823b411ffafb5e1538c86c368 @@ -9716,7 +9725,7 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.0, crc-32@npm:^1.2.2": +"crc-32@npm:^1.2.0": version: 1.2.2 resolution: "crc-32@npm:1.2.2" bin: @@ -10010,7 +10019,7 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:~1.11.5": +"dayjs@npm:~1.11.13, dayjs@npm:~1.11.5": version: 1.11.13 resolution: "dayjs@npm:1.11.13" checksum: 10/7374d63ab179b8d909a95e74790def25c8986e329ae989840bacb8b1888be116d20e1c4eee75a69ea0dfbae13172efc50ef85619d304ee7ca3c01d5878b704f5 @@ -10047,7 +10056,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.4.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0": +"debug@npm:4, debug@npm:4.4.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7, debug@npm:^4.4.0": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -11893,7 +11902,7 @@ __metadata: languageName: node linkType: hard -"fast-json-patch@npm:^3.0.0-1": +"fast-json-patch@npm:^3.0.0-1, fast-json-patch@npm:^3.1.0": version: 3.1.1 resolution: "fast-json-patch@npm:3.1.1" checksum: 10/3e56304e1c95ad1862a50e5b3f557a74c65c0ff2ba5b15caab983b43e70e86ddbc5bc887e9f7064f0aacfd0f0435a29ab2f000fe463379e72b906486345e6671 @@ -13611,7 +13620,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -13672,7 +13681,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2, https-proxy-agent@npm:^7.0.6": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2, https-proxy-agent@npm:^7.0.3, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -18318,6 +18327,52 @@ __metadata: languageName: node linkType: hard +"pm2@npm:^6.0.5": + version: 6.0.5 + resolution: "pm2@npm:6.0.5" + dependencies: + "@pm2/agent": "npm:~2.1.1" + "@pm2/io": "npm:~6.1.0" + "@pm2/js-api": "npm:~0.8.0" + "@pm2/pm2-version-check": "npm:latest" + async: "npm:~3.2.6" + blessed: "npm:0.1.81" + chalk: "npm:3.0.0" + chokidar: "npm:^3.5.3" + cli-tableau: "npm:^2.0.0" + commander: "npm:2.15.1" + croner: "npm:~4.1.92" + dayjs: "npm:~1.11.13" + debug: "npm:^4.3.7" + enquirer: "npm:2.3.6" + eventemitter2: "npm:5.0.1" + fclone: "npm:1.0.11" + js-yaml: "npm:~4.1.0" + mkdirp: "npm:1.0.4" + needle: "npm:2.4.0" + pidusage: "npm:~3.0" + pm2-axon: "npm:~4.0.1" + pm2-axon-rpc: "npm:~0.7.1" + pm2-deploy: "npm:~1.0.2" + pm2-multimeter: "npm:^0.1.2" + pm2-sysmonit: "npm:^1.2.8" + promptly: "npm:^2" + semver: "npm:^7.6.2" + source-map-support: "npm:0.5.21" + sprintf-js: "npm:1.1.2" + vizion: "npm:~2.2.1" + dependenciesMeta: + pm2-sysmonit: + optional: true + bin: + pm2: bin/pm2 + pm2-dev: bin/pm2-dev + pm2-docker: bin/pm2-docker + pm2-runtime: bin/pm2-runtime + checksum: 10/1c45db52abf90c6e303c26eb1fd3688de0651625bb53232762b581b110d81dc1348bb273bfbe4603a106845837096cad09df492153615f2cfb07741d49dfb6d3 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -18554,6 +18609,22 @@ __metadata: languageName: node linkType: hard +"proxy-agent@npm:~6.4.0": + version: 6.4.0 + resolution: "proxy-agent@npm:6.4.0" + dependencies: + agent-base: "npm:^7.0.2" + debug: "npm:^4.3.4" + http-proxy-agent: "npm:^7.0.1" + https-proxy-agent: "npm:^7.0.3" + lru-cache: "npm:^7.14.1" + pac-proxy-agent: "npm:^7.0.1" + proxy-from-env: "npm:^1.1.0" + socks-proxy-agent: "npm:^8.0.2" + checksum: 10/a22f202b74cc52f093efd9bfe52de8db08eda8bbc16b9d3d73acda2acc1b40223966e5521b1706788b06adf9265f093ed554d989b354e81b2d6ad482e5bd4d23 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -22305,27 +22376,7 @@ __metadata: languageName: node linkType: hard -"web3-core@npm:^4.4.0, web3-core@npm:^4.5.0, web3-core@npm:^4.6.0, web3-core@npm:^4.7.1": - version: 4.7.1 - resolution: "web3-core@npm:4.7.1" - dependencies: - web3-errors: "npm:^1.3.1" - web3-eth-accounts: "npm:^4.3.1" - web3-eth-iban: "npm:^4.0.7" - web3-providers-http: "npm:^4.2.0" - web3-providers-ipc: "npm:^4.0.7" - web3-providers-ws: "npm:^4.0.8" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - dependenciesMeta: - web3-providers-ipc: - optional: true - checksum: 10/c6b9447e62f5c57ccc3c96492adf5630cb3256968c15ce5675c660dec1f6da0bf60397efa88588029640f749ff45a1adaa0167a402ba0b4a46e600d8eda76334 - languageName: node - linkType: hard - -"web3-errors@npm:^1.1.3, web3-errors@npm:^1.2.0, web3-errors@npm:^1.3.0, web3-errors@npm:^1.3.1": +"web3-errors@npm:^1.2.0, web3-errors@npm:^1.3.1": version: 1.3.1 resolution: "web3-errors@npm:1.3.1" dependencies: @@ -22344,7 +22395,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: @@ -22375,21 +22426,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-accounts@npm:^4.3.1": - version: 4.3.1 - resolution: "web3-eth-accounts@npm:4.3.1" - dependencies: - "@ethereumjs/rlp": "npm:^4.0.1" - crc-32: "npm:^1.2.2" - ethereum-cryptography: "npm:^2.0.0" - web3-errors: "npm:^1.3.1" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/f8b689146c908d88b983bd467c3e794ed96e284490aa3f74e665580202db4f0826d4108f0aa95dc6ef1e14f9a8a41939ff2c4485e9713744dc6474d7082d9239 - languageName: node - linkType: hard - "web3-eth-contract@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-contract@npm:1.10.4" @@ -22406,22 +22442,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-contract@npm:^4.5.0, web3-eth-contract@npm:^4.7.2": - version: 4.7.2 - resolution: "web3-eth-contract@npm:4.7.2" - dependencies: - "@ethereumjs/rlp": "npm:^5.0.2" - web3-core: "npm:^4.7.1" - web3-errors: "npm:^1.3.1" - web3-eth: "npm:^4.11.1" - web3-eth-abi: "npm:^4.4.1" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/f5dd22199a69c6f10b0c38daee790341f80247a0155bad03e7c1a9ffad2d6c47722010b4fd0e3fe7832a43eb72a2fceadfd2892712ef199898c1e43067a92c0d - languageName: node - linkType: hard - "web3-eth-ens@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-ens@npm:1.10.4" @@ -22438,23 +22458,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-ens@npm:^4.4.0": - version: 4.4.0 - resolution: "web3-eth-ens@npm:4.4.0" - dependencies: - "@adraffy/ens-normalize": "npm:^1.8.8" - web3-core: "npm:^4.5.0" - web3-errors: "npm:^1.2.0" - web3-eth: "npm:^4.8.0" - web3-eth-contract: "npm:^4.5.0" - web3-net: "npm:^4.1.0" - web3-types: "npm:^1.7.0" - web3-utils: "npm:^4.3.0" - web3-validator: "npm:^2.0.6" - checksum: 10/25a1535e095d8ffcbc0641041af69e42aa60ba2989477108a5678c42a06135df9134ccc6024c89c216cb3408848e3905ee178d5b12e3bb740e895ee6ee0bd2cf - languageName: node - linkType: hard - "web3-eth-iban@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-iban@npm:1.10.4" @@ -22465,18 +22468,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-iban@npm:^4.0.7": - version: 4.0.7 - resolution: "web3-eth-iban@npm:4.0.7" - dependencies: - web3-errors: "npm:^1.1.3" - web3-types: "npm:^1.3.0" - web3-utils: "npm:^4.0.7" - web3-validator: "npm:^2.0.3" - checksum: 10/9d7521b4d4aef3a0d697905c7859d8e4d7ce82234320beecba9b24d254592a7ccf0354f329289b4e11a816fcbe3eceb842c4c87678f5e8ec622c8351bc1b9170 - languageName: node - linkType: hard - "web3-eth-personal@npm:1.10.4": version: 1.10.4 resolution: "web3-eth-personal@npm:1.10.4" @@ -22491,20 +22482,6 @@ __metadata: languageName: node linkType: hard -"web3-eth-personal@npm:^4.1.0": - version: 4.1.0 - resolution: "web3-eth-personal@npm:4.1.0" - dependencies: - web3-core: "npm:^4.6.0" - web3-eth: "npm:^4.9.0" - web3-rpc-methods: "npm:^1.3.0" - web3-types: "npm:^1.8.0" - web3-utils: "npm:^4.3.1" - web3-validator: "npm:^2.0.6" - checksum: 10/a560b0ef1f28961101c47824aa6fc71722c4e581ef5ffc5b68cf1b7db0fd5804032239f872a167a589b3c0ebe223353b8112b38e247e1f5b5ac48991e12f853c - languageName: node - linkType: hard - "web3-eth@npm:1.10.4": version: 1.10.4 resolution: "web3-eth@npm:1.10.4" @@ -22525,25 +22502,6 @@ __metadata: languageName: node linkType: hard -"web3-eth@npm:^4.11.1, web3-eth@npm:^4.8.0, web3-eth@npm:^4.9.0": - version: 4.11.1 - resolution: "web3-eth@npm:4.11.1" - dependencies: - setimmediate: "npm:^1.0.5" - web3-core: "npm:^4.7.1" - web3-errors: "npm:^1.3.1" - web3-eth-abi: "npm:^4.4.1" - web3-eth-accounts: "npm:^4.3.1" - web3-net: "npm:^4.1.0" - web3-providers-ws: "npm:^4.0.8" - web3-rpc-methods: "npm:^1.3.0" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/b39f5f1559a012ece0017f3976207ffb5358c4ebb2e8518721efcc4975005ed8948814613795d1ceee67eb28f33608cbc89f6b231534241052de231c6477ed17 - languageName: node - linkType: hard - "web3-net@npm:1.10.4": version: 1.10.4 resolution: "web3-net@npm:1.10.4" @@ -22555,18 +22513,6 @@ __metadata: languageName: node linkType: hard -"web3-net@npm:^4.1.0": - version: 4.1.0 - resolution: "web3-net@npm:4.1.0" - dependencies: - web3-core: "npm:^4.4.0" - web3-rpc-methods: "npm:^1.3.0" - web3-types: "npm:^1.6.0" - web3-utils: "npm:^4.3.0" - checksum: 10/2899ed28d9afda9f9faee6424752cb967dabf79128bce25321318e069a41571b9bd9477b480f290fd65f07cd6c0c641def0d72f31a730705112bd14c301f4e5e - languageName: node - linkType: hard - "web3-providers-http@npm:1.10.4": version: 1.10.4 resolution: "web3-providers-http@npm:1.10.4" @@ -22579,18 +22525,6 @@ __metadata: languageName: node linkType: hard -"web3-providers-http@npm:^4.2.0": - version: 4.2.0 - resolution: "web3-providers-http@npm:4.2.0" - dependencies: - cross-fetch: "npm:^4.0.0" - web3-errors: "npm:^1.3.0" - web3-types: "npm:^1.7.0" - web3-utils: "npm:^4.3.1" - checksum: 10/812b05d1e0dd8b6c5005bdcfe3c5fbddfe6cdd082bd2654dfe171ad98c3b7ff85b0bab371c70366d2bace2cf45fbf7d2f087b4cb281dbfa12372b902b8138eeb - languageName: node - linkType: hard - "web3-providers-ipc@npm:1.10.4": version: 1.10.4 resolution: "web3-providers-ipc@npm:1.10.4" @@ -22601,17 +22535,6 @@ __metadata: languageName: node linkType: hard -"web3-providers-ipc@npm:^4.0.7": - version: 4.0.7 - resolution: "web3-providers-ipc@npm:4.0.7" - dependencies: - web3-errors: "npm:^1.1.3" - web3-types: "npm:^1.3.0" - web3-utils: "npm:^4.0.7" - checksum: 10/b953818479f5d9c7b748e10977430fd7e377696f9160ae19b1917c0317e89671c4be824c06723b6fda190258927160fcec0e8e7c1aa87a5f0344008ef7649cda - languageName: node - linkType: hard - "web3-providers-ws@npm:1.10.4": version: 1.10.4 resolution: "web3-providers-ws@npm:1.10.4" @@ -22623,45 +22546,6 @@ __metadata: languageName: node linkType: hard -"web3-providers-ws@npm:^4.0.8": - version: 4.0.8 - resolution: "web3-providers-ws@npm:4.0.8" - dependencies: - "@types/ws": "npm:8.5.3" - isomorphic-ws: "npm:^5.0.0" - web3-errors: "npm:^1.2.0" - web3-types: "npm:^1.7.0" - web3-utils: "npm:^4.3.1" - ws: "npm:^8.17.1" - checksum: 10/9b9fa96fa1fc9455fb1b632de50f542d2589710002ea2cb0cd6a5c1ed9f72960d80ce219ac66b038ea6d0a767056fe653aa258a1c084aa78d5745870cc2703b4 - languageName: node - linkType: hard - -"web3-rpc-methods@npm:^1.3.0": - version: 1.3.0 - resolution: "web3-rpc-methods@npm:1.3.0" - dependencies: - web3-core: "npm:^4.4.0" - web3-types: "npm:^1.6.0" - web3-validator: "npm:^2.0.6" - checksum: 10/8c134b1f2ae1cf94d5c452c53fe699d5951c22c62ea82084559db06722a5f0db2047be4209172ff90432c42f70cf8081fea0ea85a024e4cbcd0e037efd9acfa8 - languageName: node - linkType: hard - -"web3-rpc-providers@npm:^1.0.0-rc.4": - version: 1.0.0-rc.4 - resolution: "web3-rpc-providers@npm:1.0.0-rc.4" - dependencies: - web3-errors: "npm:^1.3.1" - web3-providers-http: "npm:^4.2.0" - web3-providers-ws: "npm:^4.0.8" - web3-types: "npm:^1.10.0" - web3-utils: "npm:^4.3.3" - web3-validator: "npm:^2.0.6" - checksum: 10/a6dff5ce76e6905eb3e8e7175984305b859a35f17ffad9511371e0840097cdccc4d8dd4a4bc893aeb78f93c22034b4c73cac79551a4d7cba204e55590018909b - languageName: node - linkType: hard - "web3-shh@npm:1.10.4": version: 1.10.4 resolution: "web3-shh@npm:1.10.4" @@ -22674,7 +22558,7 @@ __metadata: languageName: node linkType: hard -"web3-types@npm:^1.10.0, web3-types@npm:^1.3.0, web3-types@npm:^1.6.0, web3-types@npm:^1.7.0, web3-types@npm:^1.8.0": +"web3-types@npm:^1.10.0, web3-types@npm:^1.6.0": version: 1.10.0 resolution: "web3-types@npm:1.10.0" checksum: 10/849f05a001896b27082c5b5c46c62b65a28f463366eeec7223802418a61db6d3487ebfb73d1fe6dcad3f0849a76e20706098819cb4e266df4f75ca24617e62a1 @@ -22697,7 +22581,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: @@ -22710,7 +22594,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: @@ -22738,31 +22622,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" @@ -23097,7 +22956,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: