Thanks to visit codestin.com
Credit goes to github.com

Skip to content
29 changes: 27 additions & 2 deletions validator-cli/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# bots
# Validator bot

A collection of bots for the Vea challenger and bridger ecosystem.

Expand All @@ -8,4 +8,29 @@ A collection of bots for the Vea challenger and bridger ecosystem.

`pm2 start`

Runs watcher every minute, and challenges any false claims on the fast bridge receiver.
By default, the watcher performs two core functions:

- Bridger: Submits stored snapshots to the fast bridge receiver.
- Challenger: Challenges any detected invalid claims.

# flags

`--saveSnapshot`

Enables snapshot saving on the inbox when the bot observes a valid state.

`--path=challenger | bridger | both`

- challenger: Only challenge invalid claims
- bridger: Only submit snapshots
- both: Default mode, acts as both challenger and bridger

# Example usage

Run as both challenger and bridger with snapshots enabled:

`pm2 start -- --saveSnapshot`

Run only as challenger:

`pm2 start dist/watcher.js -- --path=challenger`
18 changes: 12 additions & 6 deletions validator-cli/src/consts/bridgeRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import veaOutboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/chiado
import veaInboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisTestnet.json";
import veaOutboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json";
import veaRouterArbToGnosisTestnet from "@kleros/vea-contracts/deployments/sepolia/RouterArbToGnosisTestnet.json";
interface Bridge {
export interface Bridge {
chain: string;
minChallengePeriod: number;
sequencerDelayLimit: number;
Expand All @@ -30,7 +30,7 @@ type RouteConfigs = {
deposit: bigint;
};

export enum Network {
enum Network {
DEVNET = "devnet",
TESTNET = "testnet",
}
Expand All @@ -39,7 +39,7 @@ const arbToEthConfigs: { [key in Network]: RouteConfigs } = {
[Network.DEVNET]: {
veaInbox: veaInboxArbToEthDevnet,
veaOutbox: veaOutboxArbToEthDevnet,
epochPeriod: 1800,
epochPeriod: 300,
deposit: BigInt("1000000000000000000"),
},
[Network.TESTNET]: {
Expand All @@ -54,7 +54,7 @@ const arbToGnosisConfigs: { [key in Network]: RouteConfigs } = {
[Network.DEVNET]: {
veaInbox: veaInboxArbToGnosisDevnet,
veaOutbox: veaOutboxArbToGnosisDevnet,
epochPeriod: 1800,
epochPeriod: 300,
deposit: BigInt("100000000000000000"),
},
[Network.TESTNET]: {
Expand All @@ -66,7 +66,7 @@ const arbToGnosisConfigs: { [key in Network]: RouteConfigs } = {
},
};

export const bridges: { [chainId: number]: Bridge } = {
const bridges: { [chainId: number]: Bridge } = {
11155111: {
chain: "sepolia",
minChallengePeriod: 10800,
Expand All @@ -87,10 +87,16 @@ export const bridges: { [chainId: number]: Bridge } = {
},
};

// For the remaining time in an epoch the bot should save snapshots
const snapshotSavingPeriod = {
[Network.DEVNET]: 90, // 1m 30s
[Network.TESTNET]: 600, // 10 mins
};

const getBridgeConfig = (chainId: number): Bridge => {
const bridge = bridges[chainId];
if (!bridge) throw new Error(`Bridge not found for chain`);
return bridges[chainId];
};

export { getBridgeConfig, Bridge };
export { bridges, getBridgeConfig, Network, snapshotSavingPeriod };
51 changes: 43 additions & 8 deletions validator-cli/src/helpers/snapshot.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Network } from "../consts/bridgeRoutes";
import { Network, snapshotSavingPeriod } from "../consts/bridgeRoutes";
import { isSnapshotNeeded, saveSnapshot } from "./snapshot";
import { MockEmitter } from "../utils/emitter";

describe("snapshot", () => {
let veaInbox: any;
let veaOutbox: any;
let count: number = 1;
const chainId = 11155111;
let fetchLastSavedMessage: jest.Mock;
Expand All @@ -14,8 +15,12 @@ describe("snapshot", () => {
filters: {
SnapshotSaved: jest.fn(),
},
snapshots: jest.fn(),
getAddress: jest.fn().mockResolvedValue("0x1"),
};
veaOutbox = {
stateRoot: jest.fn(),
};
});
describe("isSnapshotNeeded", () => {
it("should return false and updated count when there are no new messages and count is -1 ", () => {
Expand All @@ -27,9 +32,10 @@ describe("snapshot", () => {
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
};
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
snapshotNeeded: false,
latestCount: currentCount,
Expand All @@ -45,9 +51,10 @@ describe("snapshot", () => {
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
};
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
snapshotNeeded: false,
latestCount: count,
Expand All @@ -62,9 +69,10 @@ describe("snapshot", () => {
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
};
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
snapshotNeeded: false,
latestCount: currentCount,
Expand All @@ -79,9 +87,10 @@ describe("snapshot", () => {
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
};
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
snapshotNeeded: true,
latestCount: currentCount,
Expand All @@ -96,9 +105,30 @@ describe("snapshot", () => {
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
};
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
snapshotNeeded: true,
latestCount: currentCount,
});
});
it.only("should return true if claim was missed in previous epoch", async () => {
count = 1;
let currentCount = 3;
veaInbox.count.mockResolvedValue(currentCount);
fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3");
veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed"));
veaOutbox.stateRoot.mockResolvedValue("0xabcde");
veaInbox.snapshots.mockResolvedValue("0x0");
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
Comment on lines +117 to 132
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 | 🔴 Critical

🧩 Analysis chain

Remove it.only to avoid running a single test only.

This will cripple the test suite.

-it.only("should return true if claim was missed in previous epoch", async () => {
+it("should return true if claim was missed in previous epoch", async () => {

Run this to detect any lingering “only” in tests:


🏁 Script executed:

#!/bin/bash
rg -nP '\b(it|describe|test)\.only\(' -g 'validator-cli/**' -S

Length of output: 132


Remove it.only() from test at line 117 to restore full test suite execution.

The it.only() prevents other tests from running and will break the test suite. Change line 117 from it.only(...) to it(...).

-it.only("should return true if claim was missed in previous epoch", async () => {
+it("should return true if claim was missed in previous epoch", async () => {
📝 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
it.only("should return true if claim was missed in previous epoch", async () => {
count = 1;
let currentCount = 3;
veaInbox.count.mockResolvedValue(currentCount);
fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3");
veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed"));
veaOutbox.stateRoot.mockResolvedValue("0xabcde");
veaInbox.snapshots.mockResolvedValue("0x0");
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
it("should return true if claim was missed in previous epoch", async () => {
count = 1;
let currentCount = 3;
veaInbox.count.mockResolvedValue(currentCount);
fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3");
veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed"));
veaOutbox.stateRoot.mockResolvedValue("0xabcde");
veaInbox.snapshots.mockResolvedValue("0x0");
const params = {
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage,
} as any;
expect(isSnapshotNeeded(params)).resolves.toEqual({
🤖 Prompt for AI Agents
In validator-cli/src/helpers/snapshot.test.ts around lines 117 to 132, the test
is marked with it.only which prevents the rest of the test suite from running;
change the test declaration from it.only(...) to it(...) so the test runs
normally with the full suite, save the file, and run the test runner to verify
all tests execute.

snapshotNeeded: true,
latestCount: currentCount,
Expand All @@ -119,6 +149,7 @@ describe("snapshot", () => {
const res = await saveSnapshot({
chainId,
veaInbox,
veaOutbox,
network,
epochPeriod,
count,
Expand Down Expand Up @@ -146,6 +177,7 @@ describe("snapshot", () => {
const res = await saveSnapshot({
chainId,
veaInbox,
veaOutbox,
network,
epochPeriod,
count,
Expand All @@ -172,6 +204,7 @@ describe("snapshot", () => {
const res = await saveSnapshot({
chainId,
veaInbox,
veaOutbox,
network,
epochPeriod,
count: -1,
Expand All @@ -184,21 +217,23 @@ describe("snapshot", () => {
expect(res).toEqual({ transactionHandler, latestCount: currentCount });
});

it("should save snapshot if snapshot is needed at anytime for devnet", async () => {
it("should save snapshot in time limit for devnet", async () => {
const savingPeriod = snapshotSavingPeriod[Network.DEVNET];
const currentCount = 6;
count = -1;
veaInbox.count.mockResolvedValue(currentCount);
const isSnapshotNeededMock = jest.fn().mockResolvedValue({
snapshotNeeded: true,
latestCount: currentCount,
});
const now = 1801; // 600 seconds after the epoch started
const now = epochPeriod + epochPeriod - savingPeriod; // 60 seconds before the second epoch ends
const transactionHandler = {
saveSnapshot: jest.fn(),
};
const res = await saveSnapshot({
chainId,
veaInbox,
veaOutbox,
network: Network.DEVNET,
epochPeriod,
count,
Expand Down
42 changes: 30 additions & 12 deletions validator-cli/src/helpers/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Network } from "../consts/bridgeRoutes";
import { ZeroHash } from "ethers";
import { Network, snapshotSavingPeriod } from "../consts/bridgeRoutes";
import { getLastMessageSaved } from "../utils/graphQueries";
import { BotEvents } from "../utils/botEvents";
import { defaultEmitter } from "../utils/emitter";

interface SnapshotCheckParams {
epochPeriod: number;
chainId: number;
veaInbox: any;
veaOutbox: any;
count: number;
fetchLastSavedMessage?: typeof getLastMessageSaved;
}

export interface SaveSnapshotParams {
chainId: number;
veaInbox: any;
veaOutbox: any;
network: Network;
epochPeriod: number;
count: number;
Expand All @@ -25,6 +28,7 @@ export interface SaveSnapshotParams {
export const saveSnapshot = async ({
chainId,
veaInbox,
veaOutbox,
network,
epochPeriod,
count,
Expand All @@ -33,18 +37,18 @@ export const saveSnapshot = async ({
toSaveSnapshot = isSnapshotNeeded,
now = Math.floor(Date.now() / 1000),
}: SaveSnapshotParams): Promise<any> => {
if (network != Network.DEVNET) {
const timeElapsed = now % epochPeriod;
const timeLeftForEpoch = epochPeriod - timeElapsed;
// Saving snapshots in last 10 minutes of the epoch on testnet
if (timeLeftForEpoch > 600) {
emitter.emit(BotEvents.SNAPSHOT_WAITING, timeLeftForEpoch);
return { transactionHandler, latestCount: count };
}
const timeElapsed = now % epochPeriod;
const timeLeftForEpoch = epochPeriod - timeElapsed;

if (timeLeftForEpoch > snapshotSavingPeriod[network]) {
emitter.emit(BotEvents.SNAPSHOT_WAITING, timeLeftForEpoch);
return { transactionHandler, latestCount: count };
}
const { snapshotNeeded, latestCount } = await toSaveSnapshot({
epochPeriod,
chainId,
veaInbox,
veaOutbox,
count,
});
if (!snapshotNeeded) return { transactionHandler, latestCount };
Expand All @@ -53,28 +57,42 @@ export const saveSnapshot = async ({
};

export const isSnapshotNeeded = async ({
epochPeriod,
chainId,
veaInbox,
veaOutbox,
count,
fetchLastSavedMessage = getLastMessageSaved,
}: SnapshotCheckParams): Promise<{ snapshotNeeded: boolean; latestCount: number }> => {
const currentCount = Number(await veaInbox.count());

if (count == currentCount) {
return { snapshotNeeded: false, latestCount: currentCount };
}
let lastSavedCount: number;
let lastSavedSnapshot: string;
try {
const saveSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSaved());
lastSavedCount = Number(saveSnapshotLogs[saveSnapshotLogs.length - 1].args[2]);
lastSavedSnapshot = saveSnapshotLogs[saveSnapshotLogs.length - 1].args[1];
} catch {
const veaInboxAddress = await veaInbox.getAddress();
const lastSavedMessageId = await fetchLastSavedMessage(veaInboxAddress, chainId);
const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage(
veaInboxAddress,
chainId
);
const messageIndex = extractMessageIndex(lastSavedMessageId);
lastSavedSnapshot = lastSavedStateRoot;
// adding 1 to the message index to get the last saved count
lastSavedCount = messageIndex + 1;
lastSavedCount = messageIndex;
}
Comment on lines +73 to 88
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 | 🔴 Critical

🧩 Analysis chain

Handle empty Graph response and align comment with logic.

  • If fetchLastSavedMessage returns null/undefined, destructuring throws. Provide safe defaults (lastSavedCount = 0, lastSavedSnapshot = ZeroHash).
  • The comment says “adding 1 to the message index,” but code doesn’t add 1. Either remove the note or implement the +1 if correct.

Apply this diff:

   } catch {
     const veaInboxAddress = await veaInbox.getAddress();
-    const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage(
-      veaInboxAddress,
-      chainId
-    );
-    const messageIndex = extractMessageIndex(lastSavedMessageId);
-    lastSavedSnapshot = lastSavedStateRoot;
-    // adding 1 to the message index to get the last saved count
-    lastSavedCount = messageIndex;
+    const lastSaved = await fetchLastSavedMessage(veaInboxAddress, chainId);
+    if (!lastSaved) {
+      lastSavedSnapshot = ZeroHash;
+      lastSavedCount = 0;
+    } else {
+      const messageIndex = extractMessageIndex(lastSaved.id);
+      lastSavedSnapshot = lastSaved.stateRoot;
+      // If the index is zero-based, keep as-is; otherwise, add +1.
+      lastSavedCount = messageIndex; // update comment accordingly if needed
+    }
   }

Confirm whether the graph’s message index is zero- or one-based so we can fix the comment or add +1 appropriately.


I need to verify the concerns raised in the review comment. Let me explore the codebase to understand the context.


🏁 Script executed:

#!/bin/bash
# Find and display the snapshot.ts file content around the target lines
fd snapshot.ts --type f | head -5

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Look for fetchLastSavedMessage function definition
rg -n "fetchLastSavedMessage" --type ts -A 10 -B 2

Length of output: 12707


🏁 Script executed:

#!/bin/bash
# Search for ZeroHash constant
rg -n "ZeroHash" --type ts -B 2 -A 2

Length of output: 5106


🏁 Script executed:

#!/bin/bash
# Search for tests related to snapshot
fd -e test.ts -e spec.ts | xargs grep -l "snapshot" 2>/dev/null | head -3

Length of output: 128


🏁 Script executed:

#!/bin/bash
# Check for Graph-related imports and message index usage
rg -n "messageIndex\|message.*index" --type ts -C 3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find getLastMessageSaved implementation
rg -n "getLastMessageSaved" --type ts -A 15 -B 2

Length of output: 5164


🏁 Script executed:

#!/bin/bash
# Find extractMessageIndex implementation
rg -n "extractMessageIndex" --type ts -A 10 -B 2

Length of output: 2109


🏁 Script executed:

#!/bin/bash
# Check graphQueries file
fd graphQueries --type f

Length of output: 39


Fix unsafe destructuring and misleading comment in catch block.

The code at lines 80-88 has two issues:

  1. Unsafe destructuring: getLastMessageSaved can return undefined when the graph query returns fewer than 2 snapshots or no messages, but the current code destructures without checking for null, which will throw an error.

  2. Misleading comment: The comment at line 86 states "adding 1 to the message index" but the code assigns lastSavedCount = messageIndex directly without incrementing.

Apply the suggested diff to add a null check and provide safe defaults (using the already-imported ZeroHash constant and initializing count to 0). Update the comment to reflect the actual logic or clarify if +1 is needed based on message index semantics.

const epochNow = Math.floor(Date.now() / (1000 * epochPeriod));
const currentSnapshot = await veaInbox.snapshots(epochNow);
const currentStateRoot = await veaOutbox.stateRoot();
if (currentCount > lastSavedCount) {
return { snapshotNeeded: true, latestCount: currentCount };
} else if (currentSnapshot == ZeroHash && lastSavedSnapshot != currentStateRoot) {
return { snapshotNeeded: true, latestCount: currentCount };
}
return { snapshotNeeded: false, latestCount: currentCount };
};
Expand Down
Loading
Loading