Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/bridger-cli #370

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
12 changes: 12 additions & 0 deletions bridger-cli/.env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
PRIVATE_KEY=
VEAOUTBOX_CHAIN_ID=11155111

VEAINBOX_ADDRESS=0xE12daFE59Bc3A996362d54b37DFd2BA9279cAd06
VEAINBOX_PROVIDER=http://localhost:8545

VEAOUTBOX_ADDRESS=0x209BFdC6B7c66b63A8382196Ba3d06619d0F12c9
VEAOUTBOX_PROVIDER=http://localhost:8546

# Ex: 85918/outbox-arb-sep-sep-testnet-vs/version/latest
VEAINBOX_SUBGRAPH=
VEAOUTBOX_SUBGRAPH=
28 changes: 28 additions & 0 deletions bridger-cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@kleros/bridger-cli",
"license": "MIT",
"packageManager": "[email protected]",
"engines": {
"node": ">=18.0.0"
},
"volta": {
"node": "18.20.3",
"yarn": "4.2.2"
},
"scripts": {
"start-bridger": "npx ts-node ./src/bridger.ts",
"test": "mocha --timeout 10000 --import=tsx src/utils/**/*.test.ts --exit"
},
"dependencies": {
"@kleros/vea-contracts": "workspace:^",
"@typechain/ethers-v5": "^10.2.0",
"dotenv": "^16.4.5",
"typescript": "^4.9.5",
"web3": "^1.10.4"
},
"devDependencies": {
"@types/chai": "^5",
"chai": "^5.1.2",
"mocha": "^11.0.1"
}
}
204 changes: 204 additions & 0 deletions bridger-cli/src/bridger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
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) => {
console.log("Starting bridger");
const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID);
const bridgeConfig = getBridgeConfig(chainId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle undefined bridgeConfig when chainId is unsupported

The getBridgeConfig function may return undefined if the provided chainId is not supported. Without checking for this, subsequent code may throw errors. Ensure that bridgeConfig is validated before use.

Apply this diff to add a check for bridgeConfig:

const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID);
const bridgeConfig = getBridgeConfig(chainId);
+ if (!bridgeConfig) {
+   throw new Error(`Unsupported chainId: ${chainId}`);
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID);
const bridgeConfig = getBridgeConfig(chainId);
const chainId = Number(process.env.VEAOUTBOX_CHAIN_ID);
const bridgeConfig = getBridgeConfig(chainId);
if (!bridgeConfig) {
throw new Error(`Unsupported chainId: ${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 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ignoreable) Tests can be easier if you extract the interior content of the while into a function. If you do so, you have a function that just runs the processing once. If do such extraction into a function, in the tests you will only have to worry about setting up the environment and calling the processing once. That saves you from having to worry about mocking/configuring the shut down, setting timeouts, etc.

In the current function shape this feedback is not immediately applicable, but after breaking it in small functions it will be way easier to do so. Here is a zodiac example. Hope it makes sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes agreed 👍. Won't have to create racing conditions to test the bot after this.

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 if (!claimData.challenged) {
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 {
console.log("Claim was challenged, skipping");
}
} 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ignoreable) if you provide the wait as a dependency, you can have total control of when the loop cycles from the tests. No action expected from this comment.

}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for asynchronous operations in the watch function

The watch function performs several asynchronous operations without adequate error handling. If any of these operations fail, the application may crash due to unhandled promise rejections. Incorporate try...catch blocks to gracefully handle errors and maintain application stability.

Apply this diff to add error handling (partial example):

export const watch = async (shutDownSignal: ShutdownSignal = new ShutdownSignal(), startEpoch: number = 24) => {
+ try {
  console.log("Starting bridger");
  // ... existing code ...

  while (!shutDownSignal.getIsShutdownSignal()) {
+   try {
      // ... existing while loop code ...
+   } catch (error) {
+     console.error(`Error processing epochs: ${error.message}`);
+     // Handle error (e.g., log, retry, or exit loop)
+   }
  }

+ } catch (error) {
+   console.error(`Error in watch function: ${error.message}`);
+   // Handle error (e.g., cleanup, notify)
+ }
};

Committable suggestion skipped: line range outside the PR's diff.


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);
}
29 changes: 29 additions & 0 deletions bridger-cli/src/consts/bridgeRoutes.ts
Original file line number Diff line number Diff line change
@@ -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,
},
};
mani99brar marked this conversation as resolved.
Show resolved Hide resolved

const getBridgeConfig = (chainId: number): IBridge | undefined => {
return bridges[chainId];
};
mani99brar marked this conversation as resolved.
Show resolved Hide resolved

export { getBridgeConfig };
Loading
Loading