Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/relayer tests #395

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion relayer-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"dotenv": "^16.4.5",
"pm2": "^5.2.2",
"typescript": "^4.9.5",
"web3": "^4.16.0",
"web3": "^1.10.4",
"web3-batched-send": "^1.0.3"
},
"devDependencies": {
Expand Down
15 changes: 9 additions & 6 deletions relayer-cli/src/devnetRelayExample.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { EventEmitter } from "events";
import { relayAllFrom } from "./utils/relay";
import { initialize, ShutdownManager, updateStateFile, setupExitHandlers, delay } from "./utils/relayerHelpers";
import { BotEvents } from "utils/botEvents";

export async function start(shutdownManager: ShutdownManager = new ShutdownManager()) {
export async function start(shutdownManager: ShutdownManager = new ShutdownManager(), emitter: EventEmitter) {
const chainId = parseInt(process.env.VEAOUTBOX_CHAIN_ID);
const epochPeriod = 1800; // 30 min
const network = "devnet";
Comment on lines 8 to 9
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Verify environment variables are defined before use

You're using process.env.VEAOUTBOX_CHAIN_ID to set chainId. If this environment variable is undefined, parseInt(undefined) results in NaN, which could cause issues. Similarly, sender is set from process.env.DEVNET_SENDER. Consider adding checks to ensure these environment variables are defined and handle the case where they are not.

Apply this diff to add checks for the environment variables:

+ if (!process.env.VEAOUTBOX_CHAIN_ID) {
+   throw new Error('VEAOUTBOX_CHAIN_ID environment variable is not defined');
+ }
  const chainId = parseInt(process.env.VEAOUTBOX_CHAIN_ID);

  const epochPeriod = 1800; // 30 min
  const network = "devnet";

+ if (!process.env.DEVNET_SENDER) {
+   throw new Error('DEVNET_SENDER environment variable is not defined');
+ }
  const sender = process.env.DEVNET_SENDER;

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's already an issue created for env validation and it will be addressed there.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mani99brar Understood, I'll defer to that issue for environment variable validation.

⊙﹏⊙

await setupExitHandlers(chainId, shutdownManager, network);
await setupExitHandlers(chainId, shutdownManager, network, emitter);
while (!shutdownManager.getIsShuttingDown()) {
let nonce = await initialize(chainId, network);
let nonce = await initialize(chainId, network, emitter);
// This is libghtbulb switch address in arbitrum sepolia
const sender = process.env.DEVNET_SENDER;
nonce = await relayAllFrom(chainId, nonce, sender);
if (nonce != null) await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, network);
if (nonce != null) await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, network, emitter);

const currentTS = Math.floor(Date.now() / 1000);
const delayAmount = (epochPeriod - (currentTS % epochPeriod)) * 1000 + 100 * 1000;
console.log("waiting for the next epoch. . .", Math.floor(delayAmount / 1000), "seconds");
emitter.emit(BotEvents.WAITING, delayAmount);
await delay(delayAmount);
}
}

if (require.main === module) {
const emitter = new EventEmitter();
const shutdownManager = new ShutdownManager(false);
start(shutdownManager);
start(shutdownManager, emitter);
}
60 changes: 48 additions & 12 deletions relayer-cli/src/testnetRelayer.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
require("dotenv").config();
import { relayBatch } from "utils/relay";
import { initialize, updateStateFile, delay, setupExitHandlers, ShutdownManager } from "utils/relayerHelpers";
import { EventEmitter } from "node:events";
import { relayBatch, RelayBatchDeps } from "utils/relay";
import {
initialize as initializeNonce,
updateStateFile,
delay,
setupExitHandlers,
ShutdownManager,
} from "utils/relayerHelpers";
import { getEpochPeriod } from "consts/bridgeRoutes";
import { initialize as initializeEmitter } from "utils/logger";
import { BotEvents } from "utils/botEvents";

export async function start(shutdownManager: ShutdownManager = new ShutdownManager()) {
const network = "testnet";
const chainId = parseInt(process.env.VEAOUTBOX_CHAIN_ID);
interface RelayerConfig {
chainId: number;
network: string;
shutdownManager: ShutdownManager;
emitter: EventEmitter;
}

/**
* Start the relayer
* @param chainId The chain id of the relayer
* @param network The network of the relayer
* @param shutdownManager The shutdown manager
* @param emitter The event emitter
*/
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update JSDoc comments to reflect the new function signature

The start function now accepts a single RelayerConfig object as a parameter, but the JSDoc comments list individual parameters (chainId, network, shutdownManager, emitter). Please update the JSDoc to accurately describe the RelayerConfig parameter and its properties.

Apply this diff to update the JSDoc comments:

 /**
  * Start the relayer
-  * @param chainId The chain id of the relayer
-  * @param network The network of the relayer
-  * @param shutdownManager The shutdown manager
-  * @param emitter The event emitter
+  * @param config The relayer configuration
+  * @param config.chainId The chain id of the relayer
+  * @param config.network The network of the relayer
+  * @param config.shutdownManager The shutdown manager
+  * @param config.emitter The event emitter
  */
 export async function start({ chainId, network, shutdownManager, emitter }: RelayerConfig) {
📝 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
/**
* Start the relayer
* @param chainId The chain id of the relayer
* @param network The network of the relayer
* @param shutdownManager The shutdown manager
* @param emitter The event emitter
*/
/**
* Start the relayer
* @param config The relayer configuration
* @param config.chainId The chain id of the relayer
* @param config.network The network of the relayer
* @param config.shutdownManager The shutdown manager
* @param config.emitter The event emitter
*/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think its needed, the comments and implementation is clear enough.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mani99brar Understood, I'll retract the suggestion. The TypeScript types and current documentation are indeed clear enough.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mani99brar the bot is right, the correct way of documenting object properties is with the dot notation

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

export async function start({ chainId, network, shutdownManager, emitter }: RelayerConfig) {
initializeEmitter(emitter);
emitter.emit(BotEvents.STARTED, chainId, network);
const epochPeriod = getEpochPeriod(chainId);
const batchSize = 10; // 10 messages per batch
const maxBatchSize = 10; // 10 messages per batch

await setupExitHandlers(chainId, shutdownManager, network);
await setupExitHandlers(chainId, shutdownManager, network, emitter);

while (!shutdownManager.getIsShuttingDown()) {
let nonce = await initialize(chainId, network);
nonce = await relayBatch(chainId, nonce, batchSize);
if (nonce != null) await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, network);
let nonce = await initializeNonce(chainId, network, emitter);
const relayBatchDeps: RelayBatchDeps = {
chainId,
nonce,
maxBatchSize,
};
nonce = await relayBatch(relayBatchDeps);
if (nonce != null) await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, network, emitter);
const currentTS = Math.floor(Date.now() / 1000);
const delayAmount = (epochPeriod - (currentTS % epochPeriod)) * 1000 + 100 * 1000;
console.log("waiting for the next epoch. . .", Math.floor(delayAmount / 1000), "seconds");
emitter.emit(BotEvents.WAITING, delayAmount);
await delay(delayAmount);
}
}

if (require.main === module) {
const emitter = new EventEmitter();
const shutdownManager = new ShutdownManager(false);
start(shutdownManager);
const testnetRelayerConfig: RelayerConfig = {
shutdownManager,
emitter,
chainId: Number(process.env.VEAOUTBOX_CHAIN_ID),
network: "testnet",
};
Comment on lines +59 to +61
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Verify that VEAOUTBOX_CHAIN_ID environment variable is defined

You're using Number(process.env.VEAOUTBOX_CHAIN_ID) to set chainId. If VEAOUTBOX_CHAIN_ID is undefined, Number(undefined) results in NaN, which could cause issues downstream. Consider adding a check to ensure that process.env.VEAOUTBOX_CHAIN_ID is defined and handle the case where it is not.

Apply this diff to add a check for the environment variable:

+ if (!process.env.VEAOUTBOX_CHAIN_ID) {
+   throw new Error('VEAOUTBOX_CHAIN_ID environment variable is not defined');
+ }
  const testnetRelayerConfig: RelayerConfig = {
    shutdownManager,
    emitter,
    chainId: Number(process.env.VEAOUTBOX_CHAIN_ID),
    network: "testnet",
  };
📝 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
chainId: Number(process.env.VEAOUTBOX_CHAIN_ID),
network: "testnet",
};
if (!process.env.VEAOUTBOX_CHAIN_ID) {
throw new Error('VEAOUTBOX_CHAIN_ID environment variable is not defined');
}
const testnetRelayerConfig: RelayerConfig = {
shutdownManager,
emitter,
chainId: Number(process.env.VEAOUTBOX_CHAIN_ID),
network: "testnet",
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's already an issue created for env validation and it will be addressed there.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mani99brar Understood, thanks for letting me know that environment variable validation will be handled separately.


start(testnetRelayerConfig);
}
15 changes: 15 additions & 0 deletions relayer-cli/src/utils/botEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum BotEvents {
// Bridger state
STARTED = "started",
WAITING = "waiting",
EXIT = "exit",

// Bot health
EXCEPTION = "exception",
PROMISE_REJECTION = "promise_rejection",

// Lock file
LOCK_CLAIMED = "lock_claimed",
LOCK_DIRECTORY = "lock_directory",
LOCK_RELEASED = "lock_released",
}
2 changes: 1 addition & 1 deletion relayer-cli/src/utils/ethers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
VeaInboxArbToGnosis__factory,
VeaOutboxArbToGnosis__factory,
} from "@kleros/vea-contracts/typechain-types";
import { getBridgeConfig } from "consts/bridgeRoutes";
import { getBridgeConfig } from "../consts/bridgeRoutes";

function getWallet(privateKey: string, web3ProviderURL: string): Wallet {
return new Wallet(privateKey, new JsonRpcProvider(web3ProviderURL));
Expand Down
49 changes: 49 additions & 0 deletions relayer-cli/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { EventEmitter } from "node:events";
import { BotEvents } from "./botEvents";

/**
* Listens to relevant events of an EventEmitter instance and issues log lines
*
* @param emitter - The event emitter instance that issues the relevant events
*
* @example
*
* const emitter = new EventEmitter();
* initialize(emitter);
*/

export const initialize = (emitter: EventEmitter) => {
return configurableInitialize(emitter);
};

export const configurableInitialize = (emitter: EventEmitter) => {
// Relayer state logs
emitter.on(BotEvents.STARTED, (chainId, network) => {
console.log(`Relayer started for ${chainId} on ${network}`);
});
emitter.on(BotEvents.WAITING, (delayAmount) => {
console.log(`Waiting for next epoch: ${delayAmount} ms`);
});
emitter.on(BotEvents.EXIT, () => {
console.log("Exiting");
});

// Bot health logs
emitter.on(BotEvents.EXCEPTION, (err) => {
console.error("Uncaught Exception occurred", err);
});
emitter.on(BotEvents.PROMISE_REJECTION, (reason, promise) => {
console.error("Unhandled promise rejection:", reason, "at", promise);
});

// Lock file logs
emitter.on(BotEvents.LOCK_CLAIMED, () => {
console.log("Lock claimed");
});
emitter.on(BotEvents.LOCK_DIRECTORY, (pwd) => {
console.log(`Lock file directory: ${pwd}`);
});
emitter.on(BotEvents.LOCK_RELEASED, () => {
console.log("Lock released");
});
};
Comment on lines +19 to +49
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling to event listeners.

The event listeners should include try-catch blocks to prevent crashes if event handling fails.

Here's an example for one event handler:

-  emitter.on(BotEvents.STARTED, (chainId, network) => {
-    console.log(`Relayer started for ${chainId} on ${network}`);
-  });
+  emitter.on(BotEvents.STARTED, (chainId, network) => {
+    try {
+      console.log(`Relayer started for ${chainId} on ${network}`);
+    } catch (error) {
+      console.error('Failed to handle STARTED event:', error);
+    }
+  });
📝 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
export const configurableInitialize = (emitter: EventEmitter) => {
// Relayer state logs
emitter.on(BotEvents.STARTED, (chainId, network) => {
console.log(`Relayer started for ${chainId} on ${network}`);
});
emitter.on(BotEvents.WAITING, (delayAmount) => {
console.log(`Waiting for next epoch: ${delayAmount} ms`);
});
emitter.on(BotEvents.EXIT, () => {
console.log("Exiting");
});
// Bot health logs
emitter.on(BotEvents.EXCEPTION, (err) => {
console.error("Uncaught Exception occurred", err);
});
emitter.on(BotEvents.PROMISE_REJECTION, (reason, promise) => {
console.error("Unhandled promise rejection:", reason, "at", promise);
});
// Lock file logs
emitter.on(BotEvents.LOCK_CLAIMED, () => {
console.log("Lock claimed");
});
emitter.on(BotEvents.LOCK_DIRECTORY, (pwd) => {
console.log(`Lock file directory: ${pwd}`);
});
emitter.on(BotEvents.LOCK_RELEASED, () => {
console.log("Lock released");
});
};
export const configurableInitialize = (emitter: EventEmitter) => {
// Relayer state logs
emitter.on(BotEvents.STARTED, (chainId, network) => {
try {
console.log(`Relayer started for ${chainId} on ${network}`);
} catch (error) {
console.error('Failed to handle STARTED event:', error);
}
});
emitter.on(BotEvents.WAITING, (delayAmount) => {
try {
console.log(`Waiting for next epoch: ${delayAmount} ms`);
} catch (error) {
console.error('Failed to handle WAITING event:', error);
}
});
emitter.on(BotEvents.EXIT, () => {
try {
console.log("Exiting");
} catch (error) {
console.error('Failed to handle EXIT event:', error);
}
});
// Bot health logs
emitter.on(BotEvents.EXCEPTION, (err) => {
try {
console.error("Uncaught Exception occurred", err);
} catch (error) {
console.error('Failed to handle EXCEPTION event:', error);
}
});
emitter.on(BotEvents.PROMISE_REJECTION, (reason, promise) => {
try {
console.error("Unhandled promise rejection:", reason, "at", promise);
} catch (error) {
console.error('Failed to handle PROMISE_REJECTION event:', error);
}
});
// Lock file logs
emitter.on(BotEvents.LOCK_CLAIMED, () => {
try {
console.log("Lock claimed");
} catch (error) {
console.error('Failed to handle LOCK_CLAIMED event:', error);
}
});
emitter.on(BotEvents.LOCK_DIRECTORY, (pwd) => {
try {
console.log(`Lock file directory: ${pwd}`);
} catch (error) {
console.error('Failed to handle LOCK_DIRECTORY event:', error);
}
});
emitter.on(BotEvents.LOCK_RELEASED, () => {
try {
console.log("Lock released");
} catch (error) {
console.error('Failed to handle LOCK_RELEASED event:', error);
}
});
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I dont see how this is necessary

Copy link
Contributor

Choose a reason for hiding this comment

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

@mani99brar You're right. The event listeners are just doing simple console logging, which is very unlikely to throw errors. Adding try-catch blocks would be unnecessary complexity. I'll retract this suggestion.


✏️ Learnings added
Learnt from: mani99brar
PR: kleros/vea#395
File: relayer-cli/src/utils/logger.ts:19-49
Timestamp: 2025-01-15T07:41:36.295Z
Learning: For simple logging operations using console.log/error, adding try-catch blocks is unnecessary as these operations are highly reliable and any errors would be caught by process-level error handlers.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

20 changes: 20 additions & 0 deletions relayer-cli/src/utils/proof.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getProofIndices } from "./proof";

describe("proof", () => {
describe("getProofIndices", () => {
it("should return an empty array", () => {
const result = getProofIndices(7, 7);
expect(result).toEqual([]);
});
it("should return the proof indices", () => {
const expectedProofIndices = ["3", "0,1", "4,6"];
const result = getProofIndices(2, 7);
expect(result).toEqual(expectedProofIndices);
});
it("should return the proof indices(for a large count", () => {
const expectedProofIndices = ["6", "4,5", "0,3", "8,14"];
const result = getProofIndices(7, 15);
expect(result).toEqual(expectedProofIndices);
});
});
});
45 changes: 34 additions & 11 deletions relayer-cli/src/utils/proof.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import request from "graphql-request";
import { getInboxSubgraph } from "../consts/bridgeRoutes";

const getMessageDataToRelay = async (chainid: number, nonce: number) => {
/**
* Get the message data to relay from the subgraph
* @param chainId The chain id of the veaOutbox chain
* @param nonce The nonce of the message
* @returns The message id and data to relay
*/
const getMessageDataToRelay = async (chainId: number, nonce: number, requestGraph: typeof request = request) => {
try {
const subgraph = getInboxSubgraph(chainid);
const subgraph = getInboxSubgraph(chainId);

const result = await request(
const result = await requestGraph(
`https://api.studio.thegraph.com/query/${subgraph}`,
`{
messageSents(first: 5, where: {nonce: ${nonce}}) {
Expand All @@ -24,9 +30,22 @@ const getMessageDataToRelay = async (chainid: number, nonce: number) => {
}
};

const getProofAtCount = async (chainid: number, nonce: number, count: number): Promise<string[]> => {
const proofIndices = getProofIndices(nonce, count);

/**
* Get the proof of the message at a given count
* @param chainId The chain id of the veaOutbox chain
* @param nonce The nonce of the message
* @param count The current veaOutbox count
* @returns The proof of the message
*/
const getProofAtCount = async (
chainId: number,
nonce: number,
count: number,
requestGraph: typeof request = request,
calculateProofIndices: typeof getProofIndices = getProofIndices,
fetchInboxSubgraph: typeof getInboxSubgraph = getInboxSubgraph
): Promise<string[]> => {
const proofIndices = calculateProofIndices(nonce, count);
if (proofIndices.length == 0) return [];

let query = "{";
Expand All @@ -38,23 +57,27 @@ const getProofAtCount = async (chainid: number, nonce: number, count: number): P
query += "}";

try {
const subgraph = getInboxSubgraph(chainid);
const subgraph = fetchInboxSubgraph(chainId);

const result = await request(`https://api.studio.thegraph.com/query/${subgraph}`, query);
const result = await requestGraph(`https://api.studio.thegraph.com/query/${subgraph}`, query);

const proof = [];

for (let i = 0; i < proofIndices.length; i++) {
proof.push(result[`layer${i}`][0].hash);
}

return proof;
} catch (e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not a change of the PR, so please don't address it within it... but this try/catch is unexpected for me. If it fails getting the hashes... it returns an empty array? not throw the error? the request failing should be unexpected, not a controlled error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right. It doesn't make any sense to return an empty array to me either. Creating an issue for it.

console.log(e);
return [];
}
};

/**
* Get the proof indices of the message
* @param nonce The nonce of the message
* @param count The current veaOutbox count
* @returns The proof indices of the message
*/
const getProofIndices = (nonce: number, count: number) => {
let proof = [];
if (nonce >= count) return proof;
Expand All @@ -76,4 +99,4 @@ const getProofIndices = (nonce: number, count: number) => {
return proof;
};

export { getProofAtCount, getMessageDataToRelay };
export { getProofAtCount, getMessageDataToRelay, getProofIndices };
Loading
Loading