From d29ae84662edd5675fb7199c38d8082b8558ff2a Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Fri, 22 Nov 2024 15:51:21 +0700 Subject: [PATCH 01/15] fix(veascan): fixed subgraph event handlers --- veascan-subgraph-inbox/src/VeaInbox.ts | 14 ++++++++------ veascan-subgraph-outbox/src/VeaOutbox.ts | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/veascan-subgraph-inbox/src/VeaInbox.ts b/veascan-subgraph-inbox/src/VeaInbox.ts index bf2c62d5..620c4124 100644 --- a/veascan-subgraph-inbox/src/VeaInbox.ts +++ b/veascan-subgraph-inbox/src/VeaInbox.ts @@ -115,12 +115,14 @@ export function handleSnapshotSent(event: SnapshotSent): void { const snapshotId = BigInt.fromI32(i).toString(); snapshot = Snapshot.load(snapshotId); - if (snapshot && snapshot.epoch === epochSent) { - // Snapshot found, update resolving field and save - snapshot.resolving = true; - snapshot.save(); - fallback.snapshot = snapshotId; - break; + if (snapshot && snapshot.epoch) { + if (BigInt.compare(snapshot.epoch as BigInt, epochSent) == 0) { + // Snapshot found, update resolving field and save + snapshot.resolving = true; + snapshot.save(); + fallback.snapshot = snapshotId; + break; + } } } fallback.save(); diff --git a/veascan-subgraph-outbox/src/VeaOutbox.ts b/veascan-subgraph-outbox/src/VeaOutbox.ts index 6e7e41f6..6a7e40fe 100644 --- a/veascan-subgraph-outbox/src/VeaOutbox.ts +++ b/veascan-subgraph-outbox/src/VeaOutbox.ts @@ -64,12 +64,12 @@ export function handleVerified(event: Verified): void { for ( let i = ref.totalClaims.minus(BigInt.fromI32(1)); i.ge(BigInt.fromI32(0)); - i.minus(BigInt.fromI32(1)) + i = i.minus(BigInt.fromI32(1)) ) { const claim = Claim.load(i.toString()); - if (claim!.epoch.equals(event.params._epoch)) { - const verification = new Verification(claim!.id); - verification.claim = claim!.id; + if (claim && claim.epoch.equals(event.params._epoch)) { + const verification = new Verification(claim.id); + verification.claim = claim.id; verification.timestamp = event.block.timestamp; verification.caller = event.transaction.from; verification.txHash = event.transaction.hash; From cabab3f4db3d412e5b0e7886aaa7550156efa4fb Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Fri, 22 Nov 2024 16:33:56 +0700 Subject: [PATCH 02/15] feat: bridger-cli --- bridger-cli/.env.dist | 12 ++ bridger-cli/package.json | 22 +++ bridger-cli/src/bridger.ts | 180 +++++++++++++++++++++++++ bridger-cli/src/consts/bridgeRoutes.ts | 29 ++++ bridger-cli/src/utils/ethers.ts | 92 +++++++++++++ bridger-cli/src/utils/graphQueries.ts | 87 ++++++++++++ bridger-cli/tsconfig.json | 12 ++ package.json | 1 + yarn.lock | 12 ++ 9 files changed, 447 insertions(+) create mode 100644 bridger-cli/.env.dist create mode 100644 bridger-cli/package.json create mode 100644 bridger-cli/src/bridger.ts create mode 100644 bridger-cli/src/consts/bridgeRoutes.ts create mode 100644 bridger-cli/src/utils/ethers.ts create mode 100644 bridger-cli/src/utils/graphQueries.ts create mode 100644 bridger-cli/tsconfig.json diff --git a/bridger-cli/.env.dist b/bridger-cli/.env.dist new file mode 100644 index 00000000..4c7d4708 --- /dev/null +++ b/bridger-cli/.env.dist @@ -0,0 +1,12 @@ +PRIVATE_KEY= +VEAOUTBOX_CHAIN_ID=11155111 + +VEAINBOX_ADDRESS=0xE12daFE59Bc3A996362d54b37DFd2BA9279cAd06 +VEAINBOX_PROVIDER= + +VEAOUTBOX_ADDRESS=0x209BFdC6B7c66b63A8382196Ba3d06619d0F12c9 +VEAOUTBOX_PROVIDER= + +# Ex: 85918/outbox-arb-sep-sep-testnet-vs/version/latest +VEAINBOX_SUBGRAPH= +VEAOUTBOX_SUBGRAPH= diff --git a/bridger-cli/package.json b/bridger-cli/package.json new file mode 100644 index 00000000..54c721b8 --- /dev/null +++ b/bridger-cli/package.json @@ -0,0 +1,22 @@ +{ + "name": "@kleros/bridger-cli", + "license": "MIT", + "packageManager": "yarn@4.2.2", + "engines": { + "node": ">=18.0.0" + }, + "volta": { + "node": "18.20.3", + "yarn": "4.2.2" + }, + "scripts": { + "start-bridger": "npx ts-node ./src/bridger.ts" + }, + "dependencies": { + "@kleros/vea-contracts": "workspace:^", + "@typechain/ethers-v5": "^10.2.0", + "dotenv": "^16.4.5", + "typescript": "^4.9.5", + "web3": "^1.10.4" + } +} diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts new file mode 100644 index 00000000..4186c965 --- /dev/null +++ b/bridger-cli/src/bridger.ts @@ -0,0 +1,180 @@ +require("dotenv").config(); +import { JsonRpcProvider } from "@ethersproject/providers"; +import { ethers } from "ethers"; +import { getClaimForEpoch, ClaimData, getLastClaimedEpoch } from "utils/graphQueries"; +import { getVeaInbox, getVeaOutboxDevnet } from "utils/ethers"; +import { getBridgeConfig } from "consts/bridgeRoutes"; + +const watch = async () => { + const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); + const bridgeConfig = getBridgeConfig(chainId); + const veaInboxAddress = process.env.VEAINBOX_ADDRESS; + const veaInboxProviderURL = process.env.VEAINBOX_PROVIDER; + const veaOutboxAddress = process.env.VEAOUTBOX_ADDRESS; + const veaOutboxProviderURL = process.env.VEAOUTBOX_PROVIDER; + const veaOutboxJSON = new JsonRpcProvider(veaOutboxProviderURL); + const PRIVATE_KEY = process.env.PRIVATE_KEY; + + const veaInbox = getVeaInbox(veaInboxAddress, PRIVATE_KEY, veaInboxProviderURL, chainId); + const veaOutbox = getVeaOutboxDevnet(veaOutboxAddress, PRIVATE_KEY, veaOutboxProviderURL, chainId); + + // Check if the current epoch is claimed on veaOutbox + const currentEpoch = Math.floor(Date.now() / 1000 / bridgeConfig.epochPeriod); + const epochs: number[] = new Array(24).fill(currentEpoch - 24).map((el, i) => el + i); + let verifiableEpoch = currentEpoch - 1; + + while (true) { + let i = 0; + while (i < epochs.length) { + const activeEpoch = epochs[i]; + console.log("Checking for epoch " + activeEpoch); + let claimableEpochHash = await veaOutbox.claimHashes(activeEpoch); + let outboxStateRoot = await veaOutbox.stateRoot(); + const finalizedOutboxBlock = await veaOutboxJSON.getBlock("finalized"); + + if (claimableEpochHash == ethers.constants.HashZero && activeEpoch == verifiableEpoch) { + // Claim can be made + const savedSnapshot = await veaInbox.snapshots(activeEpoch); + if (savedSnapshot != outboxStateRoot && savedSnapshot != ethers.constants.HashZero) { + // Its possible that a claim was made for previous epoch but its not verified yet + // Making claim if there are new messages or last claim was challenged. + const claimData = await getLastClaimedEpoch(chainId); + + if (claimData.challenged || claimData.stateroot != savedSnapshot) { + // Making claim as either last claim was challenged or there are new messages + + const gasEstimate = await veaOutbox.estimateGas.claim(activeEpoch, savedSnapshot, { + value: bridgeConfig.deposit, + }); + + const claimTransaction = await veaOutbox.claim(activeEpoch, savedSnapshot, { + value: bridgeConfig.deposit, + gasLimit: gasEstimate, + }); + console.log(`Epoch ${activeEpoch} was claimed with trnx hash ${claimTransaction.hash}`); + } else { + console.log("No new messages, no need for a claim"); + epochs.splice(i, 1); + i--; + continue; + } + } else { + if (savedSnapshot == ethers.constants.HashZero) { + console.log("No snapshot saved for epoch " + activeEpoch); + } else { + console.log("No new messages after last claim"); + } + epochs.splice(i, 1); + i--; + } + } else if (claimableEpochHash != ethers.constants.HashZero) { + console.log("Claim is already made, checking for verification stage"); + const claimData: ClaimData = await getClaimForEpoch(chainId, activeEpoch); + if (claimData == undefined) { + console.log(`Claim data not found for ${activeEpoch}, skipping for now`); + continue; + } + var claim = { + stateRoot: claimData.stateroot, + claimer: claimData.bridger, + timestampClaimed: claimData.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + const claimTransaction = await veaOutboxJSON.getTransaction(claimData.txHash); + + // ToDo: Update subgraph to get verification start data + const verifiactionLogs = await veaOutboxJSON.getLogs({ + address: veaOutboxAddress, + topics: veaOutbox.filters.VerificationStarted(activeEpoch).topics, + fromBlock: claimTransaction.blockNumber, + toBlock: "latest", + }); + + if (verifiactionLogs.length > 0) { + // Verification started update the claim struct + const verificationStartBlock = await veaOutboxJSON.getBlock(verifiactionLogs[0].blockHash); + claim.timestampVerification = verificationStartBlock.timestamp; + claim.blocknumberVerification = verificationStartBlock.number; + + // Check if the verification is already resolved + if (hashClaim(claim) == claimableEpochHash) { + // Claim not resolved yet, check if we can verifySnapshot + if (finalizedOutboxBlock.timestamp - claim.timestampVerification > bridgeConfig.minChallengePeriod) { + console.log("Verification period passed, verifying snapshot"); + // Estimate gas for verifySnapshot + const verifySnapshotTxn = await veaOutbox.verifySnapshot(activeEpoch, claim); + console.log(`Verified snapshot for epoch ${activeEpoch} with trnx hash ${verifySnapshotTxn.hash}`); + } else { + console.log( + "Censorship test in progress, sec left: " + + -1 * (finalizedOutboxBlock.timestamp - claim.timestampVerification - bridgeConfig.minChallengePeriod) + ); + } + } else { + // Claim is already verified, withdraw deposit + claim.honest = 1; // Assume the claimer is honest + if (hashClaim(claim) == claimableEpochHash) { + const withdrawDepositTxn = await veaOutbox.withdrawClaimDeposit(activeEpoch, claim); + console.log(`Withdrew deposit for epoch ${activeEpoch} with trnx hash ${withdrawDepositTxn.hash}`); + } else { + console.log("Challenger won claim"); + } + epochs.splice(i, 1); + i--; + } + } else { + console.log("Verification not started yet"); + // No verification started yet, check if we can start it + if ( + finalizedOutboxBlock.timestamp - claim.timestampClaimed > + bridgeConfig.sequencerDelayLimit + bridgeConfig.epochPeriod + ) { + const startVerifTrx = await veaOutbox.startVerification(activeEpoch, claim); + console.log(`Verification started for epoch ${activeEpoch} with trx hash ${startVerifTrx.hash}`); + // Update local struct for trnx hash and block number as it takes time for the trnx to be mined. + } else { + const timeLeft = + finalizedOutboxBlock.timestamp - + claim.timestampClaimed - + bridgeConfig.sequencerDelayLimit - + bridgeConfig.epochPeriod; + console.log("Sequencer delay not passed yet, seconds left: " + -1 * timeLeft); + } + } + } else { + epochs.splice(i, 1); + i--; + console.log("Epoch has passed: " + activeEpoch); + } + i++; + } + if (Math.floor(Date.now() / 1000 / bridgeConfig.epochPeriod) - 1 > verifiableEpoch) { + verifiableEpoch = Math.floor(Date.now() / 1000 / bridgeConfig.epochPeriod) - 1; + epochs.push(verifiableEpoch); + } + console.log("Waiting for next verifiable epoch after " + verifiableEpoch); + await wait(1000 * 10); + } +}; + +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); + +const hashClaim = (claim) => { + return ethers.utils.solidityKeccak256( + ["bytes32", "address", "uint32", "uint32", "uint32", "uint8", "address"], + [ + claim.stateRoot, + claim.claimer, + claim.timestampClaimed, + claim.timestampVerification, + claim.blocknumberVerification, + claim.honest, + claim.challenger, + ] + ); +}; + +watch(); diff --git a/bridger-cli/src/consts/bridgeRoutes.ts b/bridger-cli/src/consts/bridgeRoutes.ts new file mode 100644 index 00000000..fa90c0e4 --- /dev/null +++ b/bridger-cli/src/consts/bridgeRoutes.ts @@ -0,0 +1,29 @@ +import { BigNumber } from "ethers"; + +interface IBridge { + epochPeriod: number; + deposit: BigNumber; + minChallengePeriod: number; + sequencerDelayLimit: number; +} + +const bridges: { [chainId: number]: IBridge } = { + 11155111: { + epochPeriod: 7200, + deposit: BigNumber.from("1000000000000000000"), + minChallengePeriod: 10800, + sequencerDelayLimit: 86400, + }, + 10200: { + epochPeriod: 3600, + deposit: BigNumber.from("1000000000000000000"), + minChallengePeriod: 10800, + sequencerDelayLimit: 86400, + }, +}; + +const getBridgeConfig = (chainId: number): IBridge | undefined => { + return bridges[chainId]; +}; + +export { getBridgeConfig }; diff --git a/bridger-cli/src/utils/ethers.ts b/bridger-cli/src/utils/ethers.ts new file mode 100644 index 00000000..c2e4e291 --- /dev/null +++ b/bridger-cli/src/utils/ethers.ts @@ -0,0 +1,92 @@ +import { Wallet } from "@ethersproject/wallet"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { + VeaOutboxArbToEth__factory, + VeaOutboxArbToEthDevnet__factory, + VeaOutboxArbToGnosisDevnet__factory, + VeaInboxArbToEth__factory, + VeaInboxArbToGnosis__factory, + VeaOutboxArbToGnosis__factory, +} from "@kleros/vea-contracts/typechain-types"; + +function getWallet(privateKey: string, web3ProviderURL: string) { + return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL)); +} + +function getWalletRPC(privateKey: string, rpc: JsonRpcProvider) { + return new Wallet(privateKey, rpc); +} + +// Using destination chainId as identifier, Ex: Arbitrum One (42161) -> Ethereum Mainnet (1): Use "1" as chainId +function getVeaInbox(veaInboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { + if (chainId == 11155111) { + return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + } else if (chainId == 10200) { + return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + } else { + throw new Error(`Unsupported chainId: ${chainId}`); + } +} + +function getVeaInboxProvider(veaInboxAddress: string, privateKey: string, rpc: JsonRpcProvider, chainId: number) { + if (chainId == 11155111) { + return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWalletRPC(privateKey, rpc)); + } else if (chainId == 10200) { + return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWalletRPC(privateKey, rpc)); + } +} + +function getVeaOutbox(veaInboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { + if (chainId == 11155111) { + return VeaOutboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + } else if (chainId == 10200) { + return VeaOutboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + } else { + throw new Error(`Unsupported chainId: ${chainId}`); + } +} + +function getVeaOutboxProvider(veaOutboxAddress: string, privateKey: string, rpc: JsonRpcProvider, chainId: number) { + if (chainId == 11155111) { + return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); + } else if (chainId == 10200) { + return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); + } else { + throw new Error(`Unsupported chainId: ${chainId}`); + } +} + +function getVeaOutboxDevnetProvider( + veaOutboxAddress: string, + privateKey: string, + rpc: JsonRpcProvider, + chainId: number +) { + if (chainId == 11155111) { + return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); + } else if (chainId == 10200) { + return VeaOutboxArbToGnosisDevnet__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); + } else { + throw new Error(`Unsupported chainId: ${chainId}`); + } +} + +function getVeaOutboxDevnet(veaOutboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { + if (chainId == 11155111) { + return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + } else if (chainId == 10200) { + return VeaOutboxArbToGnosisDevnet__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + } else { + throw new Error(`Unsupported chainId: ${chainId}`); + } +} + +export { + getWalletRPC, + getVeaOutboxDevnetProvider, + getVeaOutbox, + getVeaInbox, + getVeaOutboxProvider, + getVeaInboxProvider, + getVeaOutboxDevnet, +}; diff --git a/bridger-cli/src/utils/graphQueries.ts b/bridger-cli/src/utils/graphQueries.ts new file mode 100644 index 00000000..59e2284b --- /dev/null +++ b/bridger-cli/src/utils/graphQueries.ts @@ -0,0 +1,87 @@ +import request from "graphql-request"; + +interface ClaimData { + id: string; + bridger: string; + stateroot: string; + timestamp: number; + challenged: boolean; + txHash: string; +} + +const getClaimForEpoch = async (chainid: number, epoch: number): Promise => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + + const result = await request( + `https://api.studio.thegraph.com/query/${subgraph}`, + `{ + claims(where: {epoch: ${epoch}}) { + id + bridger + stateroot + timestamp + txHash + challenged + } + }` + ); + return result[`claims`][0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +const getVerificationStatus = async (chainid: number, epoch: number): Promise => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + + const result = await request( + `https://api.studio.thegraph.com/query/${subgraph}`, + `{ + verifications(where:{claim_:{epoch:${epoch}}}){ + claim{ + stateroot + epoch + } + timestamp + id + } + }` + ); + return result[`verifications`][0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +const getLastClaimedEpoch = async (chainid: number): Promise => { + try { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; + + const result = await request( + `https://api.studio.thegraph.com/query/${subgraph}`, + `{ + claims(first:1, orderBy:timestamp, orderDirection:desc){ + id + bridger + stateroot + timestamp + verification{ + timestamp + } + challenged + } + + }` + ); + return result[`claims`][0]; + } catch (e) { + console.log(e); + return undefined; + } +}; + +export { getClaimForEpoch, getVerificationStatus, getLastClaimedEpoch, ClaimData }; diff --git a/bridger-cli/tsconfig.json b/bridger-cli/tsconfig.json new file mode 100644 index 00000000..e727fea7 --- /dev/null +++ b/bridger-cli/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "esModuleInterop": true, + "resolveJsonModule": true + }, + "ts-node": { + "require": [ + "tsconfig-paths/register" + ] + } +} diff --git a/package.json b/package.json index 0d3611b5..28b54177 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "MIT", "private": true, "workspaces": [ + "bridger-cli", "contracts", "relayer-subgraph-inbox", "validator-cli", diff --git a/yarn.lock b/yarn.lock index 2d41b9a2..4af0707e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3197,6 +3197,18 @@ __metadata: languageName: node linkType: hard +"@kleros/bridger-cli@workspace:bridger-cli": + version: 0.0.0-use.local + resolution: "@kleros/bridger-cli@workspace:bridger-cli" + dependencies: + "@kleros/vea-contracts": "workspace:^" + "@typechain/ethers-v5": "npm:^10.2.0" + dotenv: "npm:^16.4.5" + typescript: "npm:^4.9.5" + web3: "npm:^1.10.4" + languageName: unknown + linkType: soft + "@kleros/ui-components-library@npm:^2.14.0": version: 2.14.0 resolution: "@kleros/ui-components-library@npm:2.14.0" From 310184aa22fd3eb1fb1f361011be906c43dc647f Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Tue, 3 Dec 2024 17:14:57 +0700 Subject: [PATCH 03/15] feat: local subgraph node & forks --- contracts/Dockerfile | 17 +++++++ contracts/docker-compose.yml | 91 ++++++++++++++++++++++++++++++++++++ contracts/initGraphDB.sql | 1 + contracts/start-forks.sh | 16 +++++++ 4 files changed, 125 insertions(+) create mode 100644 contracts/Dockerfile create mode 100644 contracts/docker-compose.yml create mode 100644 contracts/initGraphDB.sql create mode 100644 contracts/start-forks.sh diff --git a/contracts/Dockerfile b/contracts/Dockerfile new file mode 100644 index 00000000..239e7ad7 --- /dev/null +++ b/contracts/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20.12.1-alpine + +WORKDIR / + +COPY package.json ./ + +RUN yarn install + +COPY . . + +COPY start-forks.sh /start-forks.sh + +RUN chmod +x /start-forks.sh + +EXPOSE 8545 8546 + +CMD ["/bin/sh", "/start-forks.sh"] diff --git a/contracts/docker-compose.yml b/contracts/docker-compose.yml new file mode 100644 index 00000000..ce60cfe4 --- /dev/null +++ b/contracts/docker-compose.yml @@ -0,0 +1,91 @@ +version: "3.8" + +services: + postgres: + image: postgres:latest + container_name: some-postgres + environment: + POSTGRES_PASSWORD: mysecretpassword + POSTGRES_USER: postgres + POSTGRES_DB: postgres + LC_COLLATE: C + LC_CTYPE: C + ports: + - "5432:5432" + networks: + - graph-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + retries: 5 + command: + - sh + - -c + - | + docker-entrypoint.sh postgres & + sleep 5; + psql -U postgres -c "CREATE DATABASE graphnode_db ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0;"; + wait + + ipfs: + image: ipfs/kubo:latest + container_name: ipfs_host + volumes: + - ./ipfs/staging:/export + - ./ipfs/data:/data/ipfs + ports: + - "4001:4001" + - "4001:4001/udp" + - "8080:8080" + - "5001:5001" + networks: + - graph-network + + hardhat: + build: + context: ./ + dockerfile: Dockerfile + container_name: hardhat + environment: + - NETWORK1=arbitrum-sepolia + - PORT1=8545 + - NETWORK2=sepolia + - PORT2=8546 + - INFURA_ID=63722640e4ec41a59e0105dd275c2e81 + command: ["/bin/sh", "/start-forks.sh"] + ports: + - "8545:8545" + - "8546:8546" + depends_on: + - postgres + - ipfs + networks: + - graph-network + + graph-node: + image: graphprotocol/graph-node:latest + container_name: graph_node + environment: + postgres_host: some-postgres + postgres_port: 5432 + postgres_user: postgres + postgres_pass: mysecretpassword + postgres_db: graphnode_db + ipfs: ipfs_host:5001 + ethereum: mainnet:http://host.docker.internal:8546/ + ETHEREUM_REORG_THRESHOLD: 1 + ETHEREUM_ANCESTOR_COUNT: 10 + GRAPH_LOG: debug + ports: + - "8020:8020" + - "8000:8000" + depends_on: + - postgres + - ipfs + - hardhat + networks: + - graph-network + +networks: + graph-network: + driver: bridge diff --git a/contracts/initGraphDB.sql b/contracts/initGraphDB.sql new file mode 100644 index 00000000..59cbbe0f --- /dev/null +++ b/contracts/initGraphDB.sql @@ -0,0 +1 @@ +CREATE DATABASE graphnode_db ENCODING='UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE=template0; \ No newline at end of file diff --git a/contracts/start-forks.sh b/contracts/start-forks.sh new file mode 100644 index 00000000..daae7d79 --- /dev/null +++ b/contracts/start-forks.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +if [ -z "$INFURA_ID" ]; then + echo "Error: INFURA_ID is not set." + exit 1 +fi + +# Set for 8545: Arbitrum Sepolia fork +echo "Starting Hardhat fork for $NETWORK1 on port $PORT1..." +npx hardhat node --fork https://$NETWORK1.infura.io/v3/$INFURA_ID --no-deploy --hostname 0.0.0.0 --port $PORT1 & + +# Set for 8546: Sepolia fork +echo "Starting Hardhat fork for $NETWORK2 on port $PORT2..." +npx hardhat node --fork https://$NETWORK2.infura.io/v3/$INFURA_ID --no-deploy --hostname 0.0.0.0 --port $PORT2 & + +wait From 2bfba2db6887b495120d2cdc901e1b8fdee76da1 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Tue, 3 Dec 2024 18:24:25 +0700 Subject: [PATCH 04/15] fix: infura key --- contracts/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/docker-compose.yml b/contracts/docker-compose.yml index ce60cfe4..a75902bc 100644 --- a/contracts/docker-compose.yml +++ b/contracts/docker-compose.yml @@ -51,7 +51,7 @@ services: - PORT1=8545 - NETWORK2=sepolia - PORT2=8546 - - INFURA_ID=63722640e4ec41a59e0105dd275c2e81 + - INFURA_ID=${INFURA_API_KEY} command: ["/bin/sh", "/start-forks.sh"] ports: - "8545:8545" From f779e6b5d1cc0d5d10e27d658c1002e798a57698 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Tue, 3 Dec 2024 19:03:12 +0700 Subject: [PATCH 05/15] feat: bridger tests --- bridger-cli/.env.dist | 4 +- bridger-cli/package.json | 8 +- bridger-cli/src/bridger.ts | 48 +++- bridger-cli/src/utils/bridger.test.ts | 344 ++++++++++++++++++++++++++ bridger-cli/src/utils/graphQueries.ts | 6 +- 5 files changed, 392 insertions(+), 18 deletions(-) create mode 100644 bridger-cli/src/utils/bridger.test.ts diff --git a/bridger-cli/.env.dist b/bridger-cli/.env.dist index 4c7d4708..73657088 100644 --- a/bridger-cli/.env.dist +++ b/bridger-cli/.env.dist @@ -2,10 +2,10 @@ PRIVATE_KEY= VEAOUTBOX_CHAIN_ID=11155111 VEAINBOX_ADDRESS=0xE12daFE59Bc3A996362d54b37DFd2BA9279cAd06 -VEAINBOX_PROVIDER= +VEAINBOX_PROVIDER=http://localhost:8545 VEAOUTBOX_ADDRESS=0x209BFdC6B7c66b63A8382196Ba3d06619d0F12c9 -VEAOUTBOX_PROVIDER= +VEAOUTBOX_PROVIDER=http://localhost:8546 # Ex: 85918/outbox-arb-sep-sep-testnet-vs/version/latest VEAINBOX_SUBGRAPH= diff --git a/bridger-cli/package.json b/bridger-cli/package.json index 54c721b8..a84f2251 100644 --- a/bridger-cli/package.json +++ b/bridger-cli/package.json @@ -10,7 +10,8 @@ "yarn": "4.2.2" }, "scripts": { - "start-bridger": "npx ts-node ./src/bridger.ts" + "start-bridger": "npx ts-node ./src/bridger.ts", + "test": "mocha --timeout 10000 --import=tsx src/utils/**/*.test.ts --exit" }, "dependencies": { "@kleros/vea-contracts": "workspace:^", @@ -18,5 +19,10 @@ "dotenv": "^16.4.5", "typescript": "^4.9.5", "web3": "^1.10.4" + }, + "devDependencies": { + "@types/chai": "^5", + "chai": "^5.1.2", + "mocha": "^11.0.1" } } diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts index 4186c965..3e522974 100644 --- a/bridger-cli/src/bridger.ts +++ b/bridger-cli/src/bridger.ts @@ -2,10 +2,11 @@ require("dotenv").config(); import { JsonRpcProvider } from "@ethersproject/providers"; import { ethers } from "ethers"; import { getClaimForEpoch, ClaimData, getLastClaimedEpoch } from "utils/graphQueries"; -import { getVeaInbox, getVeaOutboxDevnet } from "utils/ethers"; +import { getVeaInbox, getVeaOutbox } from "utils/ethers"; import { getBridgeConfig } from "consts/bridgeRoutes"; -const watch = async () => { +export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal(), startEpoch: number = 24) => { + console.log("Starting bridger"); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); const bridgeConfig = getBridgeConfig(chainId); const veaInboxAddress = process.env.VEAINBOX_ADDRESS; @@ -16,14 +17,16 @@ const watch = async () => { const PRIVATE_KEY = process.env.PRIVATE_KEY; const veaInbox = getVeaInbox(veaInboxAddress, PRIVATE_KEY, veaInboxProviderURL, chainId); - const veaOutbox = getVeaOutboxDevnet(veaOutboxAddress, PRIVATE_KEY, veaOutboxProviderURL, chainId); + const veaOutbox = getVeaOutbox(veaOutboxAddress, PRIVATE_KEY, veaOutboxProviderURL, chainId); - // Check if the current epoch is claimed on veaOutbox - const currentEpoch = Math.floor(Date.now() / 1000 / bridgeConfig.epochPeriod); - const epochs: number[] = new Array(24).fill(currentEpoch - 24).map((el, i) => el + i); + const currentEpoch = Number(await veaOutbox.epochNow()); + if (currentEpoch < startEpoch) { + throw new Error("Current epoch is less than start epoch"); + } + const epochs: number[] = new Array(currentEpoch - startEpoch).fill(startEpoch).map((el, i) => el + i); let verifiableEpoch = currentEpoch - 1; - - while (true) { + console.log("Current epoch: " + currentEpoch); + while (!shutDownSignal.getIsShutdownSignal()) { let i = 0; while (i < epochs.length) { const activeEpoch = epochs[i]; @@ -102,7 +105,7 @@ const watch = async () => { // Check if the verification is already resolved if (hashClaim(claim) == claimableEpochHash) { // Claim not resolved yet, check if we can verifySnapshot - if (finalizedOutboxBlock.timestamp - claim.timestampVerification > bridgeConfig.minChallengePeriod) { + if (finalizedOutboxBlock.timestamp - claim.timestampVerification >= bridgeConfig.minChallengePeriod) { console.log("Verification period passed, verifying snapshot"); // Estimate gas for verifySnapshot const verifySnapshotTxn = await veaOutbox.verifySnapshot(activeEpoch, claim); @@ -125,7 +128,7 @@ const watch = async () => { epochs.splice(i, 1); i--; } - } else { + } else if (!claimData.challenged) { console.log("Verification not started yet"); // No verification started yet, check if we can start it if ( @@ -143,6 +146,8 @@ const watch = async () => { bridgeConfig.epochPeriod; console.log("Sequencer delay not passed yet, seconds left: " + -1 * timeLeft); } + } else { + console.log("Claim was challenged, skipping"); } } else { epochs.splice(i, 1); @@ -162,7 +167,7 @@ const watch = async () => { const wait = (ms) => new Promise((r) => setTimeout(r, ms)); -const hashClaim = (claim) => { +export const hashClaim = (claim) => { return ethers.utils.solidityKeccak256( ["bytes32", "address", "uint32", "uint32", "uint32", "uint8", "address"], [ @@ -177,4 +182,23 @@ const hashClaim = (claim) => { ); }; -watch(); +export class ShutdownSignal { + private isShutdownSignal: boolean; + + constructor(initialState: boolean = false) { + this.isShutdownSignal = initialState; + } + + public getIsShutdownSignal(): boolean { + return this.isShutdownSignal; + } + + public setShutdownSignal(): void { + this.isShutdownSignal = true; + } +} + +if (require.main === module) { + const shutDownSignal = new ShutdownSignal(false); + watch(shutDownSignal); +} diff --git a/bridger-cli/src/utils/bridger.test.ts b/bridger-cli/src/utils/bridger.test.ts new file mode 100644 index 00000000..a7737208 --- /dev/null +++ b/bridger-cli/src/utils/bridger.test.ts @@ -0,0 +1,344 @@ +require("dotenv").config(); +import { assert } from "chai"; +import { hashClaim } from "../bridger"; +import { getVeaOutbox, getVeaInbox } from "./ethers"; +import { watch, ShutdownSignal } from "../bridger"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { BigNumber, utils } from "ethers"; +import { getClaimForEpoch } from "./graphQueries"; + +describe("Testing bridger-cli", function () { + console.log = function () {}; + const ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000"; + const FAKE_HASH = "0x0000000000000000000000000000000000000000000000000000000000000001"; + const mockMessage = { + data: "0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcd00000000000000000000000000000000000000000000000000000000000003e8", + to: "0x1234567890abcdef1234567890abcdef12345678", + fnSelector: "0x12345678", + }; + const inboxProvider = new JsonRpcProvider("http://localhost:8545"); + const outboxProvider = new JsonRpcProvider("http://localhost:8546"); + + let claimEpoch: number; + let epochPeriod: number; + let deposit: BigNumber; + let sequencerDelay: number; + + const veaInbox = getVeaInbox( + process.env.VEAINBOX_ADDRESS, + process.env.PRIVATE_KEY, + "http://localhost:8545", + Number(process.env.VEAOUTBOX_CHAIN_ID) + ); + const veaOutbox = getVeaOutbox( + process.env.VEAOUTBOX_ADDRESS, + process.env.PRIVATE_KEY, + "http://localhost:8546", + Number(process.env.VEAOUTBOX_CHAIN_ID) + ); + + // Increase epoch on both evn chains to maintain consistency + async function increaseEpoch() { + await inboxProvider.send("evm_increaseTime", [epochPeriod]); + await outboxProvider.send("evm_increaseTime", [epochPeriod]); + await inboxProvider.send("evm_mine", []); + await outboxProvider.send("evm_mine", []); + } + + // Start bridger with a timeout + async function startBridgerWithTimeout(timeout: number, startEpoch: number = 0) { + const shutDownSignal = new ShutdownSignal(); + const bridgerPromise = watch(shutDownSignal, startEpoch); + const timeoutPromise = new Promise((resolve) => { + setTimeout(() => { + shutDownSignal.setShutdownSignal(); + resolve("Timeout reached"); + }, timeout); + }); + await Promise.race([bridgerPromise, timeoutPromise]); + } + + // Sync epochs on both chains + async function syncEpochs() { + const inboxEpoch = Number(await veaInbox.epochNow()); + const outboxEpoch = Number(await veaOutbox.epochNow()); + if (inboxEpoch !== outboxEpoch) { + if (inboxEpoch > outboxEpoch) { + await outboxProvider.send("evm_increaseTime", [epochPeriod * (inboxEpoch - outboxEpoch)]); + await outboxProvider.send("evm_mine", []); + } else { + await inboxProvider.send("evm_increaseTime", [epochPeriod * (outboxEpoch - inboxEpoch)]); + await inboxProvider.send("evm_mine", []); + } + } + } + + beforeEach(async () => { + epochPeriod = Number(await veaOutbox.epochPeriod()); + deposit = await veaOutbox.deposit(); + await increaseEpoch(); + await syncEpochs(); + claimEpoch = Number(await veaInbox.epochNow()); + sequencerDelay = Number(await veaOutbox.sequencerDelayLimit()); + }); + + describe("Unit tests", function () { + it("should return correct hash for a claim", async function () { + const claim = { + stateRoot: "0x771d351d6f9f28f73f6321f0728caf54cda07d9897a4a809ea89cbeda0f084e3", + claimer: "0xFa00D29d378EDC57AA1006946F0fc6230a5E3288", + timestampClaimed: 1, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + + const localHash = hashClaim(claim); + const contractClaimHash = await veaOutbox.hashClaim(claim); + assert.equal(localHash, contractClaimHash, "Hashes do not match"); + }); + }); + + describe("Integration tests: Claiming", function () { + it.only("should claim for new saved snapshot", async function () { + // Send a message and save snapshot + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + await veaInbox.saveSnapshot(); + + // Increase epoch so that claim can be made + await increaseEpoch(); + + // Start bridger + await startBridgerWithTimeout(5000, claimEpoch); + + const toBeClaimedStateRoot = await veaInbox.snapshots(claimEpoch); + const claimData = await getClaimForEpoch(Number(process.env.VEAOUTBOX_CHAIN_ID), claimEpoch); + const claim = { + stateRoot: toBeClaimedStateRoot, + claimer: claimData.bridger, + timestampClaimed: claimData.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + // Check if claim was made + const claimHash = await veaOutbox.claimHashes(claimEpoch); + assert.notEqual(claimHash, ZERO_HASH, "Claim was not made"); + assert.equal(claimHash, hashClaim(claim), "Wrong claim was made"); + }); + + it("should not claim for old snapshot", async function () { + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + + // Increase epoch on both evn chains to maintain consistency + await increaseEpoch(); + + // Start bridger + await startBridgerWithTimeout(5000, claimEpoch); + + // Assert no new claims were made since last claim + const currentEpochClaim = await veaOutbox.claimHashes(claimEpoch); + assert.equal(currentEpochClaim, ZERO_HASH, "Claim was made"); + }); + + it("should make new claim if last claim was challenged", async function () { + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + await veaInbox.saveSnapshot(); + + // Increase epoch on both evn chains to maintain consistency + await increaseEpoch(); + await veaInbox.saveSnapshot(); + + // Make claim and challenge it + await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); + + const claimData = await getClaimForEpoch(Number(process.env.VEAOUTBOX_CHAIN_ID), claimEpoch); + const claim = { + stateRoot: FAKE_HASH, + claimer: claimData.bridger, + timestampClaimed: claimData.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + await veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"](claimEpoch, claim, { + value: deposit, + }); + // Increase epoch + await increaseEpoch(); + + await startBridgerWithTimeout(5000, claimEpoch); + + // Check if claim was made + const claimLogs = await outboxProvider.getLogs({ + address: process.env.VEAOUTBOX_ADDRESS, + topics: veaOutbox.filters.Claimed(null, utils.hexZeroPad(utils.hexValue(claimEpoch + 1), 32), null).topics, + }); + const snapshotOnInbox = await veaInbox.snapshots(claimEpoch + 1); + assert.equal(claimLogs.length, 1, "Claim was not made"); + assert.equal(Number(claimLogs[0].topics[2]), claimEpoch + 1, "Claim was made for wrong epoch"); + assert.equal(snapshotOnInbox, claimLogs[0].data, "Snapshot was not saved"); + }); + }); + + describe("Integration tests: Verification", function () { + it("should start verification when claim is verifiable", async function () { + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + await veaInbox.saveSnapshot(); + await increaseEpoch(); + + const savedSnapshot = await veaInbox.snapshots(claimEpoch); + await veaOutbox.claim(claimEpoch, savedSnapshot, { value: deposit }); + await outboxProvider.send("evm_mine", []); + + await inboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await inboxProvider.send("evm_mine", []); + await outboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await outboxProvider.send("evm_mine", []); + + await startBridgerWithTimeout(5000, claimEpoch); + + // Check if verification was started + const verificationLogs = await outboxProvider.getLogs({ + address: process.env.VEAOUTBOX_ADDRESS, + topics: veaOutbox.filters.VerificationStarted(claimEpoch).topics, + }); + assert.equal(verificationLogs.length, 1, "Verification was not started"); + }); + + it("should not verify claim when claim is challenged", async function () { + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + await veaInbox.saveSnapshot(); + + await increaseEpoch(); + + // Make claim and challenge it + const claimTx = await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); + + const oldClaimLogs = await outboxProvider.getLogs({ + address: process.env.VEAOUTBOX_ADDRESS, + topics: veaOutbox.filters.Claimed(null, utils.hexZeroPad(utils.hexValue(claimEpoch), 32), null).topics, + }); + const claimBlock = await outboxProvider.getBlock(oldClaimLogs[0].blockNumber); + + let claim = { + stateRoot: FAKE_HASH, + claimer: claimTx.from, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + await veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"](claimEpoch, claim, { + value: deposit, + }); + + await inboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await inboxProvider.send("evm_mine", []); + await outboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await outboxProvider.send("evm_mine", []); + + await startBridgerWithTimeout(5000, claimEpoch); + + const verificationLogs = await outboxProvider.getLogs({ + address: process.env.VEAOUTBOX_ADDRESS, + topics: veaOutbox.filters.VerificationStarted(claimEpoch).topics, + }); + assert.equal(verificationLogs.length, 0, "Verification was started"); + }); + + it("should verify snapshot when claim is verified", async function () { + // Also add a test for the case when the claim is not verifiable + + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + await veaInbox.saveSnapshot(); + await increaseEpoch(); + + const claimTxn = await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); + const claimBlock = await outboxProvider.getBlock(claimTxn.blockNumber); + await inboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await inboxProvider.send("evm_mine", []); + await outboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await outboxProvider.send("evm_mine", []); + + var claim = { + stateRoot: FAKE_HASH, + claimer: claimTxn.from, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + const verifyTxn = await veaOutbox.startVerification(claimEpoch, claim); + const verifyBlock = await outboxProvider.getBlock(verifyTxn.blockNumber); + claim.timestampVerification = verifyBlock.timestamp; + claim.blocknumberVerification = verifyBlock.number; + + const minChallengePeriod = Number(await veaOutbox.minChallengePeriod()); + + await outboxProvider.send("evm_increaseTime", [minChallengePeriod]); + await outboxProvider.send("evm_mine", []); + + await startBridgerWithTimeout(5000, claimEpoch); + + const postVerifyStateRoot = await veaOutbox.stateRoot(); + const latestVerifiedEpoch = await veaOutbox.latestVerifiedEpoch(); + + assert.equal(postVerifyStateRoot, FAKE_HASH, "Snapshot was not verified"); + assert.equal(Number(latestVerifiedEpoch), claimEpoch, "Snapshot was veified for wrong epoch"); + }); + + it("should withdraw deposit when claim is verified", async function () { + await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); + await veaInbox.saveSnapshot(); + await increaseEpoch(); + + const claimTxn = await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); + const claimBlock = await outboxProvider.getBlock(claimTxn.blockNumber); + await inboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await inboxProvider.send("evm_mine", []); + await outboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); + await outboxProvider.send("evm_mine", []); + + var claim = { + stateRoot: FAKE_HASH, + claimer: claimTxn.from, + timestampClaimed: claimBlock.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x0000000000000000000000000000000000000000", + }; + const verifyTxn = await veaOutbox.startVerification(claimEpoch, claim); + const verifyBlock = await outboxProvider.getBlock(verifyTxn.blockNumber); + claim.timestampVerification = verifyBlock.timestamp; + claim.blocknumberVerification = verifyBlock.number; + + const minChallengePeriod = Number(await veaOutbox.minChallengePeriod()); + + await outboxProvider.send("evm_increaseTime", [minChallengePeriod]); + await outboxProvider.send("evm_mine", []); + + await veaOutbox.verifySnapshot(claimEpoch, claim); + + const balancePreWithdraw = Number(await outboxProvider.getBalance(claimTxn.from)); + const contractBalancePreWithdraw = Number(await outboxProvider.getBalance(veaOutbox.address)); + + await startBridgerWithTimeout(5000, claimEpoch); + const balancePostWithdraw = Number(await outboxProvider.getBalance(claimTxn.from)); + const contractBalancePostWithdraw = Number(await outboxProvider.getBalance(veaOutbox.address)); + // Check if deposit was withdrawn + assert.isAbove(balancePostWithdraw, balancePreWithdraw, "Deposit was not withdrawn"); + assert.equal( + contractBalancePostWithdraw, + contractBalancePreWithdraw - Number(deposit), + "Deposit was not withdrawn" + ); + }); + }); +}); diff --git a/bridger-cli/src/utils/graphQueries.ts b/bridger-cli/src/utils/graphQueries.ts index 59e2284b..68a7b73d 100644 --- a/bridger-cli/src/utils/graphQueries.ts +++ b/bridger-cli/src/utils/graphQueries.ts @@ -14,7 +14,7 @@ const getClaimForEpoch = async (chainid: number, epoch: number): Promise => { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; const result = await request( - `https://api.studio.thegraph.com/query/${subgraph}`, + `${subgraph}`, `{ claims(first:1, orderBy:timestamp, orderDirection:desc){ id From 84b65d68365dfe4bc8f872d0dd7604b002a847e2 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Tue, 3 Dec 2024 19:10:50 +0700 Subject: [PATCH 06/15] fix: early graph calls --- bridger-cli/src/utils/bridger.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bridger-cli/src/utils/bridger.test.ts b/bridger-cli/src/utils/bridger.test.ts index a7737208..1e4dfdd6 100644 --- a/bridger-cli/src/utils/bridger.test.ts +++ b/bridger-cli/src/utils/bridger.test.ts @@ -101,7 +101,7 @@ describe("Testing bridger-cli", function () { }); describe("Integration tests: Claiming", function () { - it.only("should claim for new saved snapshot", async function () { + it("should claim for new saved snapshot", async function () { // Send a message and save snapshot await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); await veaInbox.saveSnapshot(); @@ -152,13 +152,13 @@ describe("Testing bridger-cli", function () { await veaInbox.saveSnapshot(); // Make claim and challenge it - await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); + const claimTxn = await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); + const claimBlock = await outboxProvider.getBlock(claimTxn.blockNumber); - const claimData = await getClaimForEpoch(Number(process.env.VEAOUTBOX_CHAIN_ID), claimEpoch); const claim = { stateRoot: FAKE_HASH, - claimer: claimData.bridger, - timestampClaimed: claimData.timestamp, + claimer: claimTxn.from, + timestampClaimed: claimBlock.timestamp, timestampVerification: 0, blocknumberVerification: 0, honest: 0, From 9dd90ccdfe15e74a85bea514b4721c08cab0c249 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 9 Dec 2024 16:25:13 +0700 Subject: [PATCH 07/15] feat: refactor & unit tests --- bridger-cli/jest.config.ts | 10 + bridger-cli/package.json | 8 +- bridger-cli/src/{utils => }/bridger.test.ts | 67 ++--- bridger-cli/src/bridger.ts | 158 +++------- bridger-cli/src/utils/claim.test.ts | 168 +++++++++++ bridger-cli/src/utils/claim.ts | 73 +++++ bridger-cli/src/utils/epochHandler.test.ts | 93 ++++++ bridger-cli/src/utils/epochHandler.ts | 31 ++ bridger-cli/src/utils/graphQueries.ts | 47 +-- bridger-cli/src/utils/shutdown.ts | 15 + .../src/utils/transactionHandler.test.ts | 275 ++++++++++++++++++ bridger-cli/src/utils/transactionHandler.ts | 145 +++++++++ contracts/Dockerfile | 3 + 13 files changed, 884 insertions(+), 209 deletions(-) create mode 100644 bridger-cli/jest.config.ts rename bridger-cli/src/{utils => }/bridger.test.ts (85%) create mode 100644 bridger-cli/src/utils/claim.test.ts create mode 100644 bridger-cli/src/utils/claim.ts create mode 100644 bridger-cli/src/utils/epochHandler.test.ts create mode 100644 bridger-cli/src/utils/epochHandler.ts create mode 100644 bridger-cli/src/utils/shutdown.ts create mode 100644 bridger-cli/src/utils/transactionHandler.test.ts create mode 100644 bridger-cli/src/utils/transactionHandler.ts diff --git a/bridger-cli/jest.config.ts b/bridger-cli/jest.config.ts new file mode 100644 index 00000000..1927a555 --- /dev/null +++ b/bridger-cli/jest.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "jest"; + +const config: Config = { + preset: "ts-jest", + testEnvironment: "node", + collectCoverage: true, + collectCoverageFrom: ["**/*.ts"], +}; + +export default config; diff --git a/bridger-cli/package.json b/bridger-cli/package.json index a84f2251..26dc9586 100644 --- a/bridger-cli/package.json +++ b/bridger-cli/package.json @@ -11,7 +11,7 @@ }, "scripts": { "start-bridger": "npx ts-node ./src/bridger.ts", - "test": "mocha --timeout 10000 --import=tsx src/utils/**/*.test.ts --exit" + "test": "jest --coverage" }, "dependencies": { "@kleros/vea-contracts": "workspace:^", @@ -21,8 +21,8 @@ "web3": "^1.10.4" }, "devDependencies": { - "@types/chai": "^5", - "chai": "^5.1.2", - "mocha": "^11.0.1" + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.2.5" } } diff --git a/bridger-cli/src/utils/bridger.test.ts b/bridger-cli/src/bridger.test.ts similarity index 85% rename from bridger-cli/src/utils/bridger.test.ts rename to bridger-cli/src/bridger.test.ts index 1e4dfdd6..cb5aa214 100644 --- a/bridger-cli/src/utils/bridger.test.ts +++ b/bridger-cli/src/bridger.test.ts @@ -1,13 +1,14 @@ require("dotenv").config(); import { assert } from "chai"; -import { hashClaim } from "../bridger"; -import { getVeaOutbox, getVeaInbox } from "./ethers"; -import { watch, ShutdownSignal } from "../bridger"; +import { hashClaim } from "./utils/claim"; +import { getVeaOutbox, getVeaInbox } from "./utils/ethers"; +import { watch } from "./bridger"; +import { ShutdownSignal } from "./utils/shutdown"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { BigNumber, utils } from "ethers"; -import { getClaimForEpoch } from "./graphQueries"; +import { BigNumber } from "ethers"; -describe("Testing bridger-cli", function () { +jest.setTimeout(15000); +describe("bridger", function () { console.log = function () {}; const ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000"; const FAKE_HASH = "0x0000000000000000000000000000000000000000000000000000000000000001"; @@ -82,24 +83,6 @@ describe("Testing bridger-cli", function () { sequencerDelay = Number(await veaOutbox.sequencerDelayLimit()); }); - describe("Unit tests", function () { - it("should return correct hash for a claim", async function () { - const claim = { - stateRoot: "0x771d351d6f9f28f73f6321f0728caf54cda07d9897a4a809ea89cbeda0f084e3", - claimer: "0xFa00D29d378EDC57AA1006946F0fc6230a5E3288", - timestampClaimed: 1, - timestampVerification: 0, - blocknumberVerification: 0, - honest: 0, - challenger: "0x0000000000000000000000000000000000000000", - }; - - const localHash = hashClaim(claim); - const contractClaimHash = await veaOutbox.hashClaim(claim); - assert.equal(localHash, contractClaimHash, "Hashes do not match"); - }); - }); - describe("Integration tests: Claiming", function () { it("should claim for new saved snapshot", async function () { // Send a message and save snapshot @@ -110,14 +93,16 @@ describe("Testing bridger-cli", function () { await increaseEpoch(); // Start bridger - await startBridgerWithTimeout(5000, claimEpoch); + await startBridgerWithTimeout(1000, claimEpoch); const toBeClaimedStateRoot = await veaInbox.snapshots(claimEpoch); - const claimData = await getClaimForEpoch(Number(process.env.VEAOUTBOX_CHAIN_ID), claimEpoch); + const claimData = await veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, claimEpoch, null)); + const bridger = `0x${claimData[0].topics[1].slice(26)}`; + const claimBlock = await outboxProvider.getBlock(claimData[0].blockNumber); const claim = { stateRoot: toBeClaimedStateRoot, - claimer: claimData.bridger, - timestampClaimed: claimData.timestamp, + claimer: bridger, + timestampClaimed: claimBlock.timestamp, timestampVerification: 0, blocknumberVerification: 0, honest: 0, @@ -173,10 +158,8 @@ describe("Testing bridger-cli", function () { await startBridgerWithTimeout(5000, claimEpoch); // Check if claim was made - const claimLogs = await outboxProvider.getLogs({ - address: process.env.VEAOUTBOX_ADDRESS, - topics: veaOutbox.filters.Claimed(null, utils.hexZeroPad(utils.hexValue(claimEpoch + 1), 32), null).topics, - }); + const claimLogs = await veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, claimEpoch + 1, null)); + const snapshotOnInbox = await veaInbox.snapshots(claimEpoch + 1); assert.equal(claimLogs.length, 1, "Claim was not made"); assert.equal(Number(claimLogs[0].topics[2]), claimEpoch + 1, "Claim was made for wrong epoch"); @@ -202,10 +185,7 @@ describe("Testing bridger-cli", function () { await startBridgerWithTimeout(5000, claimEpoch); // Check if verification was started - const verificationLogs = await outboxProvider.getLogs({ - address: process.env.VEAOUTBOX_ADDRESS, - topics: veaOutbox.filters.VerificationStarted(claimEpoch).topics, - }); + const verificationLogs = await veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(claimEpoch)); assert.equal(verificationLogs.length, 1, "Verification was not started"); }); @@ -218,10 +198,7 @@ describe("Testing bridger-cli", function () { // Make claim and challenge it const claimTx = await veaOutbox.claim(claimEpoch, FAKE_HASH, { value: deposit }); - const oldClaimLogs = await outboxProvider.getLogs({ - address: process.env.VEAOUTBOX_ADDRESS, - topics: veaOutbox.filters.Claimed(null, utils.hexZeroPad(utils.hexValue(claimEpoch), 32), null).topics, - }); + const oldClaimLogs = await veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, claimEpoch, null)); const claimBlock = await outboxProvider.getBlock(oldClaimLogs[0].blockNumber); let claim = { @@ -236,6 +213,7 @@ describe("Testing bridger-cli", function () { await veaOutbox["challenge(uint256,(bytes32,address,uint32,uint32,uint32,uint8,address))"](claimEpoch, claim, { value: deposit, }); + await outboxProvider.send("evm_mine", []); await inboxProvider.send("evm_increaseTime", [epochPeriod + sequencerDelay]); await inboxProvider.send("evm_mine", []); @@ -244,10 +222,7 @@ describe("Testing bridger-cli", function () { await startBridgerWithTimeout(5000, claimEpoch); - const verificationLogs = await outboxProvider.getLogs({ - address: process.env.VEAOUTBOX_ADDRESS, - topics: veaOutbox.filters.VerificationStarted(claimEpoch).topics, - }); + const verificationLogs = await veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(claimEpoch)); assert.equal(verificationLogs.length, 0, "Verification was started"); }); @@ -293,7 +268,7 @@ describe("Testing bridger-cli", function () { assert.equal(Number(latestVerifiedEpoch), claimEpoch, "Snapshot was veified for wrong epoch"); }); - it("should withdraw deposit when claim is verified", async function () { + it("should withdraw deposit when claimer is honest", async function () { await veaInbox.sendMessage(mockMessage.to, mockMessage.fnSelector, mockMessage.data); await veaInbox.saveSnapshot(); await increaseEpoch(); @@ -340,5 +315,7 @@ describe("Testing bridger-cli", function () { "Deposit was not withdrawn" ); }); + + it.todo("should not withdraw deposit when claimer is dishonest"); }); }); diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts index 3e522974..1e515d37 100644 --- a/bridger-cli/src/bridger.ts +++ b/bridger-cli/src/bridger.ts @@ -1,31 +1,27 @@ require("dotenv").config(); -import { JsonRpcProvider } from "@ethersproject/providers"; import { ethers } from "ethers"; -import { getClaimForEpoch, ClaimData, getLastClaimedEpoch } from "utils/graphQueries"; -import { getVeaInbox, getVeaOutbox } from "utils/ethers"; -import { getBridgeConfig } from "consts/bridgeRoutes"; - -export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal(), startEpoch: number = 24) => { +import { getLastClaimedEpoch } from "./utils/graphQueries"; +import { getVeaInbox, getVeaOutbox } from "./utils/ethers"; +import { fetchClaim, hashClaim } from "./utils/claim"; +import { TransactionHandler } from "./utils/transactionHandler"; +import { setEpochRange, checkForNewEpoch } from "./utils/epochHandler"; +import { ShutdownSignal } from "./utils/shutdown"; + +export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal(), startEpoch: number = 0) => { console.log("Starting bridger"); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); - const bridgeConfig = getBridgeConfig(chainId); const veaInboxAddress = process.env.VEAINBOX_ADDRESS; const veaInboxProviderURL = process.env.VEAINBOX_PROVIDER; const veaOutboxAddress = process.env.VEAOUTBOX_ADDRESS; const veaOutboxProviderURL = process.env.VEAOUTBOX_PROVIDER; - const veaOutboxJSON = new JsonRpcProvider(veaOutboxProviderURL); const PRIVATE_KEY = process.env.PRIVATE_KEY; - const veaInbox = getVeaInbox(veaInboxAddress, PRIVATE_KEY, veaInboxProviderURL, chainId); const veaOutbox = getVeaOutbox(veaOutboxAddress, PRIVATE_KEY, veaOutboxProviderURL, chainId); + const epochs = await setEpochRange(veaOutbox, startEpoch); + let verifiableEpoch = epochs[epochs.length - 1] - 1; + + const transactionHandlers: { [epoch: number]: TransactionHandler } = {}; - const currentEpoch = Number(await veaOutbox.epochNow()); - if (currentEpoch < startEpoch) { - throw new Error("Current epoch is less than start epoch"); - } - const epochs: number[] = new Array(currentEpoch - startEpoch).fill(startEpoch).map((el, i) => el + i); - let verifiableEpoch = currentEpoch - 1; - console.log("Current epoch: " + currentEpoch); while (!shutDownSignal.getIsShutdownSignal()) { let i = 0; while (i < epochs.length) { @@ -33,7 +29,7 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( console.log("Checking for epoch " + activeEpoch); let claimableEpochHash = await veaOutbox.claimHashes(activeEpoch); let outboxStateRoot = await veaOutbox.stateRoot(); - const finalizedOutboxBlock = await veaOutboxJSON.getBlock("finalized"); + const finalizedOutboxBlock = await veaOutbox.provider.getBlock("finalized"); if (claimableEpochHash == ethers.constants.HashZero && activeEpoch == verifiableEpoch) { // Claim can be made @@ -41,20 +37,14 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( if (savedSnapshot != outboxStateRoot && savedSnapshot != ethers.constants.HashZero) { // Its possible that a claim was made for previous epoch but its not verified yet // Making claim if there are new messages or last claim was challenged. - const claimData = await getLastClaimedEpoch(chainId); + const claimData = await getLastClaimedEpoch(); if (claimData.challenged || claimData.stateroot != savedSnapshot) { // Making claim as either last claim was challenged or there are new messages - - const gasEstimate = await veaOutbox.estimateGas.claim(activeEpoch, savedSnapshot, { - value: bridgeConfig.deposit, - }); - - const claimTransaction = await veaOutbox.claim(activeEpoch, savedSnapshot, { - value: bridgeConfig.deposit, - gasLimit: gasEstimate, - }); - console.log(`Epoch ${activeEpoch} was claimed with trnx hash ${claimTransaction.hash}`); + if (!transactionHandlers[activeEpoch]) { + transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox); + } + await transactionHandlers[activeEpoch].makeClaim(savedSnapshot); } else { console.log("No new messages, no need for a claim"); epochs.splice(i, 1); @@ -72,81 +62,37 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( } } else if (claimableEpochHash != ethers.constants.HashZero) { console.log("Claim is already made, checking for verification stage"); - const claimData: ClaimData = await getClaimForEpoch(chainId, activeEpoch); - if (claimData == undefined) { - console.log(`Claim data not found for ${activeEpoch}, skipping for now`); - continue; + const claim = await fetchClaim(veaOutbox, activeEpoch, chainId); + console.log(claim); + if (!transactionHandlers[activeEpoch]) { + transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox, claim); + } else { + transactionHandlers[activeEpoch].claim = claim; } - var claim = { - stateRoot: claimData.stateroot, - claimer: claimData.bridger, - timestampClaimed: claimData.timestamp, - timestampVerification: 0, - blocknumberVerification: 0, - honest: 0, - challenger: "0x0000000000000000000000000000000000000000", - }; - const claimTransaction = await veaOutboxJSON.getTransaction(claimData.txHash); - - // ToDo: Update subgraph to get verification start data - const verifiactionLogs = await veaOutboxJSON.getLogs({ - address: veaOutboxAddress, - topics: veaOutbox.filters.VerificationStarted(activeEpoch).topics, - fromBlock: claimTransaction.blockNumber, - toBlock: "latest", - }); - - if (verifiactionLogs.length > 0) { - // Verification started update the claim struct - const verificationStartBlock = await veaOutboxJSON.getBlock(verifiactionLogs[0].blockHash); - claim.timestampVerification = verificationStartBlock.timestamp; - claim.blocknumberVerification = verificationStartBlock.number; - + const transactionHandler = transactionHandlers[activeEpoch]; + if (claim.timestampVerification != 0) { // Check if the verification is already resolved if (hashClaim(claim) == claimableEpochHash) { - // Claim not resolved yet, check if we can verifySnapshot - if (finalizedOutboxBlock.timestamp - claim.timestampVerification >= bridgeConfig.minChallengePeriod) { - console.log("Verification period passed, verifying snapshot"); - // Estimate gas for verifySnapshot - const verifySnapshotTxn = await veaOutbox.verifySnapshot(activeEpoch, claim); - console.log(`Verified snapshot for epoch ${activeEpoch} with trnx hash ${verifySnapshotTxn.hash}`); - } else { - console.log( - "Censorship test in progress, sec left: " + - -1 * (finalizedOutboxBlock.timestamp - claim.timestampVerification - bridgeConfig.minChallengePeriod) - ); - } + // Claim not resolved yet, try to verify snapshot + await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp); } else { // Claim is already verified, withdraw deposit claim.honest = 1; // Assume the claimer is honest if (hashClaim(claim) == claimableEpochHash) { - const withdrawDepositTxn = await veaOutbox.withdrawClaimDeposit(activeEpoch, claim); - console.log(`Withdrew deposit for epoch ${activeEpoch} with trnx hash ${withdrawDepositTxn.hash}`); + await transactionHandler.withdrawClaimDeposit(); } else { console.log("Challenger won claim"); } epochs.splice(i, 1); i--; } - } else if (!claimData.challenged) { + } else if (claim.challenger == ethers.constants.AddressZero) { console.log("Verification not started yet"); // No verification started yet, check if we can start it - if ( - finalizedOutboxBlock.timestamp - claim.timestampClaimed > - bridgeConfig.sequencerDelayLimit + bridgeConfig.epochPeriod - ) { - const startVerifTrx = await veaOutbox.startVerification(activeEpoch, claim); - console.log(`Verification started for epoch ${activeEpoch} with trx hash ${startVerifTrx.hash}`); - // Update local struct for trnx hash and block number as it takes time for the trnx to be mined. - } else { - const timeLeft = - finalizedOutboxBlock.timestamp - - claim.timestampClaimed - - bridgeConfig.sequencerDelayLimit - - bridgeConfig.epochPeriod; - console.log("Sequencer delay not passed yet, seconds left: " + -1 * timeLeft); - } + await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); } else { + epochs.splice(i, 1); + i--; console.log("Claim was challenged, skipping"); } } else { @@ -156,48 +102,16 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( } i++; } - if (Math.floor(Date.now() / 1000 / bridgeConfig.epochPeriod) - 1 > verifiableEpoch) { - verifiableEpoch = Math.floor(Date.now() / 1000 / bridgeConfig.epochPeriod) - 1; - epochs.push(verifiableEpoch); - } + const newEpoch = checkForNewEpoch(verifiableEpoch, chainId); + if (newEpoch != verifiableEpoch) epochs.push(newEpoch); console.log("Waiting for next verifiable epoch after " + verifiableEpoch); await wait(1000 * 10); } + return epochs; }; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); -export const hashClaim = (claim) => { - return ethers.utils.solidityKeccak256( - ["bytes32", "address", "uint32", "uint32", "uint32", "uint8", "address"], - [ - claim.stateRoot, - claim.claimer, - claim.timestampClaimed, - claim.timestampVerification, - claim.blocknumberVerification, - claim.honest, - claim.challenger, - ] - ); -}; - -export class ShutdownSignal { - private isShutdownSignal: boolean; - - constructor(initialState: boolean = false) { - this.isShutdownSignal = initialState; - } - - public getIsShutdownSignal(): boolean { - return this.isShutdownSignal; - } - - public setShutdownSignal(): void { - this.isShutdownSignal = true; - } -} - if (require.main === module) { const shutDownSignal = new ShutdownSignal(false); watch(shutDownSignal); diff --git a/bridger-cli/src/utils/claim.test.ts b/bridger-cli/src/utils/claim.test.ts new file mode 100644 index 00000000..9788018a --- /dev/null +++ b/bridger-cli/src/utils/claim.test.ts @@ -0,0 +1,168 @@ +import { fetchClaim, ClaimStruct, hashClaim } from "./claim"; + +import { ethers } from "ethers"; + +describe("snapshotClaim", () => { + describe("fetchClaim", () => { + let mockClaim: ClaimStruct; + let getClaimForEpoch: jest.Mock; + let veaOutbox: any; + const epoch = 1; + const chainId = 1; + + beforeEach(() => { + mockClaim = { + stateRoot: "0x1234", + claimer: "0x1234", + timestampClaimed: 1234, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: ethers.constants.AddressZero, + }; + getClaimForEpoch = jest.fn().mockResolvedValue({ + stateroot: mockClaim.stateRoot, + bridger: mockClaim.claimer, + timestamp: mockClaim.timestampClaimed, + txHash: "0x1234", + challenged: false, + }); + + veaOutbox = { + queryFilter: jest.fn(), + provider: { + getBlock: jest.fn().mockResolvedValue({ timestamp: 1234, number: 1234 }), + }, + filters: { + VerificationStarted: jest.fn(), + Challenged: jest.fn(), + Claimed: jest.fn(), + }, + }; + }); + + it("should return a valid claim", async () => { + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + veaOutbox.queryFilter.mockImplementationOnce(() => + Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.constants.AddressZero } }]) + ); + const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + console.log(claim); + console.log(mockClaim); + expect(claim).toBeDefined(); + expect(claim).toEqual(mockClaim); + expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + }); + + it("should return a valid claim with challenger", async () => { + // we want fetchClaimForEpoch to return a claim + mockClaim.challenger = "0x1234"; + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + veaOutbox.queryFilter.mockImplementationOnce(() => + Promise.resolve([{ blockHash: "0x1234", args: { challenger: mockClaim.challenger } }]) + ); + // Update getClaimForEpoch to return a challenged claim + getClaimForEpoch.mockResolvedValueOnce({ + stateroot: mockClaim.stateRoot, + bridger: mockClaim.claimer, + timestamp: mockClaim.timestampClaimed, + txHash: "0x1234", + challenged: true, + }); + + const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + console.log(claim); + console.log(mockClaim); + expect(claim).toBeDefined(); + expect(claim).toEqual(mockClaim); + expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + }); + + it("should return a valid claim with verification", async () => { + mockClaim.timestampVerification = 1234; + mockClaim.blocknumberVerification = 1234; + veaOutbox.queryFilter.mockImplementationOnce(() => + Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.constants.AddressZero } }]) + ); + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + getClaimForEpoch.mockResolvedValueOnce({ + stateroot: mockClaim.stateRoot, + bridger: mockClaim.claimer, + timestamp: mockClaim.timestampClaimed, + txHash: "0x1234", + challenged: false, + }); + + veaOutbox.provider.getBlock.mockResolvedValueOnce({ + timestamp: mockClaim.timestampVerification, + number: mockClaim.blocknumberVerification, + }); + + const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + + expect(claim).toBeDefined(); + expect(claim).toEqual(mockClaim); + expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + }); + + it("should fallback on logs if claimData is undefined", async () => { + getClaimForEpoch.mockResolvedValueOnce(undefined); + veaOutbox.queryFilter.mockImplementationOnce(() => + Promise.resolve([ + { + blockNumber: 1234, + data: mockClaim.stateRoot, + topics: [ethers.constants.AddressZero, `0x${"0".repeat(24)}${mockClaim.claimer.slice(2)}`], + }, + ]) + ); + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + + const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + console.log(claim); + console.log(mockClaim); + expect(claim).toBeDefined(); + expect(claim).toEqual(mockClaim); + expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + }); + + it("should throw an error if no claim is found", async () => { + getClaimForEpoch.mockResolvedValueOnce(undefined); + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); + + await expect(async () => { + await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + }).rejects.toThrow(`No claim found for epoch ${epoch}`); + }); + }); + + describe("hashClaim", () => { + let mockClaim: ClaimStruct = { + stateRoot: "0xeac817ed5c5b3d1c2c548f231b7cf9a0dfd174059f450ec6f0805acf6a16a551", + claimer: "0xFa00D29d378EDC57AA1006946F0fc6230a5E3288", + timestampClaimed: 1730276784, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: ethers.constants.AddressZero, + }; + // Pre calculated from the deployed contracts + const hashOfMockClaim = "0xfee47661ef0432da320c3b4706ff7d412f421b9d1531c33ce8f2e03bfe5dcfa2"; + + it("should return a valid hash", () => { + const hash = hashClaim(mockClaim); + expect(hash).toBeDefined(); + expect(hash).toEqual(hashOfMockClaim); + }); + + it("should not return a valid hash", () => { + mockClaim.honest = 1; + const hash = hashClaim(mockClaim); + expect(hash).toBeDefined(); + expect(hash).not.toEqual(hashOfMockClaim); + }); + }); +}); diff --git a/bridger-cli/src/utils/claim.ts b/bridger-cli/src/utils/claim.ts new file mode 100644 index 00000000..a083136c --- /dev/null +++ b/bridger-cli/src/utils/claim.ts @@ -0,0 +1,73 @@ +import { ClaimData, getClaimForEpoch } from "./graphQueries"; +import { ethers } from "ethers"; +import { getBlockNumberFromEpoch } from "./epochHandler"; + +type ClaimStruct = { + stateRoot: string; + claimer: `0x${string}`; + timestampClaimed: number; + timestampVerification: number; + blocknumberVerification: number; + honest: number; + challenger: `0x${string}`; +}; + +const fetchClaim = async ( + veaOutbox, + epoch, + chainId, + fetchClaimForEpoch: typeof getClaimForEpoch = getClaimForEpoch +): Promise => { + let claimData: ClaimData | undefined = await fetchClaimForEpoch(epoch); + // ToDo: Check for logs block range Rpc dependency, if needed used claimEpochBlock + // let claimEpochBlock = await getBlockNumberFromEpoch(veaOutbox.provider, epoch, chainId); + if (claimData === undefined) { + // Initialize claimData as an empty object + claimData = {} as ClaimData; + const claimLogs = await veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, epoch, null)); + if (claimLogs.length === 0) { + throw new Error(`No claim found for epoch ${epoch}`); + } + claimData.bridger = `0x${claimLogs[0].topics[1].slice(26)}`; + claimData.stateroot = claimLogs[0].data; + claimData.timestamp = (await veaOutbox.provider.getBlock(claimLogs[0].blockNumber)).timestamp; + } + let claim: ClaimStruct = { + stateRoot: claimData.stateroot, + claimer: claimData.bridger as `0x${string}`, + timestampClaimed: claimData.timestamp, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: ethers.constants.AddressZero, + }; + const verifyLogs = await veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(epoch)); + if (verifyLogs.length > 0) { + const verificationStartBlock = await veaOutbox.provider.getBlock(verifyLogs[0].blockHash); + claim.timestampVerification = verificationStartBlock.timestamp; + claim.blocknumberVerification = verificationStartBlock.number; + } + const challengeLogs = await veaOutbox.queryFilter(veaOutbox.filters.Challenged(epoch)); + if (challengeLogs.length > 0) { + claim.challenger = challengeLogs[0].args.challenger; + } + + return claim; +}; + +const hashClaim = (claim) => { + return ethers.utils.solidityKeccak256( + ["bytes32", "address", "uint32", "uint32", "uint32", "uint8", "address"], + [ + claim.stateRoot, + claim.claimer, + claim.timestampClaimed, + claim.timestampVerification, + claim.blocknumberVerification, + claim.honest, + claim.challenger, + ] + ); +}; + +export { ClaimStruct, fetchClaim, hashClaim }; diff --git a/bridger-cli/src/utils/epochHandler.test.ts b/bridger-cli/src/utils/epochHandler.test.ts new file mode 100644 index 00000000..10130c34 --- /dev/null +++ b/bridger-cli/src/utils/epochHandler.test.ts @@ -0,0 +1,93 @@ +import { setEpochRange, getBlockNumberFromEpoch, checkForNewEpoch } from "./epochHandler"; + +describe("epochHandler", () => { + describe("setEpochRange", () => { + let veaOutbox: any; + let startEpoch: number; + beforeEach(() => { + veaOutbox = { + epochNow: jest.fn().mockResolvedValue(10), + }; + startEpoch = 10; + }); + it("should return an array of epoch", async () => { + const result = await setEpochRange(veaOutbox, startEpoch); + expect(result).toBeDefined(); + expect(result).toEqual([10]); + }); + + it("should throw an error if start { + startEpoch = 12; + await expect(setEpochRange(veaOutbox, startEpoch)).rejects.toThrow("Current epoch is less than start epoch"); + }); + + it("should return an array rolled back to default when no startEpoch provided", async () => { + const currEpoch = await veaOutbox.epochNow(); + // 10 is the default epoch rollback + const defaultEpochRollback = 10; + startEpoch = currEpoch - defaultEpochRollback; + const epochs: number[] = new Array(currEpoch - startEpoch + 1) + .fill(currEpoch - defaultEpochRollback) + .map((el, i) => el + i); + const result = await setEpochRange(veaOutbox, startEpoch); + expect(result).toBeDefined(); + expect(result).toEqual(epochs); + }); + }); + describe("getBlockNumberFromEpoch", () => { + let veaOutboxRpc: any; + let epoch: number; + const epochPeriod = 10; + beforeEach(() => { + veaOutboxRpc = { + getBlock: jest.fn().mockImplementation((blockNumber) => { + if (blockNumber === "latest") { + return { + number: 100000, + timestamp: 1000000, + }; + } else { + return { + number: 99000, + timestamp: 990000, + }; + } + }), + }; + epoch = 1000; + }); + it("should return epoch block number", async () => { + const result = await getBlockNumberFromEpoch(veaOutboxRpc, epoch, epochPeriod); + expect(result).toBeDefined(); + expect(result).toBe(900); + }); + }); + + describe("checkForNewEpoch", () => { + const epochPeriod = 10; + let currentEpoch: number; + + beforeEach(() => { + currentEpoch = 99; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should return a new epoch", () => { + const mockDateNow = jest.spyOn(Date, "now").mockImplementation(() => 1010 * 1000); + const result = checkForNewEpoch(currentEpoch, epochPeriod); + expect(result).toBe(100); + mockDateNow.mockRestore(); + }); + + it("should return no new epoch", () => { + const mockDateNow = jest.spyOn(Date, "now").mockImplementation(() => 1010 * 1000); + currentEpoch = 100; + const result = checkForNewEpoch(currentEpoch, epochPeriod); + expect(result).toBe(100); + mockDateNow.mockRestore(); + }); + }); +}); diff --git a/bridger-cli/src/utils/epochHandler.ts b/bridger-cli/src/utils/epochHandler.ts new file mode 100644 index 00000000..7c3b442b --- /dev/null +++ b/bridger-cli/src/utils/epochHandler.ts @@ -0,0 +1,31 @@ +const setEpochRange = async (veaOutbox, startEpoch): Promise> => { + const defaultEpochRollback = 10; // When no start epoch is provided, we will start from current epoch - defaultEpochRollback + const currentEpoch = Number(await veaOutbox.epochNow()); + if (currentEpoch < startEpoch) { + throw new Error("Current epoch is less than start epoch"); + } + if (startEpoch == 0) { + startEpoch = currentEpoch - defaultEpochRollback; + } + const epochs: number[] = new Array(currentEpoch - startEpoch + 1).fill(startEpoch).map((el, i) => el + i); + return epochs; +}; + +const getBlockNumberFromEpoch = async (veaOutboxRpc, epoch, epochPeriod): Promise => { + const latestBlock = await veaOutboxRpc.getBlock("latest"); + const preBlock = await veaOutboxRpc.getBlock(latestBlock.number - 1000); + const avgBlockTime = (latestBlock.timestamp - preBlock.timestamp) / 1000; + + const epochInSeconds = epoch * epochPeriod; + const epochBlock = Math.floor(latestBlock.number - (latestBlock.timestamp - epochInSeconds) / avgBlockTime); + return epochBlock - 100; +}; + +const checkForNewEpoch = (currentEpoch, epochPeriod): number => { + if (Math.floor(Date.now() / (1000 * epochPeriod)) - 1 > currentEpoch) { + currentEpoch = Math.floor(Date.now() / 1000 / epochPeriod) - 1; + } + return currentEpoch; +}; + +export { setEpochRange, getBlockNumberFromEpoch, checkForNewEpoch }; diff --git a/bridger-cli/src/utils/graphQueries.ts b/bridger-cli/src/utils/graphQueries.ts index 68a7b73d..70b59713 100644 --- a/bridger-cli/src/utils/graphQueries.ts +++ b/bridger-cli/src/utils/graphQueries.ts @@ -9,7 +9,7 @@ interface ClaimData { txHash: string; } -const getClaimForEpoch = async (chainid: number, epoch: number): Promise => { +const getClaimForEpoch = async (epoch: number): Promise => { try { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; @@ -33,37 +33,12 @@ const getClaimForEpoch = async (chainid: number, epoch: number): Promise => { - try { - const subgraph = process.env.VEAOUTBOX_SUBGRAPH; - - const result = await request( - `${subgraph}`, - `{ - verifications(where:{claim_:{epoch:${epoch}}}){ - claim{ - stateroot - epoch - } - timestamp - id - } - }` - ); - return result[`verifications`][0]; - } catch (e) { - console.log(e); - return undefined; - } -}; +const getLastClaimedEpoch = async (): Promise => { + const subgraph = process.env.VEAOUTBOX_SUBGRAPH; -const getLastClaimedEpoch = async (chainid: number): Promise => { - try { - const subgraph = process.env.VEAOUTBOX_SUBGRAPH; - - const result = await request( - `${subgraph}`, - `{ + const result = await request( + `${subgraph}`, + `{ claims(first:1, orderBy:timestamp, orderDirection:desc){ id bridger @@ -76,12 +51,8 @@ const getLastClaimedEpoch = async (chainid: number): Promise => { } }` - ); - return result[`claims`][0]; - } catch (e) { - console.log(e); - return undefined; - } + ); + return result[`claims`][0]; }; -export { getClaimForEpoch, getVerificationStatus, getLastClaimedEpoch, ClaimData }; +export { getClaimForEpoch, getLastClaimedEpoch, ClaimData }; diff --git a/bridger-cli/src/utils/shutdown.ts b/bridger-cli/src/utils/shutdown.ts new file mode 100644 index 00000000..221b42be --- /dev/null +++ b/bridger-cli/src/utils/shutdown.ts @@ -0,0 +1,15 @@ +export class ShutdownSignal { + private isShutdownSignal: boolean; + + constructor(initialState: boolean = false) { + this.isShutdownSignal = initialState; + } + + public getIsShutdownSignal(): boolean { + return this.isShutdownSignal; + } + + public setShutdownSignal(): void { + this.isShutdownSignal = true; + } +} diff --git a/bridger-cli/src/utils/transactionHandler.test.ts b/bridger-cli/src/utils/transactionHandler.test.ts new file mode 100644 index 00000000..43805afc --- /dev/null +++ b/bridger-cli/src/utils/transactionHandler.test.ts @@ -0,0 +1,275 @@ +import { TransactionHandler } from "./transactionHandler"; + +describe("TransactionHandler Tests", () => { + let chainId: number; + let epoch: number; + let claim: any; + let veaOutbox: any; + let mockGetBridgeConfig: any; + beforeEach(() => { + chainId = 1; + epoch = 10; + claim = { + stateRoot: "0x1234", + claimer: "0x1234", + timestampClaimed: 1234, + timestampVerification: 0, + blocknumberVerification: 0, + honest: 0, + challenger: "0x1234", + }; + veaOutbox = { + estimateGas: { + claim: jest.fn(), + verifySnapshot: jest.fn(), + startVerification: jest.fn(), + withdrawClaimDeposit: jest.fn(), + }, + claim: jest.fn(), + verifySnapshot: jest.fn(), + startVerification: jest.fn(), + withdrawClaimDeposit: jest.fn(), + provider: { + getTransactionReceipt: jest.fn(), + getBlock: jest.fn(), + }, + }; + mockGetBridgeConfig = jest + .fn() + .mockReturnValue({ deposit: 1000, minChallengePeriod: 1000, sequencerDelayLimit: 1000, epochPeriod: 1000 }); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("constructor", () => { + it("should create a new TransactionHandler without claim", () => { + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox); + expect(transactionHandler).toBeDefined(); + expect(transactionHandler.epoch).toEqual(epoch); + expect(transactionHandler.veaOutbox).toEqual(veaOutbox); + expect(transactionHandler.chainId).toEqual(chainId); + }); + + it("should create a new TransactionHandler with claim", () => { + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); + expect(transactionHandler).toBeDefined(); + expect(transactionHandler.epoch).toEqual(epoch); + expect(transactionHandler.veaOutbox).toEqual(veaOutbox); + expect(transactionHandler.chainId).toEqual(chainId); + expect(transactionHandler.claim).toEqual(claim); + }); + }); + + describe("checkTransactionPendingStatus", () => { + const blockNumber = 10; + const trnxHash = "0x1234"; + let transactionHandler: TransactionHandler; + beforeEach(() => { + transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox); + veaOutbox.provider = { + getTransactionReceipt: jest.fn().mockResolvedValue({ blockNumber }), + getBlock: jest.fn(), + }; + }); + it("should return true if transaction is pending", async () => { + veaOutbox.provider.getBlock.mockReturnValue({ + number: blockNumber + transactionHandler.requiredConfirmations - 1, + }); + const consoleSpy = jest.spyOn(console, "log"); + const result = await transactionHandler.checkTransactionPendingStatus(trnxHash); + expect(result).toBeTruthy(); + expect(consoleSpy).toHaveBeenCalledWith(`Transaction ${trnxHash} is not final yet.`); + }); + + it("should return false if transaction is confirmed", async () => { + veaOutbox.provider.getBlock.mockReturnValue({ + number: blockNumber + transactionHandler.requiredConfirmations, + }); + const consoleSpy = jest.spyOn(console, "log"); + const result = await transactionHandler.checkTransactionPendingStatus(trnxHash); + expect(result).toBeFalsy(); + expect(consoleSpy).toHaveBeenCalledWith( + `Transaction ${trnxHash} is final with ${transactionHandler.requiredConfirmations} confirmations` + ); + }); + it("should return true if transaction receipt is not found", async () => { + veaOutbox.provider.getTransactionReceipt.mockResolvedValue(null); + const consoleSpy = jest.spyOn(console, "log"); + const result = await transactionHandler.checkTransactionPendingStatus(trnxHash); + expect(result).toBeTruthy(); + expect(consoleSpy).toHaveBeenCalledWith(`Transaction ${trnxHash} is pending`); + }); + + it("should return false if transaction is null", async () => { + const consoleSpy = jest.spyOn(console, "log"); + const result = await transactionHandler.checkTransactionPendingStatus(null); + expect(result).toBeFalsy(); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + }); + + describe("makeClaim", () => { + beforeEach(() => { + veaOutbox.estimateGas.claim.mockResolvedValue(1000); + 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.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); + await transactionHandler.makeClaim(claim.stateRoot); + + expect(veaOutbox.estimateGas.claim).toHaveBeenCalledWith(epoch, claim.stateRoot, { value: 1000 }); + expect(veaOutbox.claim).toHaveBeenCalledWith(epoch, claim.stateRoot, { value: 1000, gasLimit: 1000 }); + expect(transactionHandler.pendingTransactions.claim).toEqual("0x1234"); + }); + + it("should not make a claim if a claim transaction is pending", async () => { + // Mock checkTransactionPendingStatus to always return true + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, mockGetBridgeConfig); + await transactionHandler.makeClaim(claim.stateRoot); + + expect(veaOutbox.estimateGas.claim).not.toHaveBeenCalled(); + expect(veaOutbox.claim).not.toHaveBeenCalled(); + expect(transactionHandler.pendingTransactions.claim).toBeNull(); + }); + }); + + describe("startVerification", () => { + let startVerifyTimeFlip: number; + beforeEach(() => { + veaOutbox.estimateGas.startVerification.mockResolvedValue(1000); + veaOutbox.startVerification.mockResolvedValue({ hash: "0x1234" }); + startVerifyTimeFlip = + claim.timestampClaimed + + mockGetBridgeConfig(chainId).epochPeriod + + mockGetBridgeConfig(chainId).sequencerDelayLimit; + }); + it("should start verification and set pending startVerification trnx", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.startVerification(startVerifyTimeFlip); + + expect(veaOutbox.estimateGas.startVerification).toHaveBeenCalledWith(epoch, claim); + expect(veaOutbox.startVerification).toHaveBeenCalledWith(epoch, claim, { gasLimit: 1000 }); + expect(transactionHandler.pendingTransactions.startVerification).toEqual("0x1234"); + }); + + it("should not start verification if timeout has not passed", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.startVerification(startVerifyTimeFlip - 1); + + expect(veaOutbox.estimateGas.startVerification).not.toHaveBeenCalled(); + expect(veaOutbox.startVerification).not.toHaveBeenCalled(); + expect(transactionHandler.pendingTransactions.startVerification).toBeNull(); + }); + + it("should not start verification if claim is not set", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); + + await expect(transactionHandler.startVerification(startVerifyTimeFlip)).rejects.toThrow("Claim is not set"); + }); + + it("should not start verification if a startVerification transaction is pending", async () => { + // Mock checkTransactionPendingStatus to always return true + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + await transactionHandler.startVerification(startVerifyTimeFlip); + expect(veaOutbox.estimateGas.startVerification).not.toHaveBeenCalled(); + expect(veaOutbox.startVerification).not.toHaveBeenCalled(); + expect(transactionHandler.pendingTransactions.startVerification).toBeNull(); + }); + }); + + describe("verifySnapshot", () => { + let verificationFlipTime: number; + beforeEach(() => { + veaOutbox.estimateGas.verifySnapshot.mockResolvedValue(1000); + veaOutbox.verifySnapshot.mockResolvedValue({ hash: "0x1234" }); + verificationFlipTime = claim.timestampClaimed + mockGetBridgeConfig(chainId).minChallengePeriod; + }); + + it("should verify snapshot and set pending verifySnapshot trnx", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.verifySnapshot(verificationFlipTime); + + expect(veaOutbox.estimateGas.verifySnapshot).toHaveBeenCalledWith(epoch, claim); + expect(veaOutbox.verifySnapshot).toHaveBeenCalledWith(epoch, claim, { gasLimit: 1000 }); + expect(transactionHandler.pendingTransactions.verifySnapshot).toEqual("0x1234"); + }); + + it("should not verify snapshot if timeout has not passed", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.verifySnapshot(verificationFlipTime - 1); + + expect(veaOutbox.estimateGas.verifySnapshot).not.toHaveBeenCalled(); + expect(veaOutbox.verifySnapshot).not.toHaveBeenCalled(); + expect(transactionHandler.pendingTransactions.verifySnapshot).toBeNull(); + }); + + it("should not verify snapshot if claim is not set", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); + + await expect(transactionHandler.verifySnapshot(verificationFlipTime)).rejects.toThrow("Claim is not set"); + }); + + it("should not verify snapshot if a verifySnapshot transaction is pending", async () => { + // Mock checkTransactionPendingStatus to always return true + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.verifySnapshot(verificationFlipTime); + + expect(veaOutbox.estimateGas.verifySnapshot).not.toHaveBeenCalled(); + expect(veaOutbox.verifySnapshot).not.toHaveBeenCalled(); + expect(transactionHandler.pendingTransactions.verifySnapshot).toBeNull(); + }); + }); + + describe("withdrawClaimDeposit", () => { + beforeEach(() => { + veaOutbox.estimateGas.withdrawClaimDeposit.mockResolvedValue(1000); + veaOutbox.withdrawClaimDeposit.mockResolvedValue({ hash: "0x1234" }); + }); + it("should withdraw deposit and set pending withdrawClaimDeposit trnx", async () => { + // Mock checkTransactionPendingStatus to always return false + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.withdrawClaimDeposit(); + expect(veaOutbox.estimateGas.withdrawClaimDeposit).toHaveBeenCalledWith(epoch, claim); + expect(veaOutbox.withdrawClaimDeposit).toHaveBeenCalledWith(epoch, claim, { gasLimit: 1000 }); + expect(transactionHandler.pendingTransactions.withdrawClaimDeposit).toEqual("0x1234"); + }); + + it("should not withdraw deposit if a withdrawClaimDeposit transaction is pending", async () => { + // Mock checkTransactionPendingStatus to always return true + jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + + await transactionHandler.withdrawClaimDeposit(); + expect(veaOutbox.estimateGas.withdrawClaimDeposit).not.toHaveBeenCalled(); + expect(veaOutbox.withdrawClaimDeposit).not.toHaveBeenCalled(); + expect(transactionHandler.pendingTransactions.withdrawClaimDeposit).toBeNull(); + }); + }); +}); diff --git a/bridger-cli/src/utils/transactionHandler.ts b/bridger-cli/src/utils/transactionHandler.ts new file mode 100644 index 00000000..aff88cbd --- /dev/null +++ b/bridger-cli/src/utils/transactionHandler.ts @@ -0,0 +1,145 @@ +import { getBridgeConfig } from "../consts/bridgeRoutes"; +import { ClaimStruct } from "./claim"; + +interface PendingTransactions { + claim: string | null; + verifySnapshot: string | null; + withdrawClaimDeposit: string | null; + startVerification: string | null; +} + +export class TransactionHandler { + public epoch: number; + public veaOutbox: any; + public chainId: number; + public claim: ClaimStruct | null; + public getBridgeConfig: typeof getBridgeConfig; + public requiredConfirmations: number = 12; + + public pendingTransactions: PendingTransactions = { + claim: null, + verifySnapshot: null, + withdrawClaimDeposit: null, + startVerification: null, + }; + + constructor( + chainId: number, + epoch: number, + veaOutbox, + claim?: ClaimStruct, + fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig + ) { + this.epoch = epoch; + this.veaOutbox = veaOutbox; + this.chainId = chainId; + this.claim = claim; + this.getBridgeConfig = fetchBridgeConfig; + } + + public async checkTransactionPendingStatus(trnxHash: string | null): Promise { + if (trnxHash == null) { + return false; + } + + const receipt = await this.veaOutbox.provider.getTransactionReceipt(trnxHash); + + if (!receipt) { + console.log(`Transaction ${trnxHash} is pending`); + return true; + } + + const currentBlock = await this.veaOutbox.provider.getBlock("latest"); + const confirmations = currentBlock.number - receipt.blockNumber; + + if (confirmations >= this.requiredConfirmations) { + console.log(`Transaction ${trnxHash} is final with ${confirmations} confirmations`); + return false; + } else { + console.log(`Transaction ${trnxHash} is not final yet.`); + return true; + } + } + + public async makeClaim(stateRoot: string) { + if (await this.checkTransactionPendingStatus(this.pendingTransactions.claim)) { + console.log("Claim transaction is still pending with hash: " + this.pendingTransactions.claim); + return; + } + const bridgeConfig = this.getBridgeConfig(this.chainId); + const estimateGas = await this.veaOutbox.estimateGas.claim(this.epoch, stateRoot, { value: bridgeConfig.deposit }); + const claimTransaction = await this.veaOutbox.claim(this.epoch, stateRoot, { + value: bridgeConfig.deposit, + gasLimit: estimateGas, + }); + console.log(`Epoch ${this.epoch} was claimed with trnx hash ${claimTransaction.hash}`); + this.pendingTransactions.claim = claimTransaction.hash; + } + + public async startVerification(latestBlockTimestamp: number) { + if (this.claim == null) { + throw new Error("Claim is not set"); + } + if (await this.checkTransactionPendingStatus(this.pendingTransactions.startVerification)) { + console.log( + "Start verification transaction is still pending with hash: " + this.pendingTransactions.startVerification + ); + return; + } + const bridgeConfig = this.getBridgeConfig(this.chainId); + const timeOver = + latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.sequencerDelayLimit - bridgeConfig.epochPeriod; + console.log(timeOver); + if (timeOver >= 0) { + const estimateGas = await this.veaOutbox.estimateGas.startVerification(this.epoch, this.claim); + const startVerifTrx = await this.veaOutbox.startVerification(this.epoch, this.claim, { gasLimit: estimateGas }); + console.log(`Verification started for epoch ${this.epoch} with trx hash ${startVerifTrx.hash}`); + this.pendingTransactions.startVerification = startVerifTrx.hash; + } else { + console.log("Sequencer delay not passed yet, seconds left: " + -1 * timeOver); + } + } + + public async verifySnapshot(latestBlockTimestamp: number) { + if (this.claim == null) { + throw new Error("Claim is not set"); + } + if (await this.checkTransactionPendingStatus(this.pendingTransactions.verifySnapshot)) { + console.log("Verify snapshot transaction is still pending with hash: " + this.pendingTransactions.verifySnapshot); + return; + } + const bridgeConfig = this.getBridgeConfig(this.chainId); + + const timeLeft = latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.minChallengePeriod; + console.log("Time left for verification: " + timeLeft); + console.log(latestBlockTimestamp, this.claim.timestampClaimed, bridgeConfig.minChallengePeriod); + // Claim not resolved yet, check if we can verifySnapshot + if (timeLeft >= 0) { + console.log("Verification period passed, verifying snapshot"); + // Estimate gas for verifySnapshot + const estimateGas = await this.veaOutbox.estimateGas.verifySnapshot(this.epoch, this.claim); + const claimTransaction = await this.veaOutbox.verifySnapshot(this.epoch, this.claim, { + gasLimit: estimateGas, + }); + console.log(`Epoch ${this.epoch} verification started with trnx hash ${claimTransaction.hash}`); + this.pendingTransactions.verifySnapshot = claimTransaction.hash; + } else { + console.log("Censorship test in progress, sec left: " + -1 * timeLeft); + } + } + + public async withdrawClaimDeposit() { + if (await this.checkTransactionPendingStatus(this.pendingTransactions.withdrawClaimDeposit)) { + console.log( + "Withdraw deposit transaction is still pending with hash: " + this.pendingTransactions.withdrawClaimDeposit + ); + return; + } + const estimateGas = await this.veaOutbox.estimateGas.withdrawClaimDeposit(this.epoch, this.claim); + const claimTransaction = await this.veaOutbox.withdrawClaimDeposit(this.epoch, this.claim, { + gasLimit: estimateGas, + }); + console.log(`Deposit withdrawn with trnx hash ${claimTransaction.hash}`); + this.pendingTransactions.withdrawClaimDeposit = claimTransaction.hash; + } +} diff --git a/contracts/Dockerfile b/contracts/Dockerfile index 239e7ad7..8a34d780 100644 --- a/contracts/Dockerfile +++ b/contracts/Dockerfile @@ -8,6 +8,9 @@ RUN yarn install COPY . . +ENV INFURA_ID="" +RUN test -n "$INFURA_ID" || (echo "INFURA_ID must be set" && false) + COPY start-forks.sh /start-forks.sh RUN chmod +x /start-forks.sh From 382681da2c1c6c5a4f0fa29e54161d7bd85a5769 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Mon, 9 Dec 2024 17:29:41 +0700 Subject: [PATCH 08/15] fix: codeRabbit fixes --- bridger-cli/.env.dist | 9 ++++++--- bridger-cli/src/bridger.ts | 2 +- bridger-cli/src/utils/claim.test.ts | 19 +++++++++---------- bridger-cli/src/utils/claim.ts | 2 -- bridger-cli/src/utils/graphQueries.ts | 8 +++----- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/bridger-cli/.env.dist b/bridger-cli/.env.dist index 73657088..0d08f874 100644 --- a/bridger-cli/.env.dist +++ b/bridger-cli/.env.dist @@ -1,12 +1,15 @@ PRIVATE_KEY= + + VEAOUTBOX_CHAIN_ID=11155111 +# VeaInbox Address and Rpc Provider VEAINBOX_ADDRESS=0xE12daFE59Bc3A996362d54b37DFd2BA9279cAd06 VEAINBOX_PROVIDER=http://localhost:8545 +# VeaOutbox Address and Rpc Provider VEAOUTBOX_ADDRESS=0x209BFdC6B7c66b63A8382196Ba3d06619d0F12c9 VEAOUTBOX_PROVIDER=http://localhost:8546 -# Ex: 85918/outbox-arb-sep-sep-testnet-vs/version/latest -VEAINBOX_SUBGRAPH= -VEAOUTBOX_SUBGRAPH= +# Ex: https://api.studio.thegraph.com/query/85918/outbox-arb-sep-sep-testnet-vs/version/latest +VEAOUTBOX_SUBGRAPH=http://localhost:8000/subgraphs/name/kleros/veascan-outbox/graphql diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts index 1e515d37..8283042d 100644 --- a/bridger-cli/src/bridger.ts +++ b/bridger-cli/src/bridger.ts @@ -62,7 +62,7 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( } } else if (claimableEpochHash != ethers.constants.HashZero) { console.log("Claim is already made, checking for verification stage"); - const claim = await fetchClaim(veaOutbox, activeEpoch, chainId); + const claim = await fetchClaim(veaOutbox, activeEpoch); console.log(claim); if (!transactionHandlers[activeEpoch]) { transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox, claim); diff --git a/bridger-cli/src/utils/claim.test.ts b/bridger-cli/src/utils/claim.test.ts index 9788018a..da5f5685 100644 --- a/bridger-cli/src/utils/claim.test.ts +++ b/bridger-cli/src/utils/claim.test.ts @@ -8,7 +8,6 @@ describe("snapshotClaim", () => { let getClaimForEpoch: jest.Mock; let veaOutbox: any; const epoch = 1; - const chainId = 1; beforeEach(() => { mockClaim = { @@ -46,12 +45,12 @@ describe("snapshotClaim", () => { veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.constants.AddressZero } }]) ); - const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); console.log(claim); console.log(mockClaim); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); - expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); }); it("should return a valid claim with challenger", async () => { @@ -70,12 +69,12 @@ describe("snapshotClaim", () => { challenged: true, }); - const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); console.log(claim); console.log(mockClaim); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); - expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); }); it("should return a valid claim with verification", async () => { @@ -98,11 +97,11 @@ describe("snapshotClaim", () => { number: mockClaim.blocknumberVerification, }); - const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); - expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); }); it("should fallback on logs if claimData is undefined", async () => { @@ -119,12 +118,12 @@ describe("snapshotClaim", () => { veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); - const claim = await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); console.log(claim); console.log(mockClaim); expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); - expect(getClaimForEpoch).toHaveBeenCalledWith(chainId, epoch); + expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); }); it("should throw an error if no claim is found", async () => { @@ -134,7 +133,7 @@ describe("snapshotClaim", () => { veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); await expect(async () => { - await fetchClaim(veaOutbox, epoch, chainId, getClaimForEpoch); + await fetchClaim(veaOutbox, epoch, getClaimForEpoch); }).rejects.toThrow(`No claim found for epoch ${epoch}`); }); }); diff --git a/bridger-cli/src/utils/claim.ts b/bridger-cli/src/utils/claim.ts index a083136c..c6a6784a 100644 --- a/bridger-cli/src/utils/claim.ts +++ b/bridger-cli/src/utils/claim.ts @@ -1,6 +1,5 @@ import { ClaimData, getClaimForEpoch } from "./graphQueries"; import { ethers } from "ethers"; -import { getBlockNumberFromEpoch } from "./epochHandler"; type ClaimStruct = { stateRoot: string; @@ -15,7 +14,6 @@ type ClaimStruct = { const fetchClaim = async ( veaOutbox, epoch, - chainId, fetchClaimForEpoch: typeof getClaimForEpoch = getClaimForEpoch ): Promise => { let claimData: ClaimData | undefined = await fetchClaimForEpoch(epoch); diff --git a/bridger-cli/src/utils/graphQueries.ts b/bridger-cli/src/utils/graphQueries.ts index 70b59713..2fb1751a 100644 --- a/bridger-cli/src/utils/graphQueries.ts +++ b/bridger-cli/src/utils/graphQueries.ts @@ -40,14 +40,12 @@ const getLastClaimedEpoch = async (): Promise => { `${subgraph}`, `{ claims(first:1, orderBy:timestamp, orderDirection:desc){ - id + id bridger stateroot timestamp - verification{ - timestamp - } - challenged + challenged + txHash } }` From 1f0ab5f6944306042d3702bdeae49d16d239a99e Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Tue, 10 Dec 2024 12:48:13 +0700 Subject: [PATCH 09/15] chore: consts refactor --- bridger-cli/src/consts/bridgeRoutes.ts | 3 + bridger-cli/src/utils/claim.test.ts | 10 ++-- bridger-cli/src/utils/ethers.ts | 78 ++++++++------------------ 3 files changed, 30 insertions(+), 61 deletions(-) diff --git a/bridger-cli/src/consts/bridgeRoutes.ts b/bridger-cli/src/consts/bridgeRoutes.ts index fa90c0e4..79a626a5 100644 --- a/bridger-cli/src/consts/bridgeRoutes.ts +++ b/bridger-cli/src/consts/bridgeRoutes.ts @@ -1,6 +1,7 @@ import { BigNumber } from "ethers"; interface IBridge { + chain: string; epochPeriod: number; deposit: BigNumber; minChallengePeriod: number; @@ -9,12 +10,14 @@ interface IBridge { const bridges: { [chainId: number]: IBridge } = { 11155111: { + chain: "sepolia", epochPeriod: 7200, deposit: BigNumber.from("1000000000000000000"), minChallengePeriod: 10800, sequencerDelayLimit: 86400, }, 10200: { + chain: "chiado", epochPeriod: 3600, deposit: BigNumber.from("1000000000000000000"), minChallengePeriod: 10800, diff --git a/bridger-cli/src/utils/claim.test.ts b/bridger-cli/src/utils/claim.test.ts index da5f5685..4e6e6ebc 100644 --- a/bridger-cli/src/utils/claim.test.ts +++ b/bridger-cli/src/utils/claim.test.ts @@ -45,9 +45,9 @@ describe("snapshotClaim", () => { veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.constants.AddressZero } }]) ); + const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); - console.log(claim); - console.log(mockClaim); + expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); @@ -70,8 +70,7 @@ describe("snapshotClaim", () => { }); const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); - console.log(claim); - console.log(mockClaim); + expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); @@ -119,8 +118,7 @@ describe("snapshotClaim", () => { veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); - console.log(claim); - console.log(mockClaim); + expect(claim).toBeDefined(); expect(claim).toEqual(mockClaim); expect(getClaimForEpoch).toHaveBeenCalledWith(epoch); diff --git a/bridger-cli/src/utils/ethers.ts b/bridger-cli/src/utils/ethers.ts index c2e4e291..e7006ee5 100644 --- a/bridger-cli/src/utils/ethers.ts +++ b/bridger-cli/src/utils/ethers.ts @@ -8,6 +8,7 @@ import { VeaInboxArbToGnosis__factory, VeaOutboxArbToGnosis__factory, } from "@kleros/vea-contracts/typechain-types"; +import { getBridgeConfig } from "consts/bridgeRoutes"; function getWallet(privateKey: string, web3ProviderURL: string) { return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL)); @@ -19,55 +20,30 @@ function getWalletRPC(privateKey: string, rpc: JsonRpcProvider) { // Using destination chainId as identifier, Ex: Arbitrum One (42161) -> Ethereum Mainnet (1): Use "1" as chainId function getVeaInbox(veaInboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { - if (chainId == 11155111) { - return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); - } else if (chainId == 10200) { - return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); - } else { - throw new Error(`Unsupported chainId: ${chainId}`); - } -} - -function getVeaInboxProvider(veaInboxAddress: string, privateKey: string, rpc: JsonRpcProvider, chainId: number) { - if (chainId == 11155111) { - return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWalletRPC(privateKey, rpc)); - } else if (chainId == 10200) { - return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWalletRPC(privateKey, rpc)); + const bridge = getBridgeConfig(chainId); + switch (bridge.chain) { + case "sepolia": + case "mainnet": + return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + case "chiado": + case "gnosis": + return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); + default: + throw new Error(`Unsupported chainId: ${chainId}`); } } -function getVeaOutbox(veaInboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { - if (chainId == 11155111) { - return VeaOutboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); - } else if (chainId == 10200) { - return VeaOutboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); - } else { - throw new Error(`Unsupported chainId: ${chainId}`); - } -} - -function getVeaOutboxProvider(veaOutboxAddress: string, privateKey: string, rpc: JsonRpcProvider, chainId: number) { - if (chainId == 11155111) { - return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); - } else if (chainId == 10200) { - return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); - } else { - throw new Error(`Unsupported chainId: ${chainId}`); - } -} - -function getVeaOutboxDevnetProvider( - veaOutboxAddress: string, - privateKey: string, - rpc: JsonRpcProvider, - chainId: number -) { - if (chainId == 11155111) { - return VeaOutboxArbToEthDevnet__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); - } else if (chainId == 10200) { - return VeaOutboxArbToGnosisDevnet__factory.connect(veaOutboxAddress, getWalletRPC(privateKey, rpc)); - } else { - throw new Error(`Unsupported chainId: ${chainId}`); +function getVeaOutbox(veaOutboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { + const bridge = getBridgeConfig(chainId); + switch (bridge.chain) { + case "sepolia": + case "mainnet": + return VeaOutboxArbToEth__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + case "chiado": + case "gnosis": + return VeaOutboxArbToGnosis__factory.connect(veaOutboxAddress, getWallet(privateKey, web3ProviderURL)); + default: + throw new Error(`Unsupported chainId: ${chainId}`); } } @@ -81,12 +57,4 @@ function getVeaOutboxDevnet(veaOutboxAddress: string, privateKey: string, web3Pr } } -export { - getWalletRPC, - getVeaOutboxDevnetProvider, - getVeaOutbox, - getVeaInbox, - getVeaOutboxProvider, - getVeaInboxProvider, - getVeaOutboxDevnet, -}; +export { getWalletRPC, getVeaOutbox, getVeaInbox, getVeaOutboxDevnet }; From 97d67772fa1ec4e9e8d0012bbaec49c218c2cb93 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Tue, 10 Dec 2024 15:16:03 +0700 Subject: [PATCH 10/15] chore: dev comments --- bridger-cli/src/bridger.test.ts | 18 ++++----- bridger-cli/src/utils/claim.ts | 26 +++++++++++- bridger-cli/src/utils/epochHandler.ts | 45 +++++++++++++++++++-- bridger-cli/src/utils/transactionHandler.ts | 16 +++++++- 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/bridger-cli/src/bridger.test.ts b/bridger-cli/src/bridger.test.ts index cb5aa214..72701462 100644 --- a/bridger-cli/src/bridger.test.ts +++ b/bridger-cli/src/bridger.test.ts @@ -301,19 +301,15 @@ describe("bridger", function () { await veaOutbox.verifySnapshot(claimEpoch, claim); - const balancePreWithdraw = Number(await outboxProvider.getBalance(claimTxn.from)); - const contractBalancePreWithdraw = Number(await outboxProvider.getBalance(veaOutbox.address)); + const balancePreWithdraw = await outboxProvider.getBalance(claimTxn.from); + const contractBalancePreWithdraw = await outboxProvider.getBalance(veaOutbox.address); await startBridgerWithTimeout(5000, claimEpoch); - const balancePostWithdraw = Number(await outboxProvider.getBalance(claimTxn.from)); - const contractBalancePostWithdraw = Number(await outboxProvider.getBalance(veaOutbox.address)); - // Check if deposit was withdrawn - assert.isAbove(balancePostWithdraw, balancePreWithdraw, "Deposit was not withdrawn"); - assert.equal( - contractBalancePostWithdraw, - contractBalancePreWithdraw - Number(deposit), - "Deposit was not withdrawn" - ); + const balancePostWithdraw = await outboxProvider.getBalance(claimTxn.from); + const contractBalancePostWithdraw = await outboxProvider.getBalance(veaOutbox.address); + + assert(balancePostWithdraw.gt(balancePreWithdraw), "Deposit was not withdrawn"); + assert(contractBalancePostWithdraw.eq(contractBalancePreWithdraw.sub(deposit)), "Deposit was not withdrawn"); }); it.todo("should not withdraw deposit when claimer is dishonest"); diff --git a/bridger-cli/src/utils/claim.ts b/bridger-cli/src/utils/claim.ts index c6a6784a..a4492e2e 100644 --- a/bridger-cli/src/utils/claim.ts +++ b/bridger-cli/src/utils/claim.ts @@ -11,9 +11,21 @@ type ClaimStruct = { challenger: `0x${string}`; }; +/** + * Fetches the claim data for a given epoch. + * + * @param veaOutbox - The VeaOutbox contract instance + * @param epoch - The epoch number for which the claim is needed + * + * @returns The claim data for the given epoch + * + * @example + * const claim = await fetchClaim(veaOutbox, 240752); + */ + const fetchClaim = async ( - veaOutbox, - epoch, + veaOutbox: any, + epoch: number, fetchClaimForEpoch: typeof getClaimForEpoch = getClaimForEpoch ): Promise => { let claimData: ClaimData | undefined = await fetchClaimForEpoch(epoch); @@ -53,6 +65,16 @@ const fetchClaim = async ( return claim; }; +/** + * Hashes the claim data. + * + * @param claim - The claim data to be hashed + * + * @returns The hash of the claim data + * + * @example + * const claimHash = hashClaim(claim); + */ const hashClaim = (claim) => { return ethers.utils.solidityKeccak256( ["bytes32", "address", "uint32", "uint32", "uint32", "uint8", "address"], diff --git a/bridger-cli/src/utils/epochHandler.ts b/bridger-cli/src/utils/epochHandler.ts index 7c3b442b..60301c8a 100644 --- a/bridger-cli/src/utils/epochHandler.ts +++ b/bridger-cli/src/utils/epochHandler.ts @@ -1,4 +1,16 @@ -const setEpochRange = async (veaOutbox, startEpoch): Promise> => { +import { JsonRpcProvider } from "@ethersproject/providers"; + +/** + * Sets the range of epochs from the start epoch to the current epoch. + * + * @param veaOutbox - The VeaOutbox instance to get the current epoch + * @param startEpoch - The starting epoch number + * @returns An array of epoch numbers from startEpoch to currentEpoch + * + * @example + * const epochs = await setEpochRange(veaOutbox, 0); + */ +const setEpochRange = async (veaOutbox: any, startEpoch: number): Promise> => { const defaultEpochRollback = 10; // When no start epoch is provided, we will start from current epoch - defaultEpochRollback const currentEpoch = Number(await veaOutbox.epochNow()); if (currentEpoch < startEpoch) { @@ -11,7 +23,23 @@ const setEpochRange = async (veaOutbox, startEpoch): Promise> => { return epochs; }; -const getBlockNumberFromEpoch = async (veaOutboxRpc, epoch, epochPeriod): Promise => { +/** + * Gets the block number for a given epoch. + * + * @param veaOutboxRpc - The VeaOutbox RPC instance to get the block number + * @param epoch - The epoch number for which the block number is needed + * @param epochPeriod - The epoch period in seconds + * + * @returns The block number for the given epoch + * + * @example + * const blockNumber = await getBlockNumberFromEpoch(veaOutboxRpc, 240752, 7200); + */ +const getBlockNumberFromEpoch = async ( + veaOutboxRpc: JsonRpcProvider, + epoch: number, + epochPeriod: number +): Promise => { const latestBlock = await veaOutboxRpc.getBlock("latest"); const preBlock = await veaOutboxRpc.getBlock(latestBlock.number - 1000); const avgBlockTime = (latestBlock.timestamp - preBlock.timestamp) / 1000; @@ -21,7 +49,18 @@ const getBlockNumberFromEpoch = async (veaOutboxRpc, epoch, epochPeriod): Promis return epochBlock - 100; }; -const checkForNewEpoch = (currentEpoch, epochPeriod): number => { +/** + * Checks if a new epoch has started. + * + * @param currentEpoch - The current epoch number + * @param epochPeriod - The epoch period in seconds + * + * @returns The updated epoch number + * + * @example + * currentEpoch = checkForNewEpoch(currentEpoch, 7200); + */ +const checkForNewEpoch = (currentEpoch: number, epochPeriod: number): number => { if (Math.floor(Date.now() / (1000 * epochPeriod)) - 1 > currentEpoch) { currentEpoch = Math.floor(Date.now() / 1000 / epochPeriod) - 1; } diff --git a/bridger-cli/src/utils/transactionHandler.ts b/bridger-cli/src/utils/transactionHandler.ts index aff88cbd..5047e95f 100644 --- a/bridger-cli/src/utils/transactionHandler.ts +++ b/bridger-cli/src/utils/transactionHandler.ts @@ -8,6 +8,20 @@ interface PendingTransactions { startVerification: string | null; } +/** + * Handles transactions for a given veaOutbox and epoch. + * + * @param chainId - The chainId of veaOutbox chain + * @param epoch - The epoch number for which the transactions are being handled + * @param veaOutbox - The veaOutbox instance to use for sending transactions + * @param claim - The claim object for the epoch + * @returns An instance of the TransactionHandler class + * + * @example + * const txHandler = new TransactionHandler(11155111, 240752, veaOutbox, claim); + * txHandler.sendTransaction(txData); + */ + export class TransactionHandler { public epoch: number; public veaOutbox: any; @@ -26,7 +40,7 @@ export class TransactionHandler { constructor( chainId: number, epoch: number, - veaOutbox, + veaOutbox: any, claim?: ClaimStruct, fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig ) { From 64427bbbaa7d4688bd2b314c2ce884013532f63c Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Wed, 11 Dec 2024 16:43:34 +0700 Subject: [PATCH 11/15] chore: refactored docker config --- contracts/.env.example | 12 ++++++++ contracts/Dockerfile | 15 +++++----- contracts/docker-compose.yml | 58 +++++++++++++++++++++++++++++------- contracts/start-forks.sh | 17 ++++++----- 4 files changed, 75 insertions(+), 27 deletions(-) diff --git a/contracts/.env.example b/contracts/.env.example index 2fdfa0eb..81b1a8fa 100644 --- a/contracts/.env.example +++ b/contracts/.env.example @@ -8,6 +8,18 @@ ETHERSCAN_API_KEY_FIX=ABC123ABC123ABC123ABC123ABC123ABC1 ARBISCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 GNOSISSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1 + +## For testing +# Names should correspond to the infura network names +INBOX_NETWORK=arbitrum-sepolia +OUTBOX_NETWORK=sepolia +# Empty for direct routes +ROUTER_NETWORK= +# RPC port of local fork for the Graph node: +# 8545 for inboxFork, 8546 for outboxFork, 8547 for routerFork +RPC_GRAPH=8456 + + # Optionally for debugging # TENDERLY_USERNAME=your_username # TENDERLY_PROJECT=your_project diff --git a/contracts/Dockerfile b/contracts/Dockerfile index 8a34d780..2864d670 100644 --- a/contracts/Dockerfile +++ b/contracts/Dockerfile @@ -1,20 +1,19 @@ FROM node:20.12.1-alpine -WORKDIR / +WORKDIR /home/node COPY package.json ./ -RUN yarn install +RUN yarn install --ignore-scripts COPY . . -ENV INFURA_ID="" -RUN test -n "$INFURA_ID" || (echo "INFURA_ID must be set" && false) +ENV INFURA_API_KEY="" -COPY start-forks.sh /start-forks.sh +RUN chmod +x /home/node/start-forks.sh -RUN chmod +x /start-forks.sh +USER node -EXPOSE 8545 8546 +EXPOSE 8545 8546 8547 -CMD ["/bin/sh", "/start-forks.sh"] +CMD ["/bin/sh", "/home/node/start-forks.sh"] diff --git a/contracts/docker-compose.yml b/contracts/docker-compose.yml index a75902bc..81725239 100644 --- a/contracts/docker-compose.yml +++ b/contracts/docker-compose.yml @@ -41,27 +41,61 @@ services: networks: - graph-network - hardhat: + hardhat-inbox-fork: build: context: ./ dockerfile: Dockerfile - container_name: hardhat + container_name: inbox_fork environment: - - NETWORK1=arbitrum-sepolia - - PORT1=8545 - - NETWORK2=sepolia - - PORT2=8546 - - INFURA_ID=${INFURA_API_KEY} - command: ["/bin/sh", "/start-forks.sh"] + - NETWORK=${INBOX_NETWORK} + - PORT=8545 + - INFURA_API_KEY=${INFURA_API_KEY} + command: ["/bin/sh", "/home/node/start-forks.sh"] ports: - "8545:8545" - - "8546:8546" depends_on: - postgres - ipfs networks: - graph-network + hardhat-outbox-fork: + build: + context: ./ + dockerfile: Dockerfile + container_name: outbox_fork + environment: + - NETWORK=${OUTBOX_NETWORK} + - PORT=8546 + - INFURA_API_KEY=${INFURA_API_KEY} + command: ["/bin/sh", "/home/node/start-forks.sh"] + ports: + - "8546:8546" + depends_on: + - postgres + - ipfs + networks: + - graph-network + + hardhat-router-fork: + build: + context: ./ + dockerfile: Dockerfile + container_name: router_fork + environment: + - NETWORK=${ROUTER_NETWORK} + - PORT=8547 + - INFURA_API_KEY=${INFURA_API_KEY} + command: ["/bin/sh", "/home/node/start-forks.sh"] + ports: + - "8547:8547" + depends_on: + - postgres + - ipfs + networks: + - graph-network + + graph-node: image: graphprotocol/graph-node:latest container_name: graph_node @@ -72,7 +106,7 @@ services: postgres_pass: mysecretpassword postgres_db: graphnode_db ipfs: ipfs_host:5001 - ethereum: mainnet:http://host.docker.internal:8546/ + ethereum: mainnet:http://host.docker.internal:${RPC_GRAPH}/ ETHEREUM_REORG_THRESHOLD: 1 ETHEREUM_ANCESTOR_COUNT: 10 GRAPH_LOG: debug @@ -82,7 +116,9 @@ services: depends_on: - postgres - ipfs - - hardhat + - hardhat-inbox-fork + - hardhat-outbox-fork + - hardhat-router-fork networks: - graph-network diff --git a/contracts/start-forks.sh b/contracts/start-forks.sh index daae7d79..fcab359b 100644 --- a/contracts/start-forks.sh +++ b/contracts/start-forks.sh @@ -1,16 +1,17 @@ #!/bin/sh -if [ -z "$INFURA_ID" ]; then - echo "Error: INFURA_ID is not set." +if [ -z "$INFURA_API_KEY" ]; then + echo "Error: INFURA_API_KEY is not set." exit 1 fi -# Set for 8545: Arbitrum Sepolia fork -echo "Starting Hardhat fork for $NETWORK1 on port $PORT1..." -npx hardhat node --fork https://$NETWORK1.infura.io/v3/$INFURA_ID --no-deploy --hostname 0.0.0.0 --port $PORT1 & +if [ -z "$NETWORK" ]; then + echo "Error: NETWORK is not set for $PORT." + exit 1 +fi -# Set for 8546: Sepolia fork -echo "Starting Hardhat fork for $NETWORK2 on port $PORT2..." -npx hardhat node --fork https://$NETWORK2.infura.io/v3/$INFURA_ID --no-deploy --hostname 0.0.0.0 --port $PORT2 & +# Set for 8545: Arbitrum Sepolia fork +echo "Starting Hardhat fork for $NETWORK on port $PORT..." +npx hardhat node --fork https://$NETWORK.infura.io/v3/$INFURA_API_KEY --no-deploy --hostname 0.0.0.0 --port $PORT & wait From 4690526fdd58fc72c2ca4e580581c5bda9e0897f Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Thu, 12 Dec 2024 16:35:09 +0700 Subject: [PATCH 12/15] feat: custom logging and errors --- bridger-cli/src/bridger.test.ts | 20 ++-- bridger-cli/src/bridger.ts | 60 ++++++++---- bridger-cli/src/utils/botEvents.ts | 28 ++++++ bridger-cli/src/utils/claim.test.ts | 4 +- bridger-cli/src/utils/claim.ts | 14 ++- bridger-cli/src/utils/emitter.ts | 14 +++ bridger-cli/src/utils/epochHandler.test.ts | 29 +++--- bridger-cli/src/utils/epochHandler.ts | 21 ++-- bridger-cli/src/utils/errors.ts | 27 +++++ bridger-cli/src/utils/ethers.ts | 2 +- bridger-cli/src/utils/logger.ts | 98 +++++++++++++++++++ bridger-cli/src/utils/shutdown.ts | 3 + .../src/utils/transactionHandler.test.ts | 31 +++--- bridger-cli/src/utils/transactionHandler.ts | 83 ++++++++-------- 14 files changed, 322 insertions(+), 112 deletions(-) create mode 100644 bridger-cli/src/utils/botEvents.ts create mode 100644 bridger-cli/src/utils/emitter.ts create mode 100644 bridger-cli/src/utils/errors.ts create mode 100644 bridger-cli/src/utils/logger.ts diff --git a/bridger-cli/src/bridger.test.ts b/bridger-cli/src/bridger.test.ts index 72701462..f12f4b5d 100644 --- a/bridger-cli/src/bridger.test.ts +++ b/bridger-cli/src/bridger.test.ts @@ -1,15 +1,15 @@ require("dotenv").config(); import { assert } from "chai"; +import { JsonRpcProvider } from "@ethersproject/providers"; +import { BigNumber } from "ethers"; +import { MockEmitter } from "./utils/emitter"; import { hashClaim } from "./utils/claim"; import { getVeaOutbox, getVeaInbox } from "./utils/ethers"; import { watch } from "./bridger"; import { ShutdownSignal } from "./utils/shutdown"; -import { JsonRpcProvider } from "@ethersproject/providers"; -import { BigNumber } from "ethers"; jest.setTimeout(15000); describe("bridger", function () { - console.log = function () {}; const ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000"; const FAKE_HASH = "0x0000000000000000000000000000000000000000000000000000000000000001"; const mockMessage = { @@ -17,8 +17,8 @@ describe("bridger", function () { to: "0x1234567890abcdef1234567890abcdef12345678", fnSelector: "0x12345678", }; - const inboxProvider = new JsonRpcProvider("http://localhost:8545"); - const outboxProvider = new JsonRpcProvider("http://localhost:8546"); + const inboxProvider = new JsonRpcProvider(process.env.VEAINBOX_PROVIDER); + const outboxProvider = new JsonRpcProvider(process.env.VEAOUTBOX_PROVIDER); let claimEpoch: number; let epochPeriod: number; @@ -28,13 +28,13 @@ describe("bridger", function () { const veaInbox = getVeaInbox( process.env.VEAINBOX_ADDRESS, process.env.PRIVATE_KEY, - "http://localhost:8545", + process.env.VEAINBOX_PROVIDER, Number(process.env.VEAOUTBOX_CHAIN_ID) ); const veaOutbox = getVeaOutbox( process.env.VEAOUTBOX_ADDRESS, process.env.PRIVATE_KEY, - "http://localhost:8546", + process.env.VEAOUTBOX_PROVIDER, Number(process.env.VEAOUTBOX_CHAIN_ID) ); @@ -49,7 +49,8 @@ describe("bridger", function () { // Start bridger with a timeout async function startBridgerWithTimeout(timeout: number, startEpoch: number = 0) { const shutDownSignal = new ShutdownSignal(); - const bridgerPromise = watch(shutDownSignal, startEpoch); + const mockEmitter = new MockEmitter(); + const bridgerPromise = watch(shutDownSignal, startEpoch, mockEmitter); const timeoutPromise = new Promise((resolve) => { setTimeout(() => { shutDownSignal.setShutdownSignal(); @@ -93,10 +94,11 @@ describe("bridger", function () { await increaseEpoch(); // Start bridger - await startBridgerWithTimeout(1000, claimEpoch); + await startBridgerWithTimeout(5000, claimEpoch); const toBeClaimedStateRoot = await veaInbox.snapshots(claimEpoch); const claimData = await veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, claimEpoch, null)); + const bridger = `0x${claimData[0].topics[1].slice(26)}`; const claimBlock = await outboxProvider.getBlock(claimData[0].blockNumber); const claim = { diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts index 8283042d..3d7ba65e 100644 --- a/bridger-cli/src/bridger.ts +++ b/bridger-cli/src/bridger.ts @@ -1,14 +1,24 @@ require("dotenv").config(); import { ethers } from "ethers"; +import { EventEmitter } from "events"; import { getLastClaimedEpoch } from "./utils/graphQueries"; import { getVeaInbox, getVeaOutbox } from "./utils/ethers"; import { fetchClaim, hashClaim } from "./utils/claim"; import { TransactionHandler } from "./utils/transactionHandler"; -import { setEpochRange, checkForNewEpoch } from "./utils/epochHandler"; +import { setEpochRange, getLatestVerifiableEpoch } from "./utils/epochHandler"; import { ShutdownSignal } from "./utils/shutdown"; +import { initialize as initializeLogger } from "./utils/logger"; +import { defaultEmitter } from "./utils/emitter"; +import { BotEvents } from "./utils/botEvents"; +import { get } from "http"; -export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal(), startEpoch: number = 0) => { - console.log("Starting bridger"); +export const watch = async ( + shutDownSignal: ShutdownSignal = new ShutdownSignal(), + startEpoch: number = 0, + emitter: EventEmitter = defaultEmitter +) => { + initializeLogger(emitter); + emitter.emit(BotEvents.STARTED); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); const veaInboxAddress = process.env.VEAINBOX_ADDRESS; const veaInboxProviderURL = process.env.VEAINBOX_PROVIDER; @@ -26,7 +36,7 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( let i = 0; while (i < epochs.length) { const activeEpoch = epochs[i]; - console.log("Checking for epoch " + activeEpoch); + emitter.emit(BotEvents.CHECKING, activeEpoch); let claimableEpochHash = await veaOutbox.claimHashes(activeEpoch); let outboxStateRoot = await veaOutbox.stateRoot(); const finalizedOutboxBlock = await veaOutbox.provider.getBlock("finalized"); @@ -42,30 +52,42 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( if (claimData.challenged || claimData.stateroot != savedSnapshot) { // Making claim as either last claim was challenged or there are new messages if (!transactionHandlers[activeEpoch]) { - transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox); + transactionHandlers[activeEpoch] = new TransactionHandler( + chainId, + activeEpoch, + veaOutbox, + null, + null, + emitter + ); } await transactionHandlers[activeEpoch].makeClaim(savedSnapshot); } else { - console.log("No new messages, no need for a claim"); + emitter.emit(BotEvents.NO_NEW_MESSAGES); epochs.splice(i, 1); i--; continue; } } else { if (savedSnapshot == ethers.constants.HashZero) { - console.log("No snapshot saved for epoch " + activeEpoch); + emitter.emit(BotEvents.NO_SNAPSHOT); } else { - console.log("No new messages after last claim"); + emitter.emit(BotEvents.NO_NEW_MESSAGES); } epochs.splice(i, 1); i--; } } else if (claimableEpochHash != ethers.constants.HashZero) { - console.log("Claim is already made, checking for verification stage"); const claim = await fetchClaim(veaOutbox, activeEpoch); - console.log(claim); if (!transactionHandlers[activeEpoch]) { - transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox, claim); + transactionHandlers[activeEpoch] = new TransactionHandler( + chainId, + activeEpoch, + veaOutbox, + claim, + null, + emitter + ); } else { transactionHandlers[activeEpoch].claim = claim; } @@ -81,30 +103,32 @@ export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal( if (hashClaim(claim) == claimableEpochHash) { await transactionHandler.withdrawClaimDeposit(); } else { - console.log("Challenger won claim"); + emitter.emit(BotEvents.CHALLENGER_WON_CLAIM); } epochs.splice(i, 1); i--; } } else if (claim.challenger == ethers.constants.AddressZero) { - console.log("Verification not started yet"); // No verification started yet, check if we can start it await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); } else { epochs.splice(i, 1); i--; - console.log("Claim was challenged, skipping"); + emitter.emit(BotEvents.CLAIM_CHALLENGED); } } else { epochs.splice(i, 1); i--; - console.log("Epoch has passed: " + activeEpoch); + emitter.emit(BotEvents.EPOCH_PASSED, activeEpoch); } i++; } - const newEpoch = checkForNewEpoch(verifiableEpoch, chainId); - if (newEpoch != verifiableEpoch) epochs.push(newEpoch); - console.log("Waiting for next verifiable epoch after " + verifiableEpoch); + const newEpoch = getLatestVerifiableEpoch(chainId); + if (newEpoch > verifiableEpoch) { + epochs.push(newEpoch); + verifiableEpoch = newEpoch; + } + emitter.emit(BotEvents.WAITING, verifiableEpoch); await wait(1000 * 10); } return epochs; diff --git a/bridger-cli/src/utils/botEvents.ts b/bridger-cli/src/utils/botEvents.ts new file mode 100644 index 00000000..2207ce4a --- /dev/null +++ b/bridger-cli/src/utils/botEvents.ts @@ -0,0 +1,28 @@ +export enum BotEvents { + // Bridger state + STARTED = "started", + CHECKING = "checking", + WAITING = "waiting", + + // Epoch state + NO_NEW_MESSAGES = "no_new_messages", + NO_SNAPSHOT = "no_snapshot", + EPOCH_PASSED = "epoch_passed", + + // Claim state + CLAIMING = "claiming", + CHALLENGER_WON_CLAIM = "challenger_won_claim", + VERFICATION_CANT_START = "verification_cant_started", + CANT_VERIFY_SNAPSHOT = "cant_verify_snapshot", + CLAIM_CHALLENGED = "claim_challenged", + STARTING_VERIFICATION = "starting_verification", + VERIFYING = "verifying", + WITHDRAWING = "withdrawing", + + // 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", +} diff --git a/bridger-cli/src/utils/claim.test.ts b/bridger-cli/src/utils/claim.test.ts index 4e6e6ebc..2f91b354 100644 --- a/bridger-cli/src/utils/claim.test.ts +++ b/bridger-cli/src/utils/claim.test.ts @@ -1,5 +1,5 @@ import { fetchClaim, ClaimStruct, hashClaim } from "./claim"; - +import { ClaimNotFoundError } from "./errors"; import { ethers } from "ethers"; describe("snapshotClaim", () => { @@ -132,7 +132,7 @@ describe("snapshotClaim", () => { await expect(async () => { await fetchClaim(veaOutbox, epoch, getClaimForEpoch); - }).rejects.toThrow(`No claim found for epoch ${epoch}`); + }).rejects.toThrow(new ClaimNotFoundError(epoch)); }); }); diff --git a/bridger-cli/src/utils/claim.ts b/bridger-cli/src/utils/claim.ts index a4492e2e..50e26ae1 100644 --- a/bridger-cli/src/utils/claim.ts +++ b/bridger-cli/src/utils/claim.ts @@ -1,5 +1,6 @@ import { ClaimData, getClaimForEpoch } from "./graphQueries"; import { ethers } from "ethers"; +import { ClaimNotFoundError } from "./errors"; type ClaimStruct = { stateRoot: string; @@ -29,14 +30,13 @@ const fetchClaim = async ( fetchClaimForEpoch: typeof getClaimForEpoch = getClaimForEpoch ): Promise => { let claimData: ClaimData | undefined = await fetchClaimForEpoch(epoch); - // ToDo: Check for logs block range Rpc dependency, if needed used claimEpochBlock - // let claimEpochBlock = await getBlockNumberFromEpoch(veaOutbox.provider, epoch, chainId); + // TODO: Check for logs block range Rpc dependency, if needed used claimEpochBlock if (claimData === undefined) { // Initialize claimData as an empty object claimData = {} as ClaimData; const claimLogs = await veaOutbox.queryFilter(veaOutbox.filters.Claimed(null, epoch, null)); if (claimLogs.length === 0) { - throw new Error(`No claim found for epoch ${epoch}`); + throw new ClaimNotFoundError(epoch); } claimData.bridger = `0x${claimLogs[0].topics[1].slice(26)}`; claimData.stateroot = claimLogs[0].data; @@ -51,13 +51,17 @@ const fetchClaim = async ( honest: 0, challenger: ethers.constants.AddressZero, }; - const verifyLogs = await veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(epoch)); + const [verifyLogs, challengeLogs] = await Promise.all([ + veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(epoch)), + veaOutbox.queryFilter(veaOutbox.filters.Challenged(epoch)), + ]); + if (verifyLogs.length > 0) { const verificationStartBlock = await veaOutbox.provider.getBlock(verifyLogs[0].blockHash); claim.timestampVerification = verificationStartBlock.timestamp; claim.blocknumberVerification = verificationStartBlock.number; } - const challengeLogs = await veaOutbox.queryFilter(veaOutbox.filters.Challenged(epoch)); + if (challengeLogs.length > 0) { claim.challenger = challengeLogs[0].args.challenger; } diff --git a/bridger-cli/src/utils/emitter.ts b/bridger-cli/src/utils/emitter.ts new file mode 100644 index 00000000..530f8345 --- /dev/null +++ b/bridger-cli/src/utils/emitter.ts @@ -0,0 +1,14 @@ +import { EventEmitter } from "node:events"; +import { BotEvents } from "./botEvents"; + +export const defaultEmitter = new EventEmitter(); + +export class MockEmitter extends EventEmitter { + emit(event: string | symbol, ...args: any[]): boolean { + // Prevent console logs for BotEvents during tests + if (Object.values(BotEvents).includes(event as BotEvents)) { + return true; + } + return super.emit(event, ...args); + } +} diff --git a/bridger-cli/src/utils/epochHandler.test.ts b/bridger-cli/src/utils/epochHandler.test.ts index 10130c34..7a4287e0 100644 --- a/bridger-cli/src/utils/epochHandler.test.ts +++ b/bridger-cli/src/utils/epochHandler.test.ts @@ -1,4 +1,5 @@ -import { setEpochRange, getBlockNumberFromEpoch, checkForNewEpoch } from "./epochHandler"; +import { setEpochRange, getBlockNumberFromEpoch, getLatestVerifiableEpoch } from "./epochHandler"; +import { InvalidStartEpochError } from "./errors"; describe("epochHandler", () => { describe("setEpochRange", () => { @@ -18,7 +19,7 @@ describe("epochHandler", () => { it("should throw an error if start { startEpoch = 12; - await expect(setEpochRange(veaOutbox, startEpoch)).rejects.toThrow("Current epoch is less than start epoch"); + await expect(setEpochRange(veaOutbox, startEpoch)).rejects.toThrow(InvalidStartEpochError); }); it("should return an array rolled back to default when no startEpoch provided", async () => { @@ -63,31 +64,27 @@ describe("epochHandler", () => { }); }); - describe("checkForNewEpoch", () => { + describe("getLatestVerifiableEpoch", () => { const epochPeriod = 10; + const chainId = 1; let currentEpoch: number; + let mockGetBridgeConfig: jest.Mock; beforeEach(() => { currentEpoch = 99; + mockGetBridgeConfig = jest.fn().mockReturnValue({ + epochPeriod, + }); }); afterEach(() => { jest.restoreAllMocks(); }); - it("should return a new epoch", () => { - const mockDateNow = jest.spyOn(Date, "now").mockImplementation(() => 1010 * 1000); - const result = checkForNewEpoch(currentEpoch, epochPeriod); - expect(result).toBe(100); - mockDateNow.mockRestore(); - }); - - it("should return no new epoch", () => { - const mockDateNow = jest.spyOn(Date, "now").mockImplementation(() => 1010 * 1000); - currentEpoch = 100; - const result = checkForNewEpoch(currentEpoch, epochPeriod); - expect(result).toBe(100); - mockDateNow.mockRestore(); + it.only("should return a new epoch", () => { + const mockNow = 1625097600000; + const currentEpoch = getLatestVerifiableEpoch(chainId, mockNow, mockGetBridgeConfig); + expect(currentEpoch).toBe(Math.floor(mockNow / 1000 / epochPeriod) - 1); }); }); }); diff --git a/bridger-cli/src/utils/epochHandler.ts b/bridger-cli/src/utils/epochHandler.ts index 60301c8a..f5d2dd9f 100644 --- a/bridger-cli/src/utils/epochHandler.ts +++ b/bridger-cli/src/utils/epochHandler.ts @@ -1,4 +1,6 @@ import { JsonRpcProvider } from "@ethersproject/providers"; +import { InvalidStartEpochError } from "./errors"; +import { getBridgeConfig } from "../consts/bridgeRoutes"; /** * Sets the range of epochs from the start epoch to the current epoch. @@ -14,7 +16,7 @@ const setEpochRange = async (veaOutbox: any, startEpoch: number): Promise { - if (Math.floor(Date.now() / (1000 * epochPeriod)) - 1 > currentEpoch) { - currentEpoch = Math.floor(Date.now() / 1000 / epochPeriod) - 1; - } - return currentEpoch; +const getLatestVerifiableEpoch = ( + chainId: number, + now: number = Date.now(), + fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig +): number => { + const { epochPeriod } = fetchBridgeConfig(chainId); + return Math.floor(now / 1000 / epochPeriod) - 1; }; -export { setEpochRange, getBlockNumberFromEpoch, checkForNewEpoch }; +export { setEpochRange, getBlockNumberFromEpoch, getLatestVerifiableEpoch }; diff --git a/bridger-cli/src/utils/errors.ts b/bridger-cli/src/utils/errors.ts new file mode 100644 index 00000000..8172595e --- /dev/null +++ b/bridger-cli/src/utils/errors.ts @@ -0,0 +1,27 @@ +/** + * Custom errors for the CLI + */ +class ClaimNotFoundError extends Error { + constructor(epoch: number) { + super(); + this.name = "ClaimNotFoundError"; + this.message = `No claim was found for ${epoch}`; + } +} + +class InvalidStartEpochError extends Error { + constructor(epoch: number) { + super(); + this.name = "InvalidStartEpochError"; + this.message = `Current epoch is smaller than start epoch ${epoch}`; + } +} + +class ClaimNotSetError extends Error { + constructor() { + super(); + this.name = "NoClaimSetError"; + this.message = "Claim is not set"; + } +} +export { ClaimNotFoundError, InvalidStartEpochError, ClaimNotSetError }; diff --git a/bridger-cli/src/utils/ethers.ts b/bridger-cli/src/utils/ethers.ts index e7006ee5..c9fb863c 100644 --- a/bridger-cli/src/utils/ethers.ts +++ b/bridger-cli/src/utils/ethers.ts @@ -8,7 +8,7 @@ import { VeaInboxArbToGnosis__factory, VeaOutboxArbToGnosis__factory, } from "@kleros/vea-contracts/typechain-types"; -import { getBridgeConfig } from "consts/bridgeRoutes"; +import { getBridgeConfig } from "../consts/bridgeRoutes"; function getWallet(privateKey: string, web3ProviderURL: string) { return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL)); diff --git a/bridger-cli/src/utils/logger.ts b/bridger-cli/src/utils/logger.ts new file mode 100644 index 00000000..d6dbceb3 --- /dev/null +++ b/bridger-cli/src/utils/logger.ts @@ -0,0 +1,98 @@ +import { EventEmitter } from "node:events"; +import { BotEvents } from "./botEvents"; + +/** + * Listens to relevant events of an EventEmitter instance and issues log lines + * + * @param emitter - The event emitter instance that issues the relevant events + * + * @example + * + * const emitter = new EventEmitter(); + * initialize(emitter); + */ + +export const initialize = (emitter: EventEmitter) => { + return configurableInitialize(emitter); +}; + +export const configurableInitialize = (emitter: EventEmitter) => { + // Bridger state logs + emitter.on(BotEvents.STARTED, () => { + console.log("Bridger started"); + }); + + emitter.on(BotEvents.CHECKING, (epoch: number) => { + console.log(`Running checks for epoch ${epoch}`); + }); + + emitter.on(BotEvents.WAITING, (epoch: number) => { + console.log(`Waiting for next verifiable epoch after ${epoch}`); + }); + + emitter.on(BotEvents.NO_NEW_MESSAGES, () => { + console.log("No new messages found"); + }); + + emitter.on(BotEvents.NO_SNAPSHOT, () => { + console.log("No snapshot saved for epoch"); + }); + + emitter.on(BotEvents.EPOCH_PASSED, (epoch: number) => { + console.log(`Epoch ${epoch} has passed`); + }); + + emitter.on(BotEvents.CHALLENGER_WON_CLAIM, () => { + console.log("Challenger won claim"); + }); + + emitter.on(BotEvents.CLAIM_CHALLENGED, () => { + console.log("Claim was challenged, skipping"); + }); + + // Transaction state logs + emitter.on(BotEvents.TXN_MADE, (transaction: string, epoch: number, state: string) => { + console.log(`${state} transaction for ${epoch} made with hash: ${transaction}`); + }); + emitter.on(BotEvents.TXN_PENDING, (transaction: string) => { + console.log(`Transaction is still pending with hash: ${transaction}`); + }); + + emitter.on(BotEvents.TXN_FINAL, (transaction: string, confirmations: number) => { + console.log(`Transaction(${transaction}) is final with ${confirmations} confirmations`); + }); + + emitter.on(BotEvents.TXN_NOT_FINAL, (transaction: string, confirmations: number) => { + console.log(`Transaction(${transaction}) is not final yet, ${confirmations} confirmations left.`); + }); + emitter.on(BotEvents.TXN_PENDING_CONFIRMATIONS, (transaction: string, confirmations: number) => { + console.log(`Transaction(${transaction}) is pending with ${confirmations} confirmations`); + }); + + // Claim state logs + // makeClaim() + emitter.on(BotEvents.CLAIMING, (epoch: number) => { + console.log(`Making claim for epoch ${epoch}`); + }); + + // startVerification() + emitter.on(BotEvents.STARTING_VERIFICATION, (epoch: number) => { + console.log(`Starting verification for epoch ${epoch}`); + }); + emitter.on(BotEvents.VERFICATION_CANT_START, (time) => { + console.log(`Waiting for sequencer delay to pass to start verification, seconds left: ${time}`); + }); + + // verifySnapshot() + emitter.on(BotEvents.VERIFYING, (epoch: number) => { + console.log(`Verifying snapshot for epoch ${epoch}`); + }); + emitter.on(BotEvents.CANT_VERIFY_SNAPSHOT, (time) => { + console.log(`Waiting for min challenge period to pass to verify snapshot, seconds left: ${time}`); + }); + + // withdrawClaimDeposit() + emitter.on(BotEvents.WITHDRAWING, (epoch: number) => { + console.log(`Withdrawing deposit for epoch ${epoch}`); + }); +}; diff --git a/bridger-cli/src/utils/shutdown.ts b/bridger-cli/src/utils/shutdown.ts index 221b42be..74671caf 100644 --- a/bridger-cli/src/utils/shutdown.ts +++ b/bridger-cli/src/utils/shutdown.ts @@ -1,3 +1,6 @@ +/** + * A class to represent a shutdown signal. + */ export class ShutdownSignal { private isShutdownSignal: boolean; diff --git a/bridger-cli/src/utils/transactionHandler.test.ts b/bridger-cli/src/utils/transactionHandler.test.ts index 43805afc..b75ff115 100644 --- a/bridger-cli/src/utils/transactionHandler.test.ts +++ b/bridger-cli/src/utils/transactionHandler.test.ts @@ -1,4 +1,6 @@ import { TransactionHandler } from "./transactionHandler"; +import { ClaimNotSetError } from "./errors"; +import { BotEvents } from "./botEvents"; describe("TransactionHandler Tests", () => { let chainId: number; @@ -72,40 +74,43 @@ describe("TransactionHandler Tests", () => { getBlock: jest.fn(), }; }); - it("should return true if transaction is pending", async () => { + it("should return true if transaction is not final", async () => { veaOutbox.provider.getBlock.mockReturnValue({ number: blockNumber + transactionHandler.requiredConfirmations - 1, }); - const consoleSpy = jest.spyOn(console, "log"); + const emitSpy = jest.spyOn(transactionHandler.emitter, "emit"); const result = await transactionHandler.checkTransactionPendingStatus(trnxHash); expect(result).toBeTruthy(); - expect(consoleSpy).toHaveBeenCalledWith(`Transaction ${trnxHash} is not final yet.`); + expect(emitSpy).toHaveBeenCalledWith( + BotEvents.TXN_NOT_FINAL, + trnxHash, + transactionHandler.requiredConfirmations - 1 + ); }); it("should return false if transaction is confirmed", async () => { veaOutbox.provider.getBlock.mockReturnValue({ number: blockNumber + transactionHandler.requiredConfirmations, }); - const consoleSpy = jest.spyOn(console, "log"); + const emitSpy = jest.spyOn(transactionHandler.emitter, "emit"); const result = await transactionHandler.checkTransactionPendingStatus(trnxHash); expect(result).toBeFalsy(); - expect(consoleSpy).toHaveBeenCalledWith( - `Transaction ${trnxHash} is final with ${transactionHandler.requiredConfirmations} confirmations` - ); + expect(emitSpy).toHaveBeenCalledWith(BotEvents.TXN_FINAL, trnxHash, transactionHandler.requiredConfirmations); }); + it("should return true if transaction receipt is not found", async () => { veaOutbox.provider.getTransactionReceipt.mockResolvedValue(null); - const consoleSpy = jest.spyOn(console, "log"); + const emitSpy = jest.spyOn(transactionHandler.emitter, "emit"); const result = await transactionHandler.checkTransactionPendingStatus(trnxHash); expect(result).toBeTruthy(); - expect(consoleSpy).toHaveBeenCalledWith(`Transaction ${trnxHash} is pending`); + expect(emitSpy).toHaveBeenCalledWith(BotEvents.TXN_PENDING, trnxHash); }); it("should return false if transaction is null", async () => { - const consoleSpy = jest.spyOn(console, "log"); + const emitSpy = jest.spyOn(transactionHandler.emitter, "emit"); const result = await transactionHandler.checkTransactionPendingStatus(null); expect(result).toBeFalsy(); - expect(consoleSpy).not.toHaveBeenCalled(); + expect(emitSpy).not.toHaveBeenCalled(); }); }); @@ -178,7 +183,7 @@ describe("TransactionHandler Tests", () => { jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); - await expect(transactionHandler.startVerification(startVerifyTimeFlip)).rejects.toThrow("Claim is not set"); + await expect(transactionHandler.startVerification(startVerifyTimeFlip)).rejects.toThrow(ClaimNotSetError); }); it("should not start verification if a startVerification transaction is pending", async () => { @@ -229,7 +234,7 @@ describe("TransactionHandler Tests", () => { jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); - await expect(transactionHandler.verifySnapshot(verificationFlipTime)).rejects.toThrow("Claim is not set"); + await expect(transactionHandler.verifySnapshot(verificationFlipTime)).rejects.toThrow(ClaimNotSetError); }); it("should not verify snapshot if a verifySnapshot transaction is pending", async () => { diff --git a/bridger-cli/src/utils/transactionHandler.ts b/bridger-cli/src/utils/transactionHandler.ts index 5047e95f..8e59db29 100644 --- a/bridger-cli/src/utils/transactionHandler.ts +++ b/bridger-cli/src/utils/transactionHandler.ts @@ -1,5 +1,9 @@ +import { EventEmitter } from "node:events"; import { getBridgeConfig } from "../consts/bridgeRoutes"; import { ClaimStruct } from "./claim"; +import { ClaimNotSetError } from "./errors"; +import { defaultEmitter } from "./emitter"; +import { BotEvents } from "./botEvents"; interface PendingTransactions { claim: string | null; @@ -15,6 +19,8 @@ interface PendingTransactions { * @param epoch - The epoch number for which the transactions are being handled * @param veaOutbox - The veaOutbox instance to use for sending transactions * @param claim - The claim object for the epoch + * @param fetchBridgeConfig - The function to fetch the bridge config + * @param emiitter - The event emitter instance to use for emitting events * @returns An instance of the TransactionHandler class * * @example @@ -29,6 +35,7 @@ export class TransactionHandler { public claim: ClaimStruct | null; public getBridgeConfig: typeof getBridgeConfig; public requiredConfirmations: number = 12; + public emitter: EventEmitter; public pendingTransactions: PendingTransactions = { claim: null, @@ -42,13 +49,15 @@ export class TransactionHandler { epoch: number, veaOutbox: any, claim?: ClaimStruct, - fetchBridgeConfig: typeof getBridgeConfig = getBridgeConfig + fetchBridgeConfig: typeof getBridgeConfig | null = getBridgeConfig, + emiitter?: EventEmitter ) { this.epoch = epoch; this.veaOutbox = veaOutbox; this.chainId = chainId; this.claim = claim; - this.getBridgeConfig = fetchBridgeConfig; + this.getBridgeConfig = getBridgeConfig || fetchBridgeConfig; + this.emitter = emiitter || defaultEmitter; } public async checkTransactionPendingStatus(trnxHash: string | null): Promise { @@ -59,7 +68,7 @@ export class TransactionHandler { const receipt = await this.veaOutbox.provider.getTransactionReceipt(trnxHash); if (!receipt) { - console.log(`Transaction ${trnxHash} is pending`); + this.emitter.emit(BotEvents.TXN_PENDING, trnxHash); return true; } @@ -67,17 +76,17 @@ export class TransactionHandler { const confirmations = currentBlock.number - receipt.blockNumber; if (confirmations >= this.requiredConfirmations) { - console.log(`Transaction ${trnxHash} is final with ${confirmations} confirmations`); + this.emitter.emit(BotEvents.TXN_FINAL, trnxHash, confirmations); return false; } else { - console.log(`Transaction ${trnxHash} is not final yet.`); + this.emitter.emit(BotEvents.TXN_NOT_FINAL, trnxHash, confirmations); return true; } } public async makeClaim(stateRoot: string) { + this.emitter.emit(BotEvents.CLAIMING, this.epoch); if (await this.checkTransactionPendingStatus(this.pendingTransactions.claim)) { - console.log("Claim transaction is still pending with hash: " + this.pendingTransactions.claim); return; } const bridgeConfig = this.getBridgeConfig(this.chainId); @@ -86,74 +95,68 @@ export class TransactionHandler { value: bridgeConfig.deposit, gasLimit: estimateGas, }); - console.log(`Epoch ${this.epoch} was claimed with trnx hash ${claimTransaction.hash}`); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Claim"); this.pendingTransactions.claim = claimTransaction.hash; } public async startVerification(latestBlockTimestamp: number) { + this.emitter.emit(BotEvents.STARTING_VERIFICATION, this.epoch); if (this.claim == null) { - throw new Error("Claim is not set"); + throw new ClaimNotSetError(); } if (await this.checkTransactionPendingStatus(this.pendingTransactions.startVerification)) { - console.log( - "Start verification transaction is still pending with hash: " + this.pendingTransactions.startVerification - ); return; } const bridgeConfig = this.getBridgeConfig(this.chainId); const timeOver = latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.sequencerDelayLimit - bridgeConfig.epochPeriod; - console.log(timeOver); - if (timeOver >= 0) { - const estimateGas = await this.veaOutbox.estimateGas.startVerification(this.epoch, this.claim); - const startVerifTrx = await this.veaOutbox.startVerification(this.epoch, this.claim, { gasLimit: estimateGas }); - console.log(`Verification started for epoch ${this.epoch} with trx hash ${startVerifTrx.hash}`); - this.pendingTransactions.startVerification = startVerifTrx.hash; - } else { - console.log("Sequencer delay not passed yet, seconds left: " + -1 * timeOver); + + if (timeOver < 0) { + this.emitter.emit(BotEvents.VERFICATION_CANT_START, -1 * timeOver); + return; } + const estimateGas = await this.veaOutbox.estimateGas.startVerification(this.epoch, this.claim); + const startVerifTrx = await this.veaOutbox.startVerification(this.epoch, this.claim, { gasLimit: estimateGas }); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, startVerifTrx.hash, "Start Verification"); + this.pendingTransactions.startVerification = startVerifTrx.hash; } public async verifySnapshot(latestBlockTimestamp: number) { + this.emitter.emit(BotEvents.VERIFYING, this.epoch); if (this.claim == null) { - throw new Error("Claim is not set"); + throw new ClaimNotSetError(); } if (await this.checkTransactionPendingStatus(this.pendingTransactions.verifySnapshot)) { - console.log("Verify snapshot transaction is still pending with hash: " + this.pendingTransactions.verifySnapshot); return; } const bridgeConfig = this.getBridgeConfig(this.chainId); const timeLeft = latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.minChallengePeriod; - console.log("Time left for verification: " + timeLeft); - console.log(latestBlockTimestamp, this.claim.timestampClaimed, bridgeConfig.minChallengePeriod); + // Claim not resolved yet, check if we can verifySnapshot - if (timeLeft >= 0) { - console.log("Verification period passed, verifying snapshot"); - // Estimate gas for verifySnapshot - const estimateGas = await this.veaOutbox.estimateGas.verifySnapshot(this.epoch, this.claim); - const claimTransaction = await this.veaOutbox.verifySnapshot(this.epoch, this.claim, { - gasLimit: estimateGas, - }); - console.log(`Epoch ${this.epoch} verification started with trnx hash ${claimTransaction.hash}`); - this.pendingTransactions.verifySnapshot = claimTransaction.hash; - } else { - console.log("Censorship test in progress, sec left: " + -1 * timeLeft); + if (timeLeft < 0) { + this.emitter.emit(BotEvents.CANT_VERIFY_SNAPSHOT, -1 * timeLeft); + return; } + // Estimate gas for verifySnapshot + const estimateGas = await this.veaOutbox.estimateGas.verifySnapshot(this.epoch, this.claim); + const claimTransaction = await this.veaOutbox.verifySnapshot(this.epoch, this.claim, { + gasLimit: estimateGas, + }); + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Verify Snapshot"); + this.pendingTransactions.verifySnapshot = claimTransaction.hash; } public async withdrawClaimDeposit() { + this.emitter.emit(BotEvents.WITHDRAWING, this.epoch); if (await this.checkTransactionPendingStatus(this.pendingTransactions.withdrawClaimDeposit)) { - console.log( - "Withdraw deposit transaction is still pending with hash: " + this.pendingTransactions.withdrawClaimDeposit - ); return; } const estimateGas = await this.veaOutbox.estimateGas.withdrawClaimDeposit(this.epoch, this.claim); - const claimTransaction = await this.veaOutbox.withdrawClaimDeposit(this.epoch, this.claim, { + const withdrawTxn = await this.veaOutbox.withdrawClaimDeposit(this.epoch, this.claim, { gasLimit: estimateGas, }); - console.log(`Deposit withdrawn with trnx hash ${claimTransaction.hash}`); - this.pendingTransactions.withdrawClaimDeposit = claimTransaction.hash; + this.emitter.emit(BotEvents.TXN_MADE, this.epoch, withdrawTxn.hash, "Withdraw Deposit"); + this.pendingTransactions.withdrawClaimDeposit = withdrawTxn.hash; } } From 8e818406504f557cd017e3e047abe891fcee517f Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Thu, 12 Dec 2024 16:36:54 +0700 Subject: [PATCH 13/15] fix: remove import --- bridger-cli/src/bridger.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts index 3d7ba65e..505505be 100644 --- a/bridger-cli/src/bridger.ts +++ b/bridger-cli/src/bridger.ts @@ -10,7 +10,6 @@ import { ShutdownSignal } from "./utils/shutdown"; import { initialize as initializeLogger } from "./utils/logger"; import { defaultEmitter } from "./utils/emitter"; import { BotEvents } from "./utils/botEvents"; -import { get } from "http"; export const watch = async ( shutDownSignal: ShutdownSignal = new ShutdownSignal(), From 590f6ff33e6222dd6a53d31bebcb1de40258012c Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Thu, 2 Jan 2025 18:11:07 +0530 Subject: [PATCH 14/15] chore: ethers v6 upgrade --- bridger-cli/src/bridger.test.ts | 13 +++--- bridger-cli/src/bridger.ts | 40 ++++++----------- bridger-cli/src/consts/bridgeRoutes.ts | 8 ++-- bridger-cli/src/utils/claim.test.ts | 10 ++--- bridger-cli/src/utils/claim.ts | 6 +-- bridger-cli/src/utils/ethers.ts | 14 ++---- .../src/utils/transactionHandler.test.ts | 43 ++++++++----------- bridger-cli/src/utils/transactionHandler.ts | 23 ++++------ 8 files changed, 61 insertions(+), 96 deletions(-) diff --git a/bridger-cli/src/bridger.test.ts b/bridger-cli/src/bridger.test.ts index f12f4b5d..9c5a4e3e 100644 --- a/bridger-cli/src/bridger.test.ts +++ b/bridger-cli/src/bridger.test.ts @@ -1,9 +1,8 @@ require("dotenv").config(); import { assert } from "chai"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { BigNumber } from "ethers"; import { MockEmitter } from "./utils/emitter"; -import { hashClaim } from "./utils/claim"; +import { ClaimStruct, hashClaim } from "./utils/claim"; import { getVeaOutbox, getVeaInbox } from "./utils/ethers"; import { watch } from "./bridger"; import { ShutdownSignal } from "./utils/shutdown"; @@ -22,7 +21,7 @@ describe("bridger", function () { let claimEpoch: number; let epochPeriod: number; - let deposit: BigNumber; + let deposit: bigint; let sequencerDelay: number; const veaInbox = getVeaInbox( @@ -101,9 +100,9 @@ describe("bridger", function () { const bridger = `0x${claimData[0].topics[1].slice(26)}`; const claimBlock = await outboxProvider.getBlock(claimData[0].blockNumber); - const claim = { + const claim: ClaimStruct = { stateRoot: toBeClaimedStateRoot, - claimer: bridger, + claimer: bridger as `0x${string}`, timestampClaimed: claimBlock.timestamp, timestampVerification: 0, blocknumberVerification: 0, @@ -304,11 +303,11 @@ describe("bridger", function () { await veaOutbox.verifySnapshot(claimEpoch, claim); const balancePreWithdraw = await outboxProvider.getBalance(claimTxn.from); - const contractBalancePreWithdraw = await outboxProvider.getBalance(veaOutbox.address); + const contractBalancePreWithdraw = await outboxProvider.getBalance(process.env.VEAOUTBOX_ADDRESS); await startBridgerWithTimeout(5000, claimEpoch); const balancePostWithdraw = await outboxProvider.getBalance(claimTxn.from); - const contractBalancePostWithdraw = await outboxProvider.getBalance(veaOutbox.address); + const contractBalancePostWithdraw = await outboxProvider.getBalance(process.env.VEAOUTBOX_ADDRESS); assert(balancePostWithdraw.gt(balancePreWithdraw), "Deposit was not withdrawn"); assert(contractBalancePostWithdraw.eq(contractBalancePreWithdraw.sub(deposit)), "Deposit was not withdrawn"); diff --git a/bridger-cli/src/bridger.ts b/bridger-cli/src/bridger.ts index 505505be..4dd43529 100644 --- a/bridger-cli/src/bridger.ts +++ b/bridger-cli/src/bridger.ts @@ -1,4 +1,5 @@ require("dotenv").config(); +import { JsonRpcProvider } from "@ethersproject/providers"; import { ethers } from "ethers"; import { EventEmitter } from "events"; import { getLastClaimedEpoch } from "./utils/graphQueries"; @@ -20,12 +21,13 @@ export const watch = async ( emitter.emit(BotEvents.STARTED); const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID); const veaInboxAddress = process.env.VEAINBOX_ADDRESS; - const veaInboxProviderURL = process.env.VEAINBOX_PROVIDER; const veaOutboxAddress = process.env.VEAOUTBOX_ADDRESS; - const veaOutboxProviderURL = process.env.VEAOUTBOX_PROVIDER; const PRIVATE_KEY = process.env.PRIVATE_KEY; - const veaInbox = getVeaInbox(veaInboxAddress, PRIVATE_KEY, veaInboxProviderURL, chainId); - const veaOutbox = getVeaOutbox(veaOutboxAddress, PRIVATE_KEY, veaOutboxProviderURL, chainId); + const veaInboxRPC = process.env.VEAINBOX_PROVIDER; + const veaOutboxRPC = process.env.VEAOUTBOX_PROVIDER; + const veaInbox = getVeaInbox(veaInboxAddress, PRIVATE_KEY, veaInboxRPC, chainId); + const veaOutbox = getVeaOutbox(veaOutboxAddress, PRIVATE_KEY, veaOutboxRPC, chainId); + const veaOutboxProvider = new JsonRpcProvider(veaOutboxRPC); const epochs = await setEpochRange(veaOutbox, startEpoch); let verifiableEpoch = epochs[epochs.length - 1] - 1; @@ -38,12 +40,12 @@ export const watch = async ( emitter.emit(BotEvents.CHECKING, activeEpoch); let claimableEpochHash = await veaOutbox.claimHashes(activeEpoch); let outboxStateRoot = await veaOutbox.stateRoot(); - const finalizedOutboxBlock = await veaOutbox.provider.getBlock("finalized"); + const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized"); - if (claimableEpochHash == ethers.constants.HashZero && activeEpoch == verifiableEpoch) { + if (claimableEpochHash == ethers.ZeroAddress && activeEpoch == verifiableEpoch) { // Claim can be made const savedSnapshot = await veaInbox.snapshots(activeEpoch); - if (savedSnapshot != outboxStateRoot && savedSnapshot != ethers.constants.HashZero) { + if (savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash) { // Its possible that a claim was made for previous epoch but its not verified yet // Making claim if there are new messages or last claim was challenged. const claimData = await getLastClaimedEpoch(); @@ -51,14 +53,7 @@ export const watch = async ( if (claimData.challenged || claimData.stateroot != savedSnapshot) { // Making claim as either last claim was challenged or there are new messages if (!transactionHandlers[activeEpoch]) { - transactionHandlers[activeEpoch] = new TransactionHandler( - chainId, - activeEpoch, - veaOutbox, - null, - null, - emitter - ); + transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox, null, emitter); } await transactionHandlers[activeEpoch].makeClaim(savedSnapshot); } else { @@ -68,7 +63,7 @@ export const watch = async ( continue; } } else { - if (savedSnapshot == ethers.constants.HashZero) { + if (savedSnapshot == ethers.ZeroHash) { emitter.emit(BotEvents.NO_SNAPSHOT); } else { emitter.emit(BotEvents.NO_NEW_MESSAGES); @@ -76,17 +71,10 @@ export const watch = async ( epochs.splice(i, 1); i--; } - } else if (claimableEpochHash != ethers.constants.HashZero) { + } else if (claimableEpochHash != ethers.ZeroHash) { const claim = await fetchClaim(veaOutbox, activeEpoch); if (!transactionHandlers[activeEpoch]) { - transactionHandlers[activeEpoch] = new TransactionHandler( - chainId, - activeEpoch, - veaOutbox, - claim, - null, - emitter - ); + transactionHandlers[activeEpoch] = new TransactionHandler(chainId, activeEpoch, veaOutbox, claim, emitter); } else { transactionHandlers[activeEpoch].claim = claim; } @@ -107,7 +95,7 @@ export const watch = async ( epochs.splice(i, 1); i--; } - } else if (claim.challenger == ethers.constants.AddressZero) { + } else if (claim.challenger == ethers.ZeroAddress) { // No verification started yet, check if we can start it await transactionHandler.startVerification(finalizedOutboxBlock.timestamp); } else { diff --git a/bridger-cli/src/consts/bridgeRoutes.ts b/bridger-cli/src/consts/bridgeRoutes.ts index 79a626a5..4726f29b 100644 --- a/bridger-cli/src/consts/bridgeRoutes.ts +++ b/bridger-cli/src/consts/bridgeRoutes.ts @@ -1,9 +1,7 @@ -import { BigNumber } from "ethers"; - interface IBridge { chain: string; epochPeriod: number; - deposit: BigNumber; + deposit: bigint; minChallengePeriod: number; sequencerDelayLimit: number; } @@ -12,14 +10,14 @@ const bridges: { [chainId: number]: IBridge } = { 11155111: { chain: "sepolia", epochPeriod: 7200, - deposit: BigNumber.from("1000000000000000000"), + deposit: BigInt("1000000000000000000"), minChallengePeriod: 10800, sequencerDelayLimit: 86400, }, 10200: { chain: "chiado", epochPeriod: 3600, - deposit: BigNumber.from("1000000000000000000"), + deposit: BigInt("1000000000000000000"), minChallengePeriod: 10800, sequencerDelayLimit: 86400, }, diff --git a/bridger-cli/src/utils/claim.test.ts b/bridger-cli/src/utils/claim.test.ts index 2f91b354..81b45582 100644 --- a/bridger-cli/src/utils/claim.test.ts +++ b/bridger-cli/src/utils/claim.test.ts @@ -17,7 +17,7 @@ describe("snapshotClaim", () => { timestampVerification: 0, blocknumberVerification: 0, honest: 0, - challenger: ethers.constants.AddressZero, + challenger: ethers.ZeroAddress as `0x${string}`, }; getClaimForEpoch = jest.fn().mockResolvedValue({ stateroot: mockClaim.stateRoot, @@ -43,7 +43,7 @@ describe("snapshotClaim", () => { it("should return a valid claim", async () => { veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); veaOutbox.queryFilter.mockImplementationOnce(() => - Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.constants.AddressZero } }]) + Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.ZeroAddress } }]) ); const claim = await fetchClaim(veaOutbox, epoch, getClaimForEpoch); @@ -80,7 +80,7 @@ describe("snapshotClaim", () => { mockClaim.timestampVerification = 1234; mockClaim.blocknumberVerification = 1234; veaOutbox.queryFilter.mockImplementationOnce(() => - Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.constants.AddressZero } }]) + Promise.resolve([{ blockHash: "0x1234", args: { challenger: ethers.ZeroAddress } }]) ); veaOutbox.queryFilter.mockImplementationOnce(() => Promise.resolve([])); getClaimForEpoch.mockResolvedValueOnce({ @@ -110,7 +110,7 @@ describe("snapshotClaim", () => { { blockNumber: 1234, data: mockClaim.stateRoot, - topics: [ethers.constants.AddressZero, `0x${"0".repeat(24)}${mockClaim.claimer.slice(2)}`], + topics: [ethers.ZeroAddress, `0x${"0".repeat(24)}${mockClaim.claimer.slice(2)}`], }, ]) ); @@ -144,7 +144,7 @@ describe("snapshotClaim", () => { timestampVerification: 0, blocknumberVerification: 0, honest: 0, - challenger: ethers.constants.AddressZero, + challenger: ethers.ZeroAddress as `0x${string}`, }; // Pre calculated from the deployed contracts const hashOfMockClaim = "0xfee47661ef0432da320c3b4706ff7d412f421b9d1531c33ce8f2e03bfe5dcfa2"; diff --git a/bridger-cli/src/utils/claim.ts b/bridger-cli/src/utils/claim.ts index 50e26ae1..71e9d739 100644 --- a/bridger-cli/src/utils/claim.ts +++ b/bridger-cli/src/utils/claim.ts @@ -49,7 +49,7 @@ const fetchClaim = async ( timestampVerification: 0, blocknumberVerification: 0, honest: 0, - challenger: ethers.constants.AddressZero, + challenger: ethers.ZeroAddress as `0x${string}`, }; const [verifyLogs, challengeLogs] = await Promise.all([ veaOutbox.queryFilter(veaOutbox.filters.VerificationStarted(epoch)), @@ -79,8 +79,8 @@ const fetchClaim = async ( * @example * const claimHash = hashClaim(claim); */ -const hashClaim = (claim) => { - return ethers.utils.solidityKeccak256( +const hashClaim = (claim: ClaimStruct) => { + return ethers.solidityPackedKeccak256( ["bytes32", "address", "uint32", "uint32", "uint32", "uint8", "address"], [ claim.stateRoot, diff --git a/bridger-cli/src/utils/ethers.ts b/bridger-cli/src/utils/ethers.ts index c9fb863c..aad87bf7 100644 --- a/bridger-cli/src/utils/ethers.ts +++ b/bridger-cli/src/utils/ethers.ts @@ -1,5 +1,4 @@ -import { Wallet } from "@ethersproject/wallet"; -import { JsonRpcProvider } from "@ethersproject/providers"; +import { Wallet, JsonRpcProvider } from "ethers"; import { VeaOutboxArbToEth__factory, VeaOutboxArbToEthDevnet__factory, @@ -20,16 +19,11 @@ function getWalletRPC(privateKey: string, rpc: JsonRpcProvider) { // Using destination chainId as identifier, Ex: Arbitrum One (42161) -> Ethereum Mainnet (1): Use "1" as chainId function getVeaInbox(veaInboxAddress: string, privateKey: string, web3ProviderURL: string, chainId: number) { - const bridge = getBridgeConfig(chainId); - switch (bridge.chain) { - case "sepolia": - case "mainnet": + switch (chainId) { + case 11155111: return VeaInboxArbToEth__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); - case "chiado": - case "gnosis": + case 10200: return VeaInboxArbToGnosis__factory.connect(veaInboxAddress, getWallet(privateKey, web3ProviderURL)); - default: - throw new Error(`Unsupported chainId: ${chainId}`); } } diff --git a/bridger-cli/src/utils/transactionHandler.test.ts b/bridger-cli/src/utils/transactionHandler.test.ts index b75ff115..3cf34219 100644 --- a/bridger-cli/src/utils/transactionHandler.test.ts +++ b/bridger-cli/src/utils/transactionHandler.test.ts @@ -1,16 +1,14 @@ import { TransactionHandler } from "./transactionHandler"; import { ClaimNotSetError } from "./errors"; import { BotEvents } from "./botEvents"; +import { getBridgeConfig } from "../consts/bridgeRoutes"; describe("TransactionHandler Tests", () => { - let chainId: number; + const chainId = 11155111; let epoch: number; let claim: any; let veaOutbox: any; - let mockGetBridgeConfig: any; beforeEach(() => { - chainId = 1; - epoch = 10; claim = { stateRoot: "0x1234", claimer: "0x1234", @@ -36,9 +34,6 @@ describe("TransactionHandler Tests", () => { getBlock: jest.fn(), }, }; - mockGetBridgeConfig = jest - .fn() - .mockReturnValue({ deposit: 1000, minChallengePeriod: 1000, sequencerDelayLimit: 1000, epochPeriod: 1000 }); }); afterEach(() => { jest.restoreAllMocks(); @@ -124,18 +119,18 @@ describe("TransactionHandler Tests", () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null); await transactionHandler.makeClaim(claim.stateRoot); - expect(veaOutbox.estimateGas.claim).toHaveBeenCalledWith(epoch, claim.stateRoot, { value: 1000 }); - expect(veaOutbox.claim).toHaveBeenCalledWith(epoch, claim.stateRoot, { value: 1000, gasLimit: 1000 }); + expect(veaOutbox.estimateGas.claim).toHaveBeenCalled(); + expect(veaOutbox.claim).toHaveBeenCalled(); expect(transactionHandler.pendingTransactions.claim).toEqual("0x1234"); }); it("should not make a claim if a claim transaction is pending", async () => { // Mock checkTransactionPendingStatus to always return true jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox); await transactionHandler.makeClaim(claim.stateRoot); expect(veaOutbox.estimateGas.claim).not.toHaveBeenCalled(); @@ -150,14 +145,12 @@ describe("TransactionHandler Tests", () => { veaOutbox.estimateGas.startVerification.mockResolvedValue(1000); veaOutbox.startVerification.mockResolvedValue({ hash: "0x1234" }); startVerifyTimeFlip = - claim.timestampClaimed + - mockGetBridgeConfig(chainId).epochPeriod + - mockGetBridgeConfig(chainId).sequencerDelayLimit; + claim.timestampClaimed + getBridgeConfig(chainId).epochPeriod + getBridgeConfig(chainId).sequencerDelayLimit; }); it("should start verification and set pending startVerification trnx", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.startVerification(startVerifyTimeFlip); @@ -169,7 +162,7 @@ describe("TransactionHandler Tests", () => { it("should not start verification if timeout has not passed", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.startVerification(startVerifyTimeFlip - 1); @@ -181,7 +174,7 @@ describe("TransactionHandler Tests", () => { it("should not start verification if claim is not set", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null); await expect(transactionHandler.startVerification(startVerifyTimeFlip)).rejects.toThrow(ClaimNotSetError); }); @@ -189,7 +182,7 @@ describe("TransactionHandler Tests", () => { it("should not start verification if a startVerification transaction is pending", async () => { // Mock checkTransactionPendingStatus to always return true jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.startVerification(startVerifyTimeFlip); expect(veaOutbox.estimateGas.startVerification).not.toHaveBeenCalled(); expect(veaOutbox.startVerification).not.toHaveBeenCalled(); @@ -202,13 +195,13 @@ describe("TransactionHandler Tests", () => { beforeEach(() => { veaOutbox.estimateGas.verifySnapshot.mockResolvedValue(1000); veaOutbox.verifySnapshot.mockResolvedValue({ hash: "0x1234" }); - verificationFlipTime = claim.timestampClaimed + mockGetBridgeConfig(chainId).minChallengePeriod; + verificationFlipTime = claim.timestampClaimed + getBridgeConfig(chainId).minChallengePeriod; }); it("should verify snapshot and set pending verifySnapshot trnx", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.verifySnapshot(verificationFlipTime); @@ -220,7 +213,7 @@ describe("TransactionHandler Tests", () => { it("should not verify snapshot if timeout has not passed", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.verifySnapshot(verificationFlipTime - 1); @@ -232,7 +225,7 @@ describe("TransactionHandler Tests", () => { it("should not verify snapshot if claim is not set", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, null); await expect(transactionHandler.verifySnapshot(verificationFlipTime)).rejects.toThrow(ClaimNotSetError); }); @@ -240,7 +233,7 @@ describe("TransactionHandler Tests", () => { it("should not verify snapshot if a verifySnapshot transaction is pending", async () => { // Mock checkTransactionPendingStatus to always return true jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.verifySnapshot(verificationFlipTime); @@ -258,7 +251,7 @@ describe("TransactionHandler Tests", () => { it("should withdraw deposit and set pending withdrawClaimDeposit trnx", async () => { // Mock checkTransactionPendingStatus to always return false jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(false); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.withdrawClaimDeposit(); expect(veaOutbox.estimateGas.withdrawClaimDeposit).toHaveBeenCalledWith(epoch, claim); @@ -269,7 +262,7 @@ describe("TransactionHandler Tests", () => { it("should not withdraw deposit if a withdrawClaimDeposit transaction is pending", async () => { // Mock checkTransactionPendingStatus to always return true jest.spyOn(TransactionHandler.prototype, "checkTransactionPendingStatus").mockResolvedValue(true); - const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim, mockGetBridgeConfig); + const transactionHandler = new TransactionHandler(chainId, epoch, veaOutbox, claim); await transactionHandler.withdrawClaimDeposit(); expect(veaOutbox.estimateGas.withdrawClaimDeposit).not.toHaveBeenCalled(); diff --git a/bridger-cli/src/utils/transactionHandler.ts b/bridger-cli/src/utils/transactionHandler.ts index 8e59db29..6beeffdd 100644 --- a/bridger-cli/src/utils/transactionHandler.ts +++ b/bridger-cli/src/utils/transactionHandler.ts @@ -33,7 +33,6 @@ export class TransactionHandler { public veaOutbox: any; public chainId: number; public claim: ClaimStruct | null; - public getBridgeConfig: typeof getBridgeConfig; public requiredConfirmations: number = 12; public emitter: EventEmitter; @@ -44,19 +43,11 @@ export class TransactionHandler { startVerification: null, }; - constructor( - chainId: number, - epoch: number, - veaOutbox: any, - claim?: ClaimStruct, - fetchBridgeConfig: typeof getBridgeConfig | null = getBridgeConfig, - emiitter?: EventEmitter - ) { + constructor(chainId: number, epoch: number, veaOutbox: any, claim?: ClaimStruct, emiitter?: EventEmitter) { this.epoch = epoch; this.veaOutbox = veaOutbox; this.chainId = chainId; this.claim = claim; - this.getBridgeConfig = getBridgeConfig || fetchBridgeConfig; this.emitter = emiitter || defaultEmitter; } @@ -89,10 +80,11 @@ export class TransactionHandler { if (await this.checkTransactionPendingStatus(this.pendingTransactions.claim)) { return; } - const bridgeConfig = this.getBridgeConfig(this.chainId); - const estimateGas = await this.veaOutbox.estimateGas.claim(this.epoch, stateRoot, { value: bridgeConfig.deposit }); + const { deposit } = getBridgeConfig(this.chainId); + + const estimateGas = await this.veaOutbox.estimateGas.claim(this.epoch, stateRoot, { value: deposit }); const claimTransaction = await this.veaOutbox.claim(this.epoch, stateRoot, { - value: bridgeConfig.deposit, + value: deposit, gasLimit: estimateGas, }); this.emitter.emit(BotEvents.TXN_MADE, this.epoch, claimTransaction.hash, "Claim"); @@ -107,7 +99,8 @@ export class TransactionHandler { if (await this.checkTransactionPendingStatus(this.pendingTransactions.startVerification)) { return; } - const bridgeConfig = this.getBridgeConfig(this.chainId); + + const bridgeConfig = getBridgeConfig(this.chainId); const timeOver = latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.sequencerDelayLimit - bridgeConfig.epochPeriod; @@ -129,7 +122,7 @@ export class TransactionHandler { if (await this.checkTransactionPendingStatus(this.pendingTransactions.verifySnapshot)) { return; } - const bridgeConfig = this.getBridgeConfig(this.chainId); + const bridgeConfig = getBridgeConfig(this.chainId); const timeLeft = latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.minChallengePeriod; From 7a6aee791f37e9c0bc414d25888b0df41e186288 Mon Sep 17 00:00:00 2001 From: Mani Brar Date: Thu, 2 Jan 2025 18:51:17 +0530 Subject: [PATCH 15/15] fix: log typo --- bridger-cli/src/utils/botEvents.ts | 2 +- bridger-cli/src/utils/logger.ts | 2 +- bridger-cli/src/utils/transactionHandler.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bridger-cli/src/utils/botEvents.ts b/bridger-cli/src/utils/botEvents.ts index 2207ce4a..e4109190 100644 --- a/bridger-cli/src/utils/botEvents.ts +++ b/bridger-cli/src/utils/botEvents.ts @@ -12,7 +12,7 @@ export enum BotEvents { // Claim state CLAIMING = "claiming", CHALLENGER_WON_CLAIM = "challenger_won_claim", - VERFICATION_CANT_START = "verification_cant_started", + VERIFICATION_CANT_START = "verification_cant_started", CANT_VERIFY_SNAPSHOT = "cant_verify_snapshot", CLAIM_CHALLENGED = "claim_challenged", STARTING_VERIFICATION = "starting_verification", diff --git a/bridger-cli/src/utils/logger.ts b/bridger-cli/src/utils/logger.ts index d6dbceb3..7aa60cd6 100644 --- a/bridger-cli/src/utils/logger.ts +++ b/bridger-cli/src/utils/logger.ts @@ -79,7 +79,7 @@ export const configurableInitialize = (emitter: EventEmitter) => { emitter.on(BotEvents.STARTING_VERIFICATION, (epoch: number) => { console.log(`Starting verification for epoch ${epoch}`); }); - emitter.on(BotEvents.VERFICATION_CANT_START, (time) => { + emitter.on(BotEvents.VERIFICATION_CANT_START, (time) => { console.log(`Waiting for sequencer delay to pass to start verification, seconds left: ${time}`); }); diff --git a/bridger-cli/src/utils/transactionHandler.ts b/bridger-cli/src/utils/transactionHandler.ts index 6beeffdd..519aa25c 100644 --- a/bridger-cli/src/utils/transactionHandler.ts +++ b/bridger-cli/src/utils/transactionHandler.ts @@ -105,7 +105,7 @@ export class TransactionHandler { latestBlockTimestamp - this.claim.timestampClaimed - bridgeConfig.sequencerDelayLimit - bridgeConfig.epochPeriod; if (timeOver < 0) { - this.emitter.emit(BotEvents.VERFICATION_CANT_START, -1 * timeOver); + this.emitter.emit(BotEvents.VERIFICATION_CANT_START, -1 * timeOver); return; } const estimateGas = await this.veaOutbox.estimateGas.startVerification(this.epoch, this.claim);