Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e555918
chore: rebase contracts
Oct 31, 2025
c6c9af7
Merge pull request #436 from kleros/fix/handle-sent-snapshots
mani99brar Oct 31, 2025
3bcc672
chore: remove dev logs
Oct 31, 2025
643e814
fix: executeable condition
Oct 31, 2025
a17231d
fix: validate txn hash
Nov 3, 2025
d023e80
feat: docker setup
Nov 10, 2025
4dbb3c3
chore: remove contract workspace & add ether dep
Nov 10, 2025
1d3a917
chore: update relative imports
Nov 10, 2025
a2eb880
chore: conditional contract build
Nov 10, 2025
14d4495
chore: remove dev comments
Nov 10, 2025
6c2dae9
chore: refactor last msg query
Nov 10, 2025
26985af
fix: snapshot log index
Nov 10, 2025
317272b
feat: pino & struct logs
Nov 13, 2025
4b14b1d
chore: remove pm2
Nov 13, 2025
a3cc948
chore: update node version
Nov 28, 2025
e7dae43
feat: heartbeat
Nov 28, 2025
2ab1be0
fix: image node version
Nov 28, 2025
f16466f
feat: add inbox field in Message
Dec 1, 2025
2b4a749
chore: try catch for lastSavedMessage ql query
Dec 1, 2025
10e123e
fix: empty messages
Dec 2, 2025
a955ad8
fix: skip challenge when finality issue
Dec 2, 2025
eaadc7c
fix: update lastMessageResponse type
Dec 2, 2025
438bc01
feat(relayer): pino
Dec 5, 2025
4a0b833
fix: saveSnapshot comment
Dec 8, 2025
336b247
chore: batch ql claim query
Dec 8, 2025
d5a6f28
feat: saveSnapshot if challenged
Dec 11, 2025
785e2d4
chore: inc block range
Dec 12, 2025
60ba6cf
fix: finality logs & check
Dec 15, 2025
b33956c
chore: add finality return
Dec 15, 2025
ff8ee2b
chore: add pino dep
Dec 15, 2025
37528a1
chore: update ql url
Dec 15, 2025
9a6a626
fix: push latest claimable epoch
Dec 15, 2025
1c0d4db
chore: remove log & catch fallback
Dec 15, 2025
f0d3305
fix: block range offset & hard fallback
Dec 17, 2025
5e683ec
chore: rebase veascan subgraph fixes
Dec 17, 2025
e6df6fa
fix: seq offline flag & claim reinit
Dec 17, 2025
bde0780
chore: use final toBlock
Dec 19, 2025
7f7dbb4
chore: remove range for verification events
Dec 19, 2025
ab37c59
chore: remove last claim conditions
Dec 19, 2025
cb7f984
fix: add lastClaim try catch
Dec 19, 2025
b73cd89
fix: snapshot lastClaim try catch
Dec 19, 2025
0066b07
Merge branch 'dev' into chore/deploy
mani99brar Dec 31, 2025
6232598
fix: review changes
Jan 5, 2026
08d664f
chore: update readme
Jan 5, 2026
511e71c
feat: agnostic hashi executor
Jan 10, 2026
c159af8
chore: hashi executor cleanup
Jan 12, 2026
7964718
chore: update watcher cycle
Jan 12, 2026
508e557
chore: update env example & readme
Jan 12, 2026
43a7117
Merge branch 'dev' into chore/deploy
mani99brar Jan 13, 2026
d400265
Merge pull request #439 from kleros/feat/agnostic-hashi-executor
mani99brar Jan 13, 2026
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
25 changes: 25 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
validator:
build:
context: .
dockerfile: ./validator-cli/Dockerfile
command: yarn start --saveSnapshot
env_file:
- path: ./validator-cli/.env
required: true
restart: unless-stopped
networks:
- vea-network

relayer:
build:
context: .
dockerfile: ./relayer-cli/Dockerfile
env_file:
- path: ./relayer-cli/.env
required: true
restart: unless-stopped
networks:
- vea-network
networks:
vea-network:
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
],
"packageManager": "yarn@4.6.0",
"volta": {
"node": "22.14.0",
"node": "24.4.0",
"yarn": "4.6.0"
},
"devDependencies": {
Expand Down
13 changes: 9 additions & 4 deletions relayer-cli/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ PRIVATE_KEY=
SENDER_ADDRESSES_DEVNET=0x906dE43dBef27639b1688Ac46532a16dc07Ce410,0x123456789abcdef123456789abcdef1234567890
SENDER_ADDRESSES_TESTNET=0x0000000000000000000000000000000000000000
SENDER_ADDRESS_MAINNET=
# Hashi sender addresses, zero address to allow all senders and enable hashi executor
SENDER_ADDRESSES_HASHI=0x0000000000000000000000000000000000000000

RPC_CHIADO=https://rpc.chiadochain.net
RPC_SEPOLIA=
RPC_ARBITRUM_SEPOLIA=
RPC_STORY=
RPC_ARBITRUM_ONE=

VEAOUTBOX_CHAINS=11155111,10200
# Comma separated list of chain IDs where VeaOutbox is deployed
VEAOUTBOX_CHAINS=11155111

# Comma separated list of chain pairs of sourceChainId-targetChainId for Hashi routes
HASHI_CHAINS=421614-11155111,421614-10200,1514-42161

# Subgraph endpoint, Example: "85918/vea-inbox-arb-sepolia-devnet/version/latest"
RELAYER_SUBGRAPH="61738/vea-inbox-arb-sepolia/version/latest"
Expand All @@ -22,6 +30,3 @@ TRANSACTION_BATCHER_CONTRACT_CHIADO=0xcC0a08D4BCC5f91ee9a1587608f7a2975EA75d73

# Ensure the path ends with a trailing slash ("/")
STATE_DIR="/home/user/vea/relayer-cli/state/"

# Hashi Executor enabler
HASHI_EXECUTOR_ENABLED=true
92 changes: 92 additions & 0 deletions relayer-cli/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#
# Builder image
#

# Node version for Vea
FROM node:24.4-alpine AS relayer-build

# Yarn version for Vea
ARG YARN_VERSION=4.6.0
RUN corepack enable \
&& corepack prepare "yarn@${YARN_VERSION}" --activate

USER node
WORKDIR /home/node/monorepo

#
# Dependencies installation step.
#

# Global monorepo dependencies
COPY --chown=node:node ./.yarn/releases/ ./.yarn/releases/
COPY --chown=node:node package.json yarn.lock .yarnrc.yml ./

#
# Packages dependencies
#
COPY --chown=node:node ./relayer-cli/package.json relayer-cli/
COPY --chown=node:node ./contracts/package.json contracts/

# We skip the `yarn cache clean` after install to speed up subsequent installs
# (mostly the yarn workspace focus)
RUN --mount=type=cache,id=yarn-cache,target=/home/node/monorepo/.yarn,uid=1000,gid=1000 \
YARN_IGNORE_PATH=1 yarn install

#
# Source code copy step.
#
COPY --chown=node:node relayer-cli ./relayer-cli
COPY --chown=node:node contracts ./contracts

#
# Build step to generate contract artifacts and typechain types
#
RUN [ -d contracts/typechain-types ] && echo "TypeChain present, skipping build" || yarn workspace @kleros/vea-contracts build
RUN yarn workspaces focus @kleros/vea-relayer-cli



# ----------------------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------------------
# ----------------------------------------------------------------------------------------------------


#
# Deployable image
#
# This is the final layer that will be deployed and run

# Node version for all Vea
FROM node:24.4-alpine AS relayer

Check warning

Code scanning / Scorecard

Pinned-Dependencies Medium

score is 7: containerImage not pinned by hash
Click Remediation section below to solve this issue

# Yarn version for all Vea
ARG YARN_VERSION=4.6.0
RUN corepack enable \
&& corepack prepare "yarn@${YARN_VERSION}" --activate

USER root
RUN mkdir -p /home/node/monorepo/.yarn && chown -R node:node /home/node/monorepo


USER node
WORKDIR /home/node/monorepo

# Copy only the necessary files from the builder stage
COPY --from=relayer-build --link /home/node/monorepo/contracts/typechain-types ./contracts/typechain-types
COPY --from=relayer-build --link /home/node/monorepo/contracts/deployments ./contracts/deployments

# Copy the globally required files and the service workspace
COPY --from=relayer-build --link /home/node/monorepo/node_modules /home/node/monorepo/node_modules
COPY --from=relayer-build --link \
/home/node/monorepo/.yarnrc.yml \
/home/node/monorepo/yarn.lock \
/home/node/monorepo/package.json \
./

COPY --from=relayer-build --link /home/node/monorepo/relayer-cli ./relayer-cli

WORKDIR /home/node/monorepo/relayer-cli

RUN mkdir -p /home/node/monorepo/relayer-cli/state && chown -R node:node /home/node/monorepo/relayer-cli/state
ENV YARN_IGNORE_PATH=1
CMD ["yarn", "start-relayer"]
20 changes: 15 additions & 5 deletions relayer-cli/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
# bots
# Relayer bot

A collection of bots for the Vea relayers.

- src/devnetRelayExample.ts
- src/relayer.ts

# pm2
# docker

`pm2 start`
`docker compose build relayer`

Runs a relayer for all messages sent through example gateway contracts.
`docker compose up relayer`

Runs a relayer for all messages sent through VeaInbox contracts.

# `env` config

- `VEAOUTBOX_CHAINS`: to chose the chains to execute messages on.

- Address params: used to execute for specific sender addresses or everyone if zero address is provided.

- `SENDER_ADDRESSES`: pass zero address to allow all senders and relayer or executor for hashi.
6 changes: 5 additions & 1 deletion relayer-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@
"start-relayer": "ts-node ./src/relayer.ts"
},
"dependencies": {
"@kleros/vea-contracts": "workspace:^",
"@typechain/ethers-v6": "^0.5.1",
"dotenv": "^16.4.5",
"ethers": "^6.13.5",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"pino": "^10.1.0",
"pm2": "^6.0.5",
"typescript": "^4.9.5"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"pino-pretty": "13.1.2",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2"
}
Expand Down
16 changes: 8 additions & 8 deletions relayer-cli/src/consts/bridgeRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// File for handling contants and configurations
require("dotenv").config();
import veaInboxArbToEthDevnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToEthDevnet.json";
import veaOutboxArbToEthDevnet from "@kleros/vea-contracts/deployments/sepolia/VeaOutboxArbToEthDevnet.json";
import veaInboxArbToEthTestnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToEthTestnet.json";
import veaOutboxArbToEthTestnet from "@kleros/vea-contracts/deployments/sepolia/VeaOutboxArbToEthTestnet.json";
import veaInboxArbToEthDevnet from "../../../contracts/deployments/arbitrumSepolia/VeaInboxArbToEthDevnet.json";
import veaOutboxArbToEthDevnet from "../../../contracts/deployments/sepolia/VeaOutboxArbToEthDevnet.json";
import veaInboxArbToEthTestnet from "../../../contracts/deployments/arbitrumSepolia/VeaInboxArbToEthTestnet.json";
import veaOutboxArbToEthTestnet from "../../../contracts/deployments/sepolia/VeaOutboxArbToEthTestnet.json";

import veaInboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisDevnet.json";
import veaOutboxArbToGnosisDevnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisDevnet.json";
import veaInboxArbToGnosisDevnet from "../../../contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisDevnet.json";
import veaOutboxArbToGnosisDevnet from "../../../contracts/deployments/chiado/VeaOutboxArbToGnosisDevnet.json";

import veaInboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisTestnet.json";
import veaOutboxArbToGnosisTestnet from "@kleros/vea-contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json";
import veaInboxArbToGnosisTestnet from "../../../contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisTestnet.json";
import veaOutboxArbToGnosisTestnet from "../../../contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json";

interface IBridge {
chainId: number;
Expand Down
55 changes: 35 additions & 20 deletions relayer-cli/src/relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,42 @@ import { initialize as initializeEmitter } from "./utils/logger";
import { BotEvents } from "./utils/botEvents";
import { getEpochPeriod, Network } from "./consts/bridgeRoutes";
import { runHashiExecutor } from "./utils/hashi";
import { sendHeartbeat } from "./utils/heartbeat";

interface RelayerConfig {
networkConfigs: RelayerNetworkConfig[];
shutdownManager: ShutdownManager;
emitter: EventEmitter;
}

const HASHI_CYCLE_TIME_MS = 5 * 60 * 1000; // 5 minutes

/**
* Start the relayer
* @param config.networkConfigs The network configurations retrieved from the env.
* @param config.shutdownManager The shutdown manager
* @param config.emitter The event emitter
*/
export async function start({ networkConfigs, shutdownManager, emitter }: RelayerConfig) {
const HEARTBEAT_URL = process.env.HEARTBEAT_URL;
await sendHeartbeat("started", HEARTBEAT_URL);
initializeEmitter(emitter);
let delayAmount = 7200 * 1000; // 2 hours in ms
const executeTimes: number[] = networkConfigs.map(() => 0);
while (!shutdownManager.getIsShuttingDown()) {
for (const networkConfig of networkConfigs) {
delayAmount = await processNetworkConfig(networkConfig, shutdownManager, emitter, delayAmount);
let executeTime: number = HASHI_CYCLE_TIME_MS + Date.now();
await sendHeartbeat("running", HEARTBEAT_URL);
for (let i = 0; i < networkConfigs.length; i++) {
if (executeTimes[i] > Date.now()) {
continue;
}
executeTimes[i] = await processNetworkConfig(networkConfigs[i], shutdownManager, emitter, executeTimes[i]);
executeTime = Math.min(executeTime, executeTimes[i]);
}
emitter.emit(BotEvents.WAITING, delayAmount);
await delay(delayAmount);
const delayMs = executeTime - Date.now();
emitter.emit(BotEvents.WAITING, delayMs);
await delay(delayMs);
}
await sendHeartbeat("stopped", HEARTBEAT_URL);
}

/**
Expand All @@ -54,37 +67,40 @@ async function processNetworkConfig(
emitter: EventEmitter,
currentDelay: number
): Promise<number> {
const { chainId, network, senders } = networkConfig;
emitter.emit(BotEvents.STARTED, chainId, network);
const { chainId, network, senders, sourceChainId } = networkConfig;
const logNetwork = sourceChainId ? `${sourceChainId}->${chainId} Hashi` : network;
emitter.emit(BotEvents.STARTED, chainId, logNetwork);
const maxBatchSize = 10; // 10 messages per batch

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

let { nonce, hashiNonce } = await initializeNonces(chainId, network, emitter);
if (nonce == null) return currentDelay;

const hashiExecutorEnabled = process.env.HASHI_EXECUTOR_ENABLED === "true";
if (hashiExecutorEnabled) {
let { nonce, hashiBlockNumber } = await initializeNonces(chainId, network, emitter);
if (sourceChainId) {
// Execute messages on Hashi
hashiNonce = await runHashiExecutor({ chainId, network, nonce: hashiNonce, emitter });
hashiBlockNumber = await runHashiExecutor({
sourceChainId,
targetChainId: chainId,
network,
blockNumber: hashiBlockNumber,
emitter,
});
await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, hashiBlockNumber, network, emitter);
return Date.now() + HASHI_CYCLE_TIME_MS;
}

const toRelayAll = senders[0] === ethers.ZeroAddress;
nonce = toRelayAll
? await relayBatch({ chainId, network, nonce, maxBatchSize, emitter })
: await relayAllFrom(chainId, network, nonce, senders, emitter);

if (nonce == null) return currentDelay;

await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, hashiNonce, network, emitter);
await updateStateFile(chainId, Math.floor(Date.now() / 1000), nonce, hashiBlockNumber, network, emitter);

if (network === Network.DEVNET) {
return 1000 * 10; // 10 seconds for devnet
return Date.now() + 1000 * 60 * 2; // 2 min for devnet
} else {
const currentTS = Math.floor(Date.now() / 1000);
const epochPeriod = getEpochPeriod(chainId);
const timeLeft = (epochPeriod - (Math.floor(currentTS / 1000) % epochPeriod)) * 1000 + 100 * 1000;
return Math.min(currentDelay, timeLeft);
return Date.now() + timeLeft;
}
}

Expand All @@ -97,6 +113,5 @@ if (require.main === module) {
shutdownManager,
emitter,
};

start(testnetRelayerConfig);
}
1 change: 1 addition & 0 deletions relayer-cli/src/utils/botEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export enum BotEvents {
HASHI_EXECUTED = "hashi_executed",
HASHI_BATCH_TXN = "hashi_batch_txn",
HASHI_EXECUTION_FAILED = "hashi_execution_failed",
HASHI_NOT_CONFIGURED = "hashi_not_configured",
}
2 changes: 1 addition & 1 deletion relayer-cli/src/utils/ethers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
VeaOutboxArbToGnosis__factory,
VeaOutboxArbToGnosisDevnet__factory,
TransactionBatcher__factory,
} from "@kleros/vea-contracts/typechain-types";
} from "../../../contracts/typechain-types";
import { getBridgeConfig, Network } from "../consts/bridgeRoutes";

function getWallet(privateKey: string, web3ProviderURL: string): Wallet {
Expand Down
9 changes: 4 additions & 5 deletions relayer-cli/src/utils/graphQueries.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import request from "graphql-request";
import { VeaOutboxArbToEth, VeaOutboxArbToGnosis } from "@kleros/vea-contracts/typechain-types";
import { VeaOutboxArbToEth, VeaOutboxArbToGnosis } from "../../../contracts/typechain-types";

async function getVeaMsgTrnx(nonce: number, inboxAddress: string) {
console.log(`Fetching transaction hashes for nonce ${nonce} from inbox ${inboxAddress}`);
try {
const subgraph = process.env.RELAYER_SUBGRAPH;
const query = `{messageSents(first: 1, where: {nonce: ${nonce}, inbox: "${inboxAddress}"}) {
id
transactionHash
}}`;
const result = (await request(`https://api.studio.thegraph.com/query/${subgraph}`, query)) as {
const result = (await request(subgraph, query)) as {
messageSents: { id: string; transactionHash: string }[];
};
return result.messageSents.map((trnx) => trnx.transactionHash);
Expand All @@ -33,7 +32,7 @@ const getCount = async (veaOutbox: VeaOutboxArbToEth | VeaOutboxArbToGnosis, cha
const stateRoot = await veaOutbox.stateRoot();

const result = (await request(
`https://api.studio.thegraph.com/query/${subgraph}`,
subgraph,
`{
snapshotSaveds(first: 1, where: { stateRoot: "${stateRoot}" }) {
count
Expand Down Expand Up @@ -65,7 +64,7 @@ const getNonceFrom = async (chainId: number, inbox: string, nonce: number, msgSe
const subgraph = process.env.RELAYER_SUBGRAPH;

const result = (await request(
`https://api.studio.thegraph.com/query/${subgraph}`,
subgraph,
`{
messageSents(
first: 1000,
Expand Down
Loading
Loading