Skip to content

Commit 687935e

Browse files
authored
Merge pull request #435 from kleros/fix/handle-sent-snapshots
Fix/handle sent snapshots
2 parents 159d672 + 996257b commit 687935e

File tree

10 files changed

+183
-26
lines changed

10 files changed

+183
-26
lines changed

validator-cli/README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# bots
1+
# Validator bot
22

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

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

99
`pm2 start`
1010

11-
Runs watcher every minute, and challenges any false claims on the fast bridge receiver.
11+
By default, the watcher performs two core functions:
12+
13+
- Bridger: Submits stored snapshots to the fast bridge receiver.
14+
- Challenger: Challenges any detected invalid claims.
15+
16+
# flags
17+
18+
`--saveSnapshot`
19+
20+
Enables snapshot saving on the inbox when the bot observes a valid state.
21+
22+
`--path=challenger | bridger | both`
23+
24+
- challenger: Only challenge invalid claims
25+
- bridger: Only submit snapshots
26+
- both: Default mode, acts as both challenger and bridger
27+
28+
# Example usage
29+
30+
Run as both challenger and bridger with snapshots enabled:
31+
32+
`pm2 start -- --saveSnapshot`
33+
34+
Run only as challenger:
35+
36+
`pm2 start dist/watcher.js -- --path=challenger`

validator-cli/src/helpers/snapshot.test.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MockEmitter } from "../utils/emitter";
44

55
describe("snapshot", () => {
66
let veaInbox: any;
7+
let veaOutbox: any;
78
let count: number = 1;
89
const chainId = 11155111;
910
let fetchLastSavedMessage: jest.Mock;
@@ -14,8 +15,12 @@ describe("snapshot", () => {
1415
filters: {
1516
SnapshotSaved: jest.fn(),
1617
},
18+
snapshots: jest.fn(),
1719
getAddress: jest.fn().mockResolvedValue("0x1"),
1820
};
21+
veaOutbox = {
22+
stateRoot: jest.fn(),
23+
};
1924
});
2025
describe("isSnapshotNeeded", () => {
2126
it("should return false and updated count when there are no new messages and count is -1 ", () => {
@@ -27,9 +32,10 @@ describe("snapshot", () => {
2732
const params = {
2833
chainId,
2934
veaInbox,
35+
veaOutbox,
3036
count,
3137
fetchLastSavedMessage,
32-
};
38+
} as any;
3339
expect(isSnapshotNeeded(params)).resolves.toEqual({
3440
snapshotNeeded: false,
3541
latestCount: currentCount,
@@ -45,9 +51,10 @@ describe("snapshot", () => {
4551
const params = {
4652
chainId,
4753
veaInbox,
54+
veaOutbox,
4855
count,
4956
fetchLastSavedMessage,
50-
};
57+
} as any;
5158
expect(isSnapshotNeeded(params)).resolves.toEqual({
5259
snapshotNeeded: false,
5360
latestCount: count,
@@ -62,9 +69,10 @@ describe("snapshot", () => {
6269
const params = {
6370
chainId,
6471
veaInbox,
72+
veaOutbox,
6573
count,
6674
fetchLastSavedMessage,
67-
};
75+
} as any;
6876
expect(isSnapshotNeeded(params)).resolves.toEqual({
6977
snapshotNeeded: false,
7078
latestCount: currentCount,
@@ -79,9 +87,10 @@ describe("snapshot", () => {
7987
const params = {
8088
chainId,
8189
veaInbox,
90+
veaOutbox,
8291
count,
8392
fetchLastSavedMessage,
84-
};
93+
} as any;
8594
expect(isSnapshotNeeded(params)).resolves.toEqual({
8695
snapshotNeeded: true,
8796
latestCount: currentCount,
@@ -96,9 +105,30 @@ describe("snapshot", () => {
96105
const params = {
97106
chainId,
98107
veaInbox,
108+
veaOutbox,
99109
count,
100110
fetchLastSavedMessage,
101-
};
111+
} as any;
112+
expect(isSnapshotNeeded(params)).resolves.toEqual({
113+
snapshotNeeded: true,
114+
latestCount: currentCount,
115+
});
116+
});
117+
it.only("should return true if claim was missed in previous epoch", async () => {
118+
count = 1;
119+
let currentCount = 3;
120+
veaInbox.count.mockResolvedValue(currentCount);
121+
fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3");
122+
veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed"));
123+
veaOutbox.stateRoot.mockResolvedValue("0xabcde");
124+
veaInbox.snapshots.mockResolvedValue("0x0");
125+
const params = {
126+
chainId,
127+
veaInbox,
128+
veaOutbox,
129+
count,
130+
fetchLastSavedMessage,
131+
} as any;
102132
expect(isSnapshotNeeded(params)).resolves.toEqual({
103133
snapshotNeeded: true,
104134
latestCount: currentCount,
@@ -119,6 +149,7 @@ describe("snapshot", () => {
119149
const res = await saveSnapshot({
120150
chainId,
121151
veaInbox,
152+
veaOutbox,
122153
network,
123154
epochPeriod,
124155
count,
@@ -146,6 +177,7 @@ describe("snapshot", () => {
146177
const res = await saveSnapshot({
147178
chainId,
148179
veaInbox,
180+
veaOutbox,
149181
network,
150182
epochPeriod,
151183
count,
@@ -172,6 +204,7 @@ describe("snapshot", () => {
172204
const res = await saveSnapshot({
173205
chainId,
174206
veaInbox,
207+
veaOutbox,
175208
network,
176209
epochPeriod,
177210
count: -1,
@@ -200,6 +233,7 @@ describe("snapshot", () => {
200233
const res = await saveSnapshot({
201234
chainId,
202235
veaInbox,
236+
veaOutbox,
203237
network: Network.DEVNET,
204238
epochPeriod,
205239
count,

validator-cli/src/helpers/snapshot.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1+
import { ZeroHash } from "ethers";
12
import { Network, snapshotSavingPeriod } from "../consts/bridgeRoutes";
23
import { getLastMessageSaved } from "../utils/graphQueries";
34
import { BotEvents } from "../utils/botEvents";
45
import { defaultEmitter } from "../utils/emitter";
56
interface SnapshotCheckParams {
7+
epochPeriod: number;
68
chainId: number;
79
veaInbox: any;
10+
veaOutbox: any;
811
count: number;
912
fetchLastSavedMessage?: typeof getLastMessageSaved;
1013
}
1114

1215
export interface SaveSnapshotParams {
1316
chainId: number;
1417
veaInbox: any;
18+
veaOutbox: any;
1519
network: Network;
1620
epochPeriod: number;
1721
count: number;
@@ -24,6 +28,7 @@ export interface SaveSnapshotParams {
2428
export const saveSnapshot = async ({
2529
chainId,
2630
veaInbox,
31+
veaOutbox,
2732
network,
2833
epochPeriod,
2934
count,
@@ -40,8 +45,10 @@ export const saveSnapshot = async ({
4045
return { transactionHandler, latestCount: count };
4146
}
4247
const { snapshotNeeded, latestCount } = await toSaveSnapshot({
48+
epochPeriod,
4349
chainId,
4450
veaInbox,
51+
veaOutbox,
4552
count,
4653
});
4754
if (!snapshotNeeded) return { transactionHandler, latestCount };
@@ -50,28 +57,42 @@ export const saveSnapshot = async ({
5057
};
5158

5259
export const isSnapshotNeeded = async ({
60+
epochPeriod,
5361
chainId,
5462
veaInbox,
63+
veaOutbox,
5564
count,
5665
fetchLastSavedMessage = getLastMessageSaved,
5766
}: SnapshotCheckParams): Promise<{ snapshotNeeded: boolean; latestCount: number }> => {
5867
const currentCount = Number(await veaInbox.count());
68+
5969
if (count == currentCount) {
6070
return { snapshotNeeded: false, latestCount: currentCount };
6171
}
6272
let lastSavedCount: number;
73+
let lastSavedSnapshot: string;
6374
try {
6475
const saveSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSaved());
6576
lastSavedCount = Number(saveSnapshotLogs[saveSnapshotLogs.length - 1].args[2]);
77+
lastSavedSnapshot = saveSnapshotLogs[saveSnapshotLogs.length - 1].args[1];
6678
} catch {
6779
const veaInboxAddress = await veaInbox.getAddress();
68-
const lastSavedMessageId = await fetchLastSavedMessage(veaInboxAddress, chainId);
80+
const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage(
81+
veaInboxAddress,
82+
chainId
83+
);
6984
const messageIndex = extractMessageIndex(lastSavedMessageId);
85+
lastSavedSnapshot = lastSavedStateRoot;
7086
// adding 1 to the message index to get the last saved count
7187
lastSavedCount = messageIndex;
7288
}
89+
const epochNow = Math.floor(Date.now() / (1000 * epochPeriod));
90+
const currentSnapshot = await veaInbox.snapshots(epochNow);
91+
const currentStateRoot = await veaOutbox.stateRoot();
7392
if (currentCount > lastSavedCount) {
7493
return { snapshotNeeded: true, latestCount: currentCount };
94+
} else if (currentSnapshot == ZeroHash && lastSavedSnapshot != currentStateRoot) {
95+
return { snapshotNeeded: true, latestCount: currentCount };
7596
}
7697
return { snapshotNeeded: false, latestCount: currentCount };
7798
};

validator-cli/src/helpers/validator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ export async function challengeAndResolveClaim({
7474
} else {
7575
transactionHandler.claim = claim;
7676
}
77+
// If claim is already resolved, nothing to do
78+
if (claim.honest !== 0) {
79+
emitter.emit(BotEvents.CLAIM_ALREADY_RESOLVED, epoch);
80+
if (claim.honest === 2) {
81+
await transactionHandler.withdrawChallengeDeposit();
82+
return transactionHandler;
83+
}
84+
return null;
85+
}
7786

7887
const { challenged, toRelay } = await challengeAndCheckRelay({
7988
veaInbox,
@@ -94,6 +103,7 @@ export async function challengeAndResolveClaim({
94103
claim,
95104
veaInbox,
96105
veaInboxProvider,
106+
veaOutbox,
97107
queryRpc,
98108
ethBlockTag,
99109
transactionHandler,
@@ -141,6 +151,7 @@ interface ResolveFlowParams {
141151
claim: ClaimStruct;
142152
veaInbox: any;
143153
veaInboxProvider: JsonRpcProvider;
154+
veaOutbox: any;
144155
queryRpc: JsonRpcProvider;
145156
ethBlockTag: "latest" | "finalized";
146157
transactionHandler: ITransactionHandler;
@@ -154,6 +165,7 @@ async function handleResolveFlow({
154165
claim,
155166
veaInbox,
156167
veaInboxProvider,
168+
veaOutbox,
157169
queryRpc,
158170
ethBlockTag,
159171
transactionHandler,
@@ -165,6 +177,7 @@ async function handleResolveFlow({
165177
chainId,
166178
veaInbox,
167179
veaInboxProvider,
180+
veaOutbox,
168181
veaOutboxProvider: queryRpc,
169182
epoch,
170183
fromBlock: blockNumberOutboxLowerBound,
@@ -175,7 +188,6 @@ async function handleResolveFlow({
175188
await transactionHandler.sendSnapshot();
176189
return;
177190
}
178-
179191
const execStatus = claimResolveState.execution.status;
180192
if (execStatus === 1) {
181193
await transactionHandler.resolveChallengedClaim(claimResolveState.sendSnapshot.txHash);

validator-cli/src/utils/botEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export enum BotEvents {
3232
WITHDRAWING_CHALLENGE_DEPOSIT = "withdrawing_challenge_deposit",
3333
WITHDRAWING_CLAIM_DEPOSIT = "withdrawing_claim_deposit",
3434
WAITING_ARB_TIMEOUT = "waiting_arb_timeout",
35+
CLAIM_ALREADY_RESOLVED = "claim_already_resolved",
3536

3637
// Devnet state
3738
ADV_DEVNET = "advance_devnet",

validator-cli/src/utils/claim.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ethers } from "ethers";
1+
import { ethers, getAddress } from "ethers";
22
import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth";
33
import { getClaim, hashClaim, getClaimResolveState, ClaimResolveStateParams } from "./claim";
44
import { ClaimNotFoundError } from "./errors";
@@ -232,8 +232,8 @@ describe("snapshotClaim", () => {
232232

233233
describe("getClaimResolveState", () => {
234234
let veaInbox: any;
235-
let veaInboxProvider: any;
236-
let veaOutboxProvider: any;
235+
let veaOutbox: any;
236+
let fetchSentSnapshotData: any;
237237
const epoch = 1;
238238
const blockNumberOutboxLowerBound = 1234;
239239
const toBlock = "latest";
@@ -253,10 +253,17 @@ describe("snapshotClaim", () => {
253253
filters: {
254254
SnapshotSent: jest.fn(),
255255
},
256+
getAddress: jest.fn(),
256257
};
258+
veaOutbox = {
259+
claimHashes: jest.fn().mockResolvedValueOnce(hashedMockClaim),
260+
getAddress: jest.fn(),
261+
};
262+
fetchSentSnapshotData = jest.fn().mockResolvedValueOnce(hashedMockClaim);
257263
mockClaimResolveStateParams = {
258264
chainId: 11155111,
259265
veaInbox,
266+
veaOutbox,
260267
veaInboxProvider: {
261268
getBlock: jest.fn().mockResolvedValueOnce({ timestamp: mockClaim.timestampClaimed, number: 1234 }),
262269
} as any,
@@ -267,6 +274,7 @@ describe("snapshotClaim", () => {
267274
fromBlock: blockNumberOutboxLowerBound,
268275
toBlock,
269276
fetchMessageStatus: jest.fn(),
277+
fetchSentSnapshotData,
270278
};
271279
});
272280

@@ -287,5 +295,15 @@ describe("snapshotClaim", () => {
287295
expect(claimResolveState.sendSnapshot.status).toBeTruthy();
288296
expect(claimResolveState.execution.status).toBe(0);
289297
});
298+
299+
it("should return false state if incorrect snapshot sent", async () => {
300+
veaInbox.queryFilter.mockResolvedValueOnce([{ transactionHash: "0x1234" }]);
301+
fetchSentSnapshotData = jest.fn().mockResolvedValueOnce("0xincorrecthash");
302+
mockClaimResolveStateParams.fetchSentSnapshotData = fetchSentSnapshotData;
303+
const claimResolveState = await getClaimResolveState(mockClaimResolveStateParams);
304+
expect(claimResolveState).toBeDefined();
305+
expect(claimResolveState.sendSnapshot.status).toBeFalsy();
306+
expect(claimResolveState.execution.status).toBe(0);
307+
});
290308
});
291309
});

0 commit comments

Comments
 (0)