diff --git a/validator-cli/src/ArbToEth/transactionHandler.test.ts b/validator-cli/src/ArbToEth/transactionHandler.test.ts index b8334793..a094baff 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.test.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.test.ts @@ -10,6 +10,7 @@ import { MockEmitter, defaultEmitter } from "../utils/emitter"; import { BotEvents } from "../utils/botEvents"; import { ClaimNotSetError } from "../utils/errors"; import { getBridgeConfig, Network } from "../consts/bridgeRoutes"; +import { saveSnapshot } from "src/utils/snapshot"; describe("ArbToEthTransactionHandler", () => { const chainId = 11155111; @@ -39,6 +40,7 @@ describe("ArbToEthTransactionHandler", () => { }; veaInbox = { sendSnapshot: jest.fn(), + saveSnapshot: jest.fn(), }; claim = { stateRoot: "0x1234", @@ -145,6 +147,35 @@ describe("ArbToEthTransactionHandler", () => { }); }); + describe("saveSnapshot", () => { + let transactionHandler: ArbToEthTransactionHandler; + beforeEach(() => { + transactionHandler = new ArbToEthTransactionHandler(transactionHandlerParams); + }); + + it("should save snapshot and set pending saveSnapshotTxn", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(0); + veaInbox.saveSnapshot.mockResolvedValue({ hash: "0x1234" }); + await transactionHandler.saveSnapshot(); + expect(veaInbox.saveSnapshot).toHaveBeenCalled(); + expect(transactionHandler.transactions.saveSnapshotTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: expect.any(Number), + }); + }); + + it("should not save snapshot if a saveSnapshot transaction is pending", async () => { + jest.spyOn(transactionHandler, "checkTransactionStatus").mockResolvedValue(1); + transactionHandler.transactions.saveSnapshotTxn = { hash: "0x1234", broadcastedTimestamp: 1000 }; + await transactionHandler.saveSnapshot(); + expect(veaInbox.saveSnapshot).not.toHaveBeenCalled(); + expect(transactionHandler.transactions.saveSnapshotTxn).toEqual({ + hash: "0x1234", + broadcastedTimestamp: 1000, + }); + }); + }); + // Happy path (claimer) describe("makeClaim", () => { let transactionHandler: ArbToEthTransactionHandler; diff --git a/validator-cli/src/ArbToEth/transactionHandler.ts b/validator-cli/src/ArbToEth/transactionHandler.ts index 4635ca7d..c4e2e667 100644 --- a/validator-cli/src/ArbToEth/transactionHandler.ts +++ b/validator-cli/src/ArbToEth/transactionHandler.ts @@ -43,6 +43,7 @@ export type Transactions = { verifySnapshotTxn: Transaction | null; challengeTxn: Transaction | null; withdrawChallengeDepositTxn: Transaction | null; + saveSnapshotTxn: Transaction | null; sendSnapshotTxn: Transaction | null; executeSnapshotTxn: Transaction | null; devnetAdvanceStateTxn?: Transaction | null; @@ -82,6 +83,7 @@ export class ArbToEthTransactionHandler { verifySnapshotTxn: null, challengeTxn: null, withdrawChallengeDepositTxn: null, + saveSnapshotTxn: null, sendSnapshotTxn: null, executeSnapshotTxn: null, }; @@ -257,7 +259,7 @@ export class ArbToEthTransactionHandler { } /** - * Withdraw the claim deposit. + * Withdraw the claim deposit from VeaOutbox(ETH). * */ public async withdrawClaimDeposit() { @@ -335,7 +337,7 @@ export class ArbToEthTransactionHandler { } /** - * Withdraw the challenge deposit. + * Withdraw the challenge deposit from VeaOutbox(ETH). * */ public async withdrawChallengeDeposit() { @@ -360,6 +362,28 @@ export class ArbToEthTransactionHandler { }; } + /** + * Save a snapshot on VeaInbox(Arb). + */ + public async saveSnapshot() { + this.emitter.emit(BotEvents.SAVING_SNAPSHOT, this.epoch); + const currentTime = Date.now(); + const transactionStatus = await this.checkTransactionStatus( + this.transactions.saveSnapshotTxn, + ContractType.INBOX, + currentTime + ); + if (transactionStatus != TransactionStatus.NOT_MADE && transactionStatus != TransactionStatus.EXPIRED) { + return; + } + const saveSnapshotTxn = await this.veaInbox.saveSnapshot(); + this.emitter.emit(BotEvents.TXN_MADE, saveSnapshotTxn.hash, this.epoch, "Save Snapshot"); + this.transactions.saveSnapshotTxn = { + hash: saveSnapshotTxn.hash, + broadcastedTimestamp: currentTime, + }; + } + /** * Send a snapshot from the VeaInbox(ARB) to the VeaOutox(ETH). */ diff --git a/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts index f352464d..0c1c7781 100644 --- a/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts +++ b/validator-cli/src/ArbToEth/transactionHandlerDevnet.ts @@ -25,6 +25,7 @@ export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler verifySnapshotTxn: null, challengeTxn: null, withdrawChallengeDepositTxn: null, + saveSnapshotTxn: null, sendSnapshotTxn: null, executeSnapshotTxn: null, devnetAdvanceStateTxn: null, diff --git a/validator-cli/src/utils/botConfig.ts b/validator-cli/src/utils/botConfig.ts index 3b4bf9b9..02301f03 100644 --- a/validator-cli/src/utils/botConfig.ts +++ b/validator-cli/src/utils/botConfig.ts @@ -18,7 +18,10 @@ interface BotPathParams { * @param defaultPath - default path to use if not specified in the command line arguments * @returns BotPaths - the bot path (BotPaths) */ -export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathParams): number { +export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathParams): { + path: number; + toSaveSnapshot: boolean; +} { const args = cliCommand.slice(2); const pathFlag = args.find((arg) => arg.startsWith("--path=")); @@ -33,8 +36,9 @@ export function getBotPath({ cliCommand, defaultPath = BotPaths.BOTH }: BotPathP if (path && !(path in pathMapping)) { throw new InvalidBotPathError(); } - - return path ? pathMapping[path] : defaultPath; + const saveSnapshotFlag = args.find((a) => a.startsWith("--saveSnapshot")); + const toSaveSnapshot = saveSnapshotFlag ? true : false; + return path ? { path: pathMapping[path], toSaveSnapshot } : { path: defaultPath, toSaveSnapshot }; } export interface NetworkConfig { diff --git a/validator-cli/src/utils/botEvents.ts b/validator-cli/src/utils/botEvents.ts index ffb67522..850c4cf9 100644 --- a/validator-cli/src/utils/botEvents.ts +++ b/validator-cli/src/utils/botEvents.ts @@ -13,6 +13,10 @@ export enum BotEvents { NO_SNAPSHOT = "no_snapshot", CLAIM_EPOCH_PASSED = "claim_epoch_passed", + // Snapshot state + SAVING_SNAPSHOT = "saving_snapshot", + SNAPSHOT_WAITING = "snapshot_saving", + // Claim state CLAIMING = "claiming", STARTING_VERIFICATION = "starting_verification", diff --git a/validator-cli/src/utils/epochHandler.ts b/validator-cli/src/utils/epochHandler.ts index 97c22500..b7b7f9a5 100644 --- a/validator-cli/src/utils/epochHandler.ts +++ b/validator-cli/src/utils/epochHandler.ts @@ -49,26 +49,23 @@ const setEpochRange = ({ return veaEpochOutboxCheckClaimsRangeArray; }; +const getLatestChallengeableEpoch = (epochPeriod: number, now: number = Date.now()): number => { + return Math.floor(now / 1000 / epochPeriod) - 2; +}; + /** - * Checks if a new epoch has started. + * Get the block number corresponding to a given epoch. * - * @param currentVerifiableEpoch - The current verifiable epoch number + * @param epoch - The epoch number * @param epochPeriod - The epoch period in seconds - * @param now - The current time in milliseconds (optional, defaults to Date.now()) + * @param provider - The JSON-RPC provider * - * @returns The updated epoch number - * - * @example - * currentEpoch = checkForNewEpoch(currentEpoch, 7200); + * @returns The block number corresponding to the given epoch */ -const getLatestChallengeableEpoch = (epochPeriod: number, now: number = Date.now()): number => { - return Math.floor(now / 1000 / epochPeriod) - 2; -}; - const getBlockFromEpoch = async (epoch: number, epochPeriod: number, provider: JsonRpcProvider): Promise => { const epochTimestamp = epoch * epochPeriod; const latestBlock = await provider.getBlock("latest"); - const baseBlock = await provider.getBlock(latestBlock.number - 1000); + const baseBlock = await provider.getBlock(latestBlock.number - 500); const secPerBlock = (latestBlock.timestamp - baseBlock.timestamp) / (latestBlock.number - baseBlock.number); const blockFallBack = Math.floor((latestBlock.timestamp - epochTimestamp) / secPerBlock); return latestBlock.number - blockFallBack; diff --git a/validator-cli/src/utils/graphQueries.ts b/validator-cli/src/utils/graphQueries.ts index fe048b4e..45364dd5 100644 --- a/validator-cli/src/utils/graphQueries.ts +++ b/validator-cli/src/utils/graphQueries.ts @@ -78,6 +78,11 @@ type VerificationData = { startTxHash: string | null; }; +/** + * Fetches the verification data for a given claim (used for claimer - happy path) + * @param claimId + * @returns VerificationData + */ const getVerificationForClaim = async (claimId: string): Promise => { try { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; @@ -97,6 +102,11 @@ const getVerificationForClaim = async (claimId: string): Promise => { try { const subgraph = process.env.VEAOUTBOX_SUBGRAPH; @@ -121,6 +131,11 @@ type SenSnapshotResponse = { }[]; }; +/** + * Fetches the snapshot data for a given epoch (used for validator - happy path) + * @param epoch + * @returns snapshot data + */ const getSnapshotSentForEpoch = async (epoch: number, veaInbox: any): Promise<{ txHash: string }> => { try { const subgraph = process.env.VEAINBOX_SUBGRAPH; @@ -143,11 +158,40 @@ const getSnapshotSentForEpoch = async (epoch: number, veaInbox: any): Promise<{ } }; +type SnapshotSavedResponse = { + snapshots: { + messages: { + id: string; + }[]; + }[]; +}; + +/** + * Fetches the last message saved for a given inbox (used for validator - happy path) + * @param veaInbox + * @returns message id + */ +const getLastMessageSaved = async (veaInbox: string): Promise => { + const subgraph = process.env.VEAINBOX_SUBGRAPH; + const result: SnapshotSavedResponse = await request( + `${subgraph}`, + `{ + snapshots(first:2, orderBy:timestamp,orderDirection:desc, where:{inbox:"${veaInbox}"}) { + messages(first: 1,orderBy:timestamp,orderDirection:desc){ + id + } + } + }` + ); + return result.snapshots[1].messages[0].id; +}; + export { getClaimForEpoch, getLastClaimedEpoch, getVerificationForClaim, getChallengerForClaim, getSnapshotSentForEpoch, + getLastMessageSaved, ClaimData, }; diff --git a/validator-cli/src/utils/logger.ts b/validator-cli/src/utils/logger.ts index ee791304..34f2c5f8 100644 --- a/validator-cli/src/utils/logger.ts +++ b/validator-cli/src/utils/logger.ts @@ -77,6 +77,14 @@ export const configurableInitialize = (emitter: EventEmitter) => { console.log(`Transaction(${transaction}) is expired`); }); + // Snapshot state logs + emitter.on(BotEvents.SAVING_SNAPSHOT, (epoch: number) => { + console.log(`Saving snapshot for epoch ${epoch}`); + }); + emitter.on(BotEvents.SNAPSHOT_WAITING, (time: number) => { + console.log(`Waiting for saving snapshot, time left: ${time}`); + }); + // Claim state logs // claim() emitter.on(BotEvents.CLAIMING, (epoch: number) => { diff --git a/validator-cli/src/utils/snapshot.test.ts b/validator-cli/src/utils/snapshot.test.ts new file mode 100644 index 00000000..066ae809 --- /dev/null +++ b/validator-cli/src/utils/snapshot.test.ts @@ -0,0 +1,204 @@ +import { Network } from "../consts/bridgeRoutes"; +import { isSnapshotNeeded, saveSnapshot } from "./snapshot"; +import { MockEmitter } from "./emitter"; + +describe("snapshot", () => { + let veaInbox: any; + let count: number = 1; + let fetchLastSavedMessage: jest.Mock; + beforeEach(() => { + veaInbox = { + count: jest.fn(), + queryFilter: jest.fn(), + filters: { + SnapshotSaved: jest.fn(), + }, + getAddress: jest.fn().mockResolvedValue("0x1"), + }; + }); + describe("isSnapshotNeeded", () => { + it("should return false and updated count when there are no new messages and count is -1 ", () => { + count = -1; + let currentCount = 1; + veaInbox.count.mockResolvedValue(currentCount); + fetchLastSavedMessage = jest.fn(); + veaInbox.queryFilter.mockResolvedValue([{ args: ["0x1", "0x2", currentCount] }]); + const params = { + veaInbox, + count, + fetchLastSavedMessage, + }; + expect(isSnapshotNeeded(params)).resolves.toEqual({ + snapshotNeeded: false, + latestCount: currentCount, + }); + }); + + it("should return false when count is equal to current count", () => { + count = 1; + let currentCount = 1; + veaInbox.count.mockResolvedValue(currentCount); + fetchLastSavedMessage = jest.fn(); + veaInbox.queryFilter.mockResolvedValue([{ args: ["0x1", "0x2", currentCount] }]); + const params = { + veaInbox, + count, + fetchLastSavedMessage, + }; + expect(isSnapshotNeeded(params)).resolves.toEqual({ + snapshotNeeded: false, + latestCount: count, + }); + }); + it("should return false if snapshot is saved for the current count", () => { + count = 1; + let currentCount = 2; + veaInbox.count.mockResolvedValue(currentCount); + fetchLastSavedMessage = jest.fn(); + veaInbox.queryFilter.mockResolvedValue([{ args: ["0x1", "0x2", currentCount] }]); + const params = { + veaInbox, + count, + fetchLastSavedMessage, + }; + expect(isSnapshotNeeded(params)).resolves.toEqual({ + snapshotNeeded: false, + latestCount: currentCount, + }); + }); + it("should return true if snapshot is needed", () => { + count = 1; + let currentCount = 2; + veaInbox.count.mockResolvedValue(currentCount); + fetchLastSavedMessage = jest.fn(); + veaInbox.queryFilter.mockResolvedValue([{ args: ["0x1", "0x2", 1] }]); + const params = { + veaInbox, + count, + fetchLastSavedMessage, + }; + expect(isSnapshotNeeded(params)).resolves.toEqual({ + snapshotNeeded: true, + latestCount: currentCount, + }); + }); + it("should fallback to fetchLastSavedMessage if queryFilter fails", () => { + count = 1; + let currentCount = 2; + veaInbox.count.mockResolvedValue(currentCount); + fetchLastSavedMessage = jest.fn().mockResolvedValue("message-0"); + veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed")); + const params = { + veaInbox, + count, + fetchLastSavedMessage, + }; + expect(isSnapshotNeeded(params)).resolves.toEqual({ + snapshotNeeded: true, + latestCount: currentCount, + }); + }); + }); + + describe("saveSnapshot", () => { + const network = Network.TESTNET; + const epochPeriod = 1200; + + it("should not save snapshot if time left for epoch is greater than 600 seconds", async () => { + veaInbox.count.mockResolvedValue(count + 1); + const now = 1220; // 20 seconds after the epoch started + const transactionHandler = { + saveSnapshot: jest.fn(), + }; + const res = await saveSnapshot({ + veaInbox, + network, + epochPeriod, + count, + transactionHandler, + emitter: new MockEmitter(), + now, + }); + + expect(transactionHandler.saveSnapshot).not.toHaveBeenCalled(); + expect(res).toEqual({ transactionHandler, latestCount: count }); + }); + + it("should save snapshot if time left for epoch is less than 600 seconds", async () => { + const currentCount = 6; // contract count + count = -1; + veaInbox.count.mockResolvedValue(currentCount); + const now = 1801; // 601 seconds after the epoch started + const isSnapshotNeededMock = jest.fn().mockResolvedValue({ + snapshotNeeded: true, + latestCount: currentCount, + }); + const transactionHandler = { + saveSnapshot: jest.fn(), + }; + const res = await saveSnapshot({ + veaInbox, + network, + epochPeriod, + count, + transactionHandler, + emitter: new MockEmitter(), + now, + toSaveSnapshot: isSnapshotNeededMock, + }); + expect(transactionHandler.saveSnapshot).toHaveBeenCalled(); + expect(res).toEqual({ transactionHandler, latestCount: currentCount }); + }); + + it("should not save snapshot if snapshot is needed", async () => { + const currentCount = 6; + veaInbox.count.mockResolvedValue(currentCount); + const isSnapshotNeededMock = jest.fn().mockResolvedValue({ + snapshotNeeded: false, + latestCount: currentCount, + }); + const now = 1801; // 601 seconds after the epoch started + const transactionHandler = { + saveSnapshot: jest.fn(), + }; + const res = await saveSnapshot({ + veaInbox, + network, + epochPeriod, + count: -1, + transactionHandler, + emitter: new MockEmitter(), + now, + toSaveSnapshot: isSnapshotNeededMock, + }); + expect(transactionHandler.saveSnapshot).not.toHaveBeenCalled(); + expect(res).toEqual({ transactionHandler, latestCount: currentCount }); + }); + + it("should save snapshot if snapshot is needed at anytime for devnet", async () => { + 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 transactionHandler = { + saveSnapshot: jest.fn(), + }; + const res = await saveSnapshot({ + veaInbox, + network: Network.DEVNET, + epochPeriod, + count, + transactionHandler, + emitter: new MockEmitter(), + now, + toSaveSnapshot: isSnapshotNeededMock, + }); + expect(transactionHandler.saveSnapshot).toHaveBeenCalled(); + expect(res).toEqual({ transactionHandler, latestCount: currentCount }); + }); + }); +}); diff --git a/validator-cli/src/utils/snapshot.ts b/validator-cli/src/utils/snapshot.ts new file mode 100644 index 00000000..bda415da --- /dev/null +++ b/validator-cli/src/utils/snapshot.ts @@ -0,0 +1,89 @@ +import { Network } from "../consts/bridgeRoutes"; +import { BotEvents } from "./botEvents"; +import { getLastMessageSaved } from "./graphQueries"; +import { defaultEmitter } from "./emitter"; + +interface SnapshotCheckParams { + veaInbox: any; + count: number; + fetchLastSavedMessage?: typeof getLastMessageSaved; +} + +export interface SaveSnapshotParams { + veaInbox: any; + network: Network; + epochPeriod: number; + count: number; + transactionHandler: any; + emitter?: typeof defaultEmitter; + toSaveSnapshot?: typeof isSnapshotNeeded; + now: number; +} + +export const saveSnapshot = async ({ + veaInbox, + network, + epochPeriod, + count, + transactionHandler, + emitter = defaultEmitter, + toSaveSnapshot = isSnapshotNeeded, + now = Date.now(), +}: SaveSnapshotParams): Promise => { + 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 { snapshotNeeded, latestCount } = await toSaveSnapshot({ + veaInbox, + count, + }); + if (!snapshotNeeded) return { transactionHandler, latestCount }; + await transactionHandler.saveSnapshot(); + return { transactionHandler, latestCount }; +}; + +export const isSnapshotNeeded = async ({ + veaInbox, + 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; + try { + const saveSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSaved()); + lastSavedCount = Number(saveSnapshotLogs[saveSnapshotLogs.length - 1].args[2]); + } catch { + const veaInboxAddress = await veaInbox.getAddress(); + const lastSavedMessageId = await fetchLastSavedMessage(veaInboxAddress); + const messageIndex = extractMessageIndex(lastSavedMessageId); + // adding 1 to the message index to get the last saved count + lastSavedCount = messageIndex + 1; + } + if (currentCount > lastSavedCount) { + return { snapshotNeeded: true, latestCount: currentCount }; + } + return { snapshotNeeded: false, latestCount: currentCount }; +}; + +function extractMessageIndex(id: string): number { + const parts = id.split("-"); + if (parts.length < 2) { + throw new Error(`Invalid message-id format: ${id}`); + } + // everything after the first dash is the index + const idxStr = parts.slice(1).join("-"); + const idx = parseInt(idxStr, 10); + if (Number.isNaN(idx)) { + throw new Error(`Cannot parse index from "${idxStr}"`); + } + return idx; +} diff --git a/validator-cli/src/watcher.ts b/validator-cli/src/watcher.ts index f077b48e..6409be6c 100644 --- a/validator-cli/src/watcher.ts +++ b/validator-cli/src/watcher.ts @@ -1,6 +1,6 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { getBridgeConfig, Network } from "./consts/bridgeRoutes"; -import { getVeaInbox, getVeaOutbox } from "./utils/ethers"; +import { getTransactionHandler, getVeaInbox, getVeaOutbox } from "./utils/ethers"; import { getBlockFromEpoch, setEpochRange } from "./utils/epochHandler"; import { getClaimValidator, getClaimer } from "./utils/ethers"; import { defaultEmitter } from "./utils/emitter"; @@ -12,6 +12,7 @@ import { getClaim } from "./utils/claim"; import { MissingEnvError } from "./utils/errors"; import { CheckAndClaimParams } from "./ArbToEth/claimer"; import { ChallengeAndResolveClaimParams } from "./ArbToEth/validator"; +import { saveSnapshot, SaveSnapshotParams } from "./utils/snapshot"; const RPC_BLOCK_LIMIT = 500; // RPC_BLOCK_LIMIT is the limit of blocks that can be queried at once @@ -31,14 +32,14 @@ export const watch = async ( const privKey = process.env.PRIVATE_KEY; if (!privKey) throw new MissingEnvError("PRIVATE_KEY"); const cliCommand = process.argv; - const path = getBotPath({ cliCommand }); + const { path, toSaveSnapshot } = getBotPath({ cliCommand }); const networkConfigs = getNetworkConfig(); emitter.emit(BotEvents.STARTED, path, networkConfigs[0].networks); const transactionHandlers: { [epoch: number]: any } = {}; - const toWatch: { [key: string]: number[] } = {}; + const toWatch: { [key: string]: { count: number; epochs: number[] } } = {}; while (!shutDownSignal.getIsShutdownSignal()) { for (const networkConfig of networkConfigs) { - await processNetwork(path, networkConfig, transactionHandlers, toWatch, emitter); + await processNetwork(path, toSaveSnapshot, networkConfig, transactionHandlers, toWatch, emitter); } await wait(1000 * 10); } @@ -46,9 +47,10 @@ export const watch = async ( async function processNetwork( path: number, + toSaveSnapshot: boolean, networkConfig: NetworkConfig, transactionHandlers: { [epoch: number]: any }, - toWatch: { [key: string]: number[] }, + toWatch: { [key: string]: { count: number; epochs: number[] } }, emitter: typeof defaultEmitter ): Promise { const { chainId, networks } = networkConfig; @@ -57,27 +59,27 @@ async function processNetwork( emitter.emit(BotEvents.WATCHING, chainId, network); const networkKey = `${chainId}_${network}`; if (!toWatch[networkKey]) { - toWatch[networkKey] = []; + toWatch[networkKey] = { count: -1, epochs: [] }; } - const veaOutboxProvider = new JsonRpcProvider(outboxRPC); let veaOutboxLatestBlock = await veaOutboxProvider.getBlock("latest"); // If the watcher has already started, only check the latest epoch if (network == Network.DEVNET) { - toWatch[networkKey] = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; - } else if (toWatch[networkKey].length == 0) { + toWatch[networkKey].epochs = [Math.floor(veaOutboxLatestBlock.timestamp / routeConfig[network].epochPeriod)]; + } else if (toWatch[networkKey].epochs.length == 0) { const epochRange = setEpochRange({ chainId, currentTimestamp: veaOutboxLatestBlock.timestamp, epochPeriod: routeConfig[network].epochPeriod, }); - toWatch[networkKey] = epochRange; + toWatch[networkKey].epochs = epochRange; } await processEpochsForNetwork({ chainId, path, + toSaveSnapshot, networkKey, network, routeConfig, @@ -88,11 +90,12 @@ async function processNetwork( emitter, }); const currentLatestBlock = await veaOutboxProvider.getBlock("latest"); - const currentLatestEpoch = Math.floor(currentLatestBlock.timestamp / routeConfig[network].epochPeriod); + const currentClaimableEpoch = Math.floor(currentLatestBlock.timestamp / routeConfig[network].epochPeriod) - 1; + const toWatchEpochs = toWatch[networkKey]; - const lastEpochInToWatch = toWatchEpochs[toWatchEpochs.length - 1]; - if (currentLatestEpoch > lastEpochInToWatch) { - toWatch[networkKey].push(currentLatestEpoch); + const lastEpochInToWatch = toWatchEpochs[toWatchEpochs.epochs.length - 1]; + if (currentClaimableEpoch > lastEpochInToWatch) { + toWatch[networkKey].epochs.push(currentClaimableEpoch); } } } @@ -100,18 +103,20 @@ async function processNetwork( interface ProcessEpochParams { chainId: number; path: number; + toSaveSnapshot: boolean; networkKey: string; network: Network; routeConfig: any; inboxRPC: string; outboxRPC: string; - toWatch: { [key: string]: number[] }; + toWatch: { [key: string]: { count: number; epochs: number[] } }; transactionHandlers: { [epoch: number]: any }; emitter: typeof defaultEmitter; } async function processEpochsForNetwork({ chainId, path, + toSaveSnapshot, networkKey, network, routeConfig, @@ -126,10 +131,39 @@ async function processEpochsForNetwork({ const veaOutbox = getVeaOutbox(routeConfig[network].veaOutbox.address, privKey, outboxRPC, chainId, network); const veaInboxProvider = new JsonRpcProvider(inboxRPC); const veaOutboxProvider = new JsonRpcProvider(outboxRPC); - let i = toWatch[networkKey].length - 1; - const latestEpoch = toWatch[networkKey][i]; + let i = toWatch[networkKey].epochs.length - 1; + const latestEpoch = toWatch[networkKey].epochs[i]; + const currentEpoch = Math.floor(Date.now() / (1000 * routeConfig[network].epochPeriod)); + // Checks and saves the snapshot if needed + if (toSaveSnapshot) { + const TransactionHandler = getTransactionHandler(chainId, network) as any; + const transactionHandler = + transactionHandlers[currentEpoch] || + new TransactionHandler({ + network, + epoch: currentEpoch, + veaInbox, + veaOutbox, + veaInboxProvider, + veaOutboxProvider, + emitter, + }); + const { updatedTransactionHandler, latestCount } = await saveSnapshot({ + veaInbox, + network, + epochPeriod: routeConfig[network].epochPeriod, + count: toWatch[networkKey].count, + transactionHandler, + } as SaveSnapshotParams); + const count = toWatch[networkKey].count; + if (count == -1 || count != latestCount) { + transactionHandlers[currentEpoch] = updatedTransactionHandler; + toWatch[networkKey].count = latestCount; + } + } + while (i >= 0) { - const epoch = toWatch[networkKey][i]; + const epoch = toWatch[networkKey].epochs[i]; const epochBlock = await getBlockFromEpoch(epoch, routeConfig[network].epochPeriod, veaOutboxProvider); const latestBlock = await veaOutboxProvider.getBlock("latest"); let toBlock: number | string = "latest"; @@ -142,6 +176,7 @@ async function processEpochsForNetwork({ const checkAndChallengeResolve = getClaimValidator(chainId, network); const checkAndClaim = getClaimer(chainId, network); let updatedTransactions; + if (path > BotPaths.CLAIMER && claim != null) { const checkAndChallengeResolveDeps: ChallengeAndResolveClaimParams = { claim, @@ -177,7 +212,7 @@ async function processEpochsForNetwork({ transactionHandlers[epoch] = updatedTransactions; } else if (epoch != latestEpoch) { delete transactionHandlers[epoch]; - toWatch[networkKey].splice(i, 1); + toWatch[networkKey].epochs.splice(i, 1); } i--; }