Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 31 additions & 0 deletions validator-cli/src/ArbToEth/transactionHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,6 +40,7 @@ describe("ArbToEthTransactionHandler", () => {
};
veaInbox = {
sendSnapshot: jest.fn(),
saveSnapshot: jest.fn(),
};
claim = {
stateRoot: "0x1234",
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 26 additions & 2 deletions validator-cli/src/ArbToEth/transactionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,6 +83,7 @@ export class ArbToEthTransactionHandler {
verifySnapshotTxn: null,
challengeTxn: null,
withdrawChallengeDepositTxn: null,
saveSnapshotTxn: null,
sendSnapshotTxn: null,
executeSnapshotTxn: null,
};
Expand Down Expand Up @@ -257,7 +259,7 @@ export class ArbToEthTransactionHandler {
}

/**
* Withdraw the claim deposit.
* Withdraw the claim deposit from VeaOutbox(ETH).
*
*/
public async withdrawClaimDeposit() {
Expand Down Expand Up @@ -335,7 +337,7 @@ export class ArbToEthTransactionHandler {
}

/**
* Withdraw the challenge deposit.
* Withdraw the challenge deposit from VeaOutbox(ETH).
*
*/
public async withdrawChallengeDeposit() {
Expand All @@ -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).
*/
Expand Down
1 change: 1 addition & 0 deletions validator-cli/src/ArbToEth/transactionHandlerDevnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class ArbToEthDevnetTransactionHandler extends ArbToEthTransactionHandler
verifySnapshotTxn: null,
challengeTxn: null,
withdrawChallengeDepositTxn: null,
saveSnapshotTxn: null,
sendSnapshotTxn: null,
executeSnapshotTxn: null,
devnetAdvanceStateTxn: null,
Expand Down
10 changes: 7 additions & 3 deletions validator-cli/src/utils/botConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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="));

Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions validator-cli/src/utils/botEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 9 additions & 12 deletions validator-cli/src/utils/epochHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> => {
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;
Expand Down
44 changes: 44 additions & 0 deletions validator-cli/src/utils/graphQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerificationData | undefined> => {
try {
const subgraph = process.env.VEAOUTBOX_SUBGRAPH;
Expand All @@ -97,6 +102,11 @@ const getVerificationForClaim = async (claimId: string): Promise<VerificationDat
}
};

/**
* Fetches the challenger data for a given claim (used for validator - unhappy path)
* @param claimId
* @returns challenger address
* */
const getChallengerForClaim = async (claimId: string): Promise<{ challenger: string } | undefined> => {
try {
const subgraph = process.env.VEAOUTBOX_SUBGRAPH;
Expand All @@ -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;
Expand All @@ -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<string> => {
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;
};
Comment on lines +174 to +187
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Out-of-bounds array access can crash the validator

result.snapshots[1].messages[0] assumes at least two snapshots each holding one message.
If the inbox has < 2 snapshots or the latest snapshot contains no messages the access will throw, killing the process.

-  return result.snapshots[1].messages[0].id;
+  if (
+    result.snapshots.length < 2 ||
+    result.snapshots[1].messages.length === 0
+  ) {
+    throw new Error(
+      `Could not determine last saved message for inbox ${veaInbox}`
+    );
+  }
+  return result.snapshots[1].messages[0].id;

Guarding against missing data keeps the CLI resilient on brand-new deployments and during subgraph lag.

📝 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
const getLastMessageSaved = async (veaInbox: string): Promise<string> => {
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;
};
const getLastMessageSaved = async (veaInbox: string): Promise<string> => {
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
}
}
}`
);
if (
result.snapshots.length < 2 ||
result.snapshots[1].messages.length === 0
) {
throw new Error(
`Could not determine last saved message for inbox ${veaInbox}`
);
}
return result.snapshots[1].messages[0].id;
};


export {
getClaimForEpoch,
getLastClaimedEpoch,
getVerificationForClaim,
getChallengerForClaim,
getSnapshotSentForEpoch,
getLastMessageSaved,
ClaimData,
};
8 changes: 8 additions & 0 deletions validator-cli/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Loading
Loading