-
Notifications
You must be signed in to change notification settings - Fork 6
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
base: dev
Are you sure you want to change the base?
feat/bridger-cli #370
Changes from 7 commits
d29ae84
cabab3f
310184a
2bfba2d
f779e6b
84b65d6
2416bcb
9dd90cc
382681d
1f0ab5f
97d6777
64427bb
4690526
8e81840
2d8a218
590f6ff
7a6aee7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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= |
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" | ||
} | ||
} |
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); | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
} | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for asynchronous operations in the The 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)
+ }
};
|
||
|
||
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); | ||
} |
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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle undefined
bridgeConfig
whenchainId
is unsupportedThe
getBridgeConfig
function may returnundefined
if the providedchainId
is not supported. Without checking for this, subsequent code may throw errors. Ensure thatbridgeConfig
is validated before use.Apply this diff to add a check for
bridgeConfig
:📝 Committable suggestion