Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
44 changes: 39 additions & 5 deletions validator-cli/src/helpers/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 Down Expand Up @@ -200,6 +233,7 @@ describe("snapshot", () => {
const res = await saveSnapshot({
chainId,
veaInbox,
veaOutbox,
network: Network.DEVNET,
epochPeriod,
count,
Expand Down
23 changes: 22 additions & 1 deletion validator-cli/src/helpers/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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 @@ -24,6 +28,7 @@ export interface SaveSnapshotParams {
export const saveSnapshot = async ({
chainId,
veaInbox,
veaOutbox,
network,
epochPeriod,
count,
Expand All @@ -40,8 +45,10 @@ export const saveSnapshot = async ({
return { transactionHandler, latestCount: count };
}
const { snapshotNeeded, latestCount } = await toSaveSnapshot({
epochPeriod,
chainId,
veaInbox,
veaOutbox,
count,
});
if (!snapshotNeeded) return { transactionHandler, latestCount };
Expand All @@ -50,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;
}
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
14 changes: 13 additions & 1 deletion validator-cli/src/helpers/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ export async function challengeAndResolveClaim({
} else {
transactionHandler.claim = claim;
}
// If claim is already resolved, nothing to do
if (claim.honest !== 0) {
emitter.emit(BotEvents.CLAIM_ALREADY_RESOLVED, epoch);
if (claim.honest === 2) {
await transactionHandler.withdrawChallengeDeposit();
return transactionHandler;
}
return null;
}

const { challenged, toRelay } = await challengeAndCheckRelay({
veaInbox,
Expand All @@ -94,6 +103,7 @@ export async function challengeAndResolveClaim({
claim,
veaInbox,
veaInboxProvider,
veaOutbox,
queryRpc,
ethBlockTag,
transactionHandler,
Expand Down Expand Up @@ -141,6 +151,7 @@ interface ResolveFlowParams {
claim: ClaimStruct;
veaInbox: any;
veaInboxProvider: JsonRpcProvider;
veaOutbox: any;
queryRpc: JsonRpcProvider;
ethBlockTag: "latest" | "finalized";
transactionHandler: ITransactionHandler;
Expand All @@ -154,6 +165,7 @@ async function handleResolveFlow({
claim,
veaInbox,
veaInboxProvider,
veaOutbox,
queryRpc,
ethBlockTag,
transactionHandler,
Expand All @@ -165,6 +177,7 @@ async function handleResolveFlow({
chainId,
veaInbox,
veaInboxProvider,
veaOutbox,
veaOutboxProvider: queryRpc,
epoch,
fromBlock: blockNumberOutboxLowerBound,
Expand All @@ -175,7 +188,6 @@ async function handleResolveFlow({
await transactionHandler.sendSnapshot();
return;
}

const execStatus = claimResolveState.execution.status;
if (execStatus === 1) {
await transactionHandler.resolveChallengedClaim(claimResolveState.sendSnapshot.txHash);
Expand Down
1 change: 1 addition & 0 deletions validator-cli/src/utils/botEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum BotEvents {
WITHDRAWING_CHALLENGE_DEPOSIT = "withdrawing_challenge_deposit",
WITHDRAWING_CLAIM_DEPOSIT = "withdrawing_claim_deposit",
WAITING_ARB_TIMEOUT = "waiting_arb_timeout",
CLAIM_ALREADY_RESOLVED = "claim_already_resolved",

// Devnet state
ADV_DEVNET = "advance_devnet",
Expand Down
24 changes: 21 additions & 3 deletions validator-cli/src/utils/claim.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ethers } from "ethers";
import { ethers, getAddress } from "ethers";
import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth";
import { getClaim, hashClaim, getClaimResolveState, ClaimResolveStateParams } from "./claim";
import { ClaimNotFoundError } from "./errors";
Expand Down Expand Up @@ -232,8 +232,8 @@ describe("snapshotClaim", () => {

describe("getClaimResolveState", () => {
let veaInbox: any;
let veaInboxProvider: any;
let veaOutboxProvider: any;
let veaOutbox: any;
let fetchSentSnapshotData: any;
const epoch = 1;
const blockNumberOutboxLowerBound = 1234;
const toBlock = "latest";
Expand All @@ -253,10 +253,17 @@ describe("snapshotClaim", () => {
filters: {
SnapshotSent: jest.fn(),
},
getAddress: jest.fn(),
};
veaOutbox = {
claimHashes: jest.fn().mockResolvedValueOnce(hashedMockClaim),
getAddress: jest.fn(),
};
fetchSentSnapshotData = jest.fn().mockResolvedValueOnce(hashedMockClaim);
mockClaimResolveStateParams = {
chainId: 11155111,
veaInbox,
veaOutbox,
veaInboxProvider: {
getBlock: jest.fn().mockResolvedValueOnce({ timestamp: mockClaim.timestampClaimed, number: 1234 }),
} as any,
Expand All @@ -267,6 +274,7 @@ describe("snapshotClaim", () => {
fromBlock: blockNumberOutboxLowerBound,
toBlock,
fetchMessageStatus: jest.fn(),
fetchSentSnapshotData,
};
});

Expand All @@ -287,5 +295,15 @@ describe("snapshotClaim", () => {
expect(claimResolveState.sendSnapshot.status).toBeTruthy();
expect(claimResolveState.execution.status).toBe(0);
});

it("should return false state if incorrect snapshot sent", async () => {
veaInbox.queryFilter.mockResolvedValueOnce([{ transactionHash: "0x1234" }]);
fetchSentSnapshotData = jest.fn().mockResolvedValueOnce("0xincorrecthash");
mockClaimResolveStateParams.fetchSentSnapshotData = fetchSentSnapshotData;
const claimResolveState = await getClaimResolveState(mockClaimResolveStateParams);
expect(claimResolveState).toBeDefined();
expect(claimResolveState.sendSnapshot.status).toBeFalsy();
expect(claimResolveState.execution.status).toBe(0);
});
});
});
Loading
Loading