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({
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;
}
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