Skip to content

Feat/happy path(claimer) in validator-cli #396

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

Closed
wants to merge 24 commits into from
Closed
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
15 changes: 9 additions & 6 deletions validator-cli/.env.dist
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
PRIVATE_KEY=

# Devnet RPCs
RPC_CHIADO=https://rpc.chiadochain.net
RPC_ARB_SEPOLIA=https://sepolia-rollup.arbitrum.io/rpc
RPC_SEPOLIA=
# Networks: devnet, testnet, mainnet
NETWORKS=devnet,testnet

# Testnet or Mainnet RPCs

# Devnet Owner
DEVNET_OWNER=0x5f4eC3Df9Cf2f0f1fDfCfCfCfCfCfCfCfCfCfCfC

# RPCs
RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 23, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove duplicate RPC URL configuration.

The Arbitrum RPC URL is defined twice:

  • Line 4: RPC_ARB_SEPOLIA=https://sepolia-rollup.arbitrum.io/rpc
  • Line 9: RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc

This could lead to confusion. Consider removing one of them or clearly documenting the difference if both are needed.

Also applies to: 4-4

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will use a single RPC var for both networks on a chain 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

@mani99brar Great! Using a single RPC variable per chain is a good simplification that will make the configuration more maintainable. This is a change from your previous approach where you had separate variables for devnet and testnet/mainnet usage, but it should make the codebase cleaner and reduce potential confusion. Let me know if you need any help with implementing this change.


✏️ Learnings added
Learnt from: mani99brar
PR: kleros/vea#396
File: validator-cli/.env.dist:15-15
Timestamp: 2025-03-31T09:03:04.957Z
Learning: User (mani99brar) has decided to simplify the approach by using a single RPC variable per chain instead of maintaining separate variables for devnet and testnet/mainnet environments, even when they point to the same URL.

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.


🧠 Learnings used
Learnt from: madhurMongia
PR: kleros/vea#359
File: validator-cli/.env.dist:3-4
Timestamp: 2025-03-28T01:51:52.423Z
Learning: In 'validator-cli/.env.dist', the `RPC_GNOSIS` variable may be intentionally set to `https://rpc.chiadochain.net` as an example.

Learnt from: mani99brar
PR: kleros/vea#388
File: validator-cli/.env.dist:9-11
Timestamp: 2025-03-28T01:51:48.908Z
Learning: User (mani99brar) intentionally keeps the same RPC URLs under different environment variables to distinguish between devnet and testnet/mainnet usage, and this is by design rather than an oversight.

RPC_ETH=
RPC_GNOSIS=https://rpc.chiadochain.net
Expand All @@ -20,7 +22,8 @@ VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS=0x2f1788F7B74e01c4C85578748290467A5f063B0b
VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193
GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d

VEAOUTBOX_CHAIN_ID=421611
VEAOUTBOX_CHAINS=11155111,421611
VEAOUTBOX_SUBGRAPH=https://api.studio.thegraph.com/query/user/outbox-arb-sep/version/latest

# Devnet Addresses
VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410
Expand Down
21 changes: 5 additions & 16 deletions validator-cli/ecosystem.config.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,16 @@
module.exports = {
apps: [
{
name: "chiado-devnet",
script: "yarn",
args: "start-chiado-devnet",
interpreter: "/bin/bash",
log_date_format: "YYYY-MM-DD HH:mm Z",
watch: false,
autorestart: false,
env: {
NODE_ENV: "development",
},
},
{
name: "start-sepolia-devnet",
script: "yarn",
args: "start-sepolia-devnet",
interpreter: "/bin/bash",
name: "validator-cli",
script: "./src/watcher.ts",
interpreter: "../node_modules/.bin/ts-node",
interpreter_args: "--project tsconfig.json -r tsconfig-paths/register",
log_date_format: "YYYY-MM-DD HH:mm Z",
watch: false,
autorestart: false,
env: {
NODE_ENV: "development",
TS_NODE_PROJECT: "./tsconfig.json",
},
},
],
Expand Down
6 changes: 2 additions & 4 deletions validator-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@
"@kleros/vea-contracts": "workspace:^",
"@typechain/ethers-v6": "^0.5.1",
"dotenv": "^16.4.5",
"pm2": "^5.2.2",
"typescript": "^4.9.5",
"web3": "^4.16.0",
"web3-batched-send": "^1.0.3"
"pm2": "^6.0.5",
"typescript": "^4.9.5"
},
"devDependencies": {
"@types/jest": "^29.5.14",
Expand Down
234 changes: 234 additions & 0 deletions validator-cli/src/ArbToEth/claimer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { ethers } from "ethers";
import { checkAndClaim, CheckAndClaimParams } from "./claimer";
import { ClaimHonestState } from "../utils/claim";
import { Network } from "../consts/bridgeRoutes";

describe("claimer", () => {
const NETWORK = Network.DEVNET;
let veaOutbox: any;
let veaInbox: any;
let veaInboxProvider: any;
let veaOutboxProvider: any;
let emitter: any;
let mockClaim: any;
let mockGetLatestClaimedEpoch: any;
let mockGetTransactionHandler: any;
let mockDeps: CheckAndClaimParams;

let mockTransactionHandler: any;
const mockTransactions = {
claimTxn: "0x111",
withdrawClaimDepositTxn: "0x222",
startVerificationTxn: "0x333",
verifySnapshotTxn: "0x444",
devnetAdvanceStateTxn: "0x555",
};
beforeEach(() => {
mockClaim = {
stateRoot: "0x1234",
claimer: "0xFa00D29d378EDC57AA1006946F0fc6230a5E3288",
timestampClaimed: 1234,
timestampVerification: 0,
blocknumberVerification: 0,
honest: 0,
challenger: ethers.ZeroAddress,
};
veaInbox = {
snapshots: jest.fn().mockResolvedValue(mockClaim.stateRoot),
};

veaOutbox = {
stateRoot: jest.fn().mockResolvedValue(mockClaim.stateRoot),
};
veaOutboxProvider = {
getBlock: jest.fn().mockResolvedValue({ number: 0, timestamp: 110 }),
};
emitter = {
emit: jest.fn(),
};

mockGetLatestClaimedEpoch = jest.fn();
mockGetTransactionHandler = jest.fn().mockReturnValue(function DummyTransactionHandler(params: any) {
// Return an object that matches our expected transaction handler.
return mockTransactionHandler;
});
mockDeps = {
chainId: 0,
claim: mockClaim,
network: NETWORK,
epoch: 10,
epochPeriod: 10,
veaInbox,
veaInboxProvider,
veaOutboxProvider,
veaOutbox,
transactionHandler: null,
emitter,
fetchLatestClaimedEpoch: mockGetLatestClaimedEpoch,
now: 110000, // (epoch+ 1) * epochPeriod * 1000 for claimable epoch
};

mockTransactionHandler = {
withdrawClaimDeposit: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.withdrawClaimDepositTxn = mockTransactions.withdrawClaimDepositTxn;
return Promise.resolve();
}),
makeClaim: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.claimTxn = mockTransactions.claimTxn;
return Promise.resolve();
}),
startVerification: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.startVerificationTxn = mockTransactions.startVerificationTxn;
return Promise.resolve();
}),
verifySnapshot: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.verifySnapshotTxn = mockTransactions.verifySnapshotTxn;
return Promise.resolve();
}),
transactions: {
claimTxn: "0x0",
withdrawClaimDepositTxn: "0x0",
startVerificationTxn: "0x0",
verifySnapshotTxn: "0x0",
},
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe("checkAndClaim", () => {
beforeEach(() => {
mockTransactionHandler = {
withdrawClaimDeposit: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.withdrawClaimDepositTxn = mockTransactions.withdrawClaimDepositTxn;
return Promise.resolve();
}),
makeClaim: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.claimTxn = mockTransactions.claimTxn;
return Promise.resolve();
}),
startVerification: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.startVerificationTxn = mockTransactions.startVerificationTxn;
return Promise.resolve();
}),
verifySnapshot: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.verifySnapshotTxn = mockTransactions.verifySnapshotTxn;
return Promise.resolve();
}),
devnetAdvanceState: jest.fn().mockImplementation(() => {
mockTransactionHandler.transactions.devnetAdvanceStateTxn = mockTransactions.devnetAdvanceStateTxn;
return Promise.resolve();
}),
transactions: {
claimTxn: "0x0",
withdrawClaimDepositTxn: "0x0",
startVerificationTxn: "0x0",
verifySnapshotTxn: "0x0",
},
};
mockGetTransactionHandler = jest.fn().mockReturnValue(function DummyTransactionHandler(param: any) {
return mockTransactionHandler;
});
mockDeps.fetchTransactionHandler = mockGetTransactionHandler;
});
it("should return null if no claim is made for a passed epoch", async () => {
mockDeps.epoch = 7; // claimable epoch - 3
mockDeps.claim = null;

mockDeps.fetchTransactionHandler = mockGetTransactionHandler;
const result = await checkAndClaim(mockDeps);
expect(result).toBeNull();
});
it("should return null if no snapshot is saved on the inbox for a claimable epoch", async () => {
veaInbox.snapshots = jest.fn().mockResolvedValue(ethers.ZeroHash);
mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({
challenged: false,
stateroot: "0x1111",
});
mockDeps.claim = null;
mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch;
const result = await checkAndClaim(mockDeps);
expect(result).toBeNull();
});
it("should return null if there are no new messages in the inbox", async () => {
veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot);
mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({
challenged: false,
stateroot: "0x1111",
});
mockDeps.claim = null;
mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch;
const result = await checkAndClaim(mockDeps);
expect(result).toBeNull();
});
describe("devnet", () => {
beforeEach(() => {
mockDeps.network = Network.DEVNET;
});
it("should make a valid claim and advance state", async () => {
veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890");
mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({
challenged: false,
stateroot: mockClaim.stateRoot,
});
mockDeps.transactionHandler = mockTransactionHandler;
mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch;
mockDeps.claim = null;
mockDeps.veaInbox = veaInbox;
const result = await checkAndClaim(mockDeps);
expect(result.transactions.devnetAdvanceStateTxn).toBe(mockTransactions.devnetAdvanceStateTxn);
});
});
describe("testnet", () => {
beforeEach(() => {
mockDeps.network = Network.TESTNET;
});
it("should make a valid claim if no claim is made", async () => {
veaInbox.snapshots = jest.fn().mockResolvedValue("0x7890");
mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({
challenged: false,
stateroot: mockClaim.stateRoot,
});
mockDeps.transactionHandler = mockTransactionHandler;
mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch;
mockDeps.claim = null;
mockDeps.veaInbox = veaInbox;
const result = await checkAndClaim(mockDeps);
expect(result.transactions.claimTxn).toBe(mockTransactions.claimTxn);
});
it("should make a valid claim if last claim was challenged", async () => {
veaInbox.snapshots = jest.fn().mockResolvedValue(mockClaim.stateRoot);
mockGetLatestClaimedEpoch = jest.fn().mockResolvedValue({
challenged: true,
stateroot: mockClaim.stateRoot,
});
mockDeps.transactionHandler = mockTransactionHandler;
mockDeps.fetchLatestClaimedEpoch = mockGetLatestClaimedEpoch;
mockDeps.claim = null;
mockDeps.veaInbox = veaInbox;
const result = await checkAndClaim(mockDeps);
expect(result.transactions.claimTxn).toEqual(mockTransactions.claimTxn);
});
it("should withdraw claim deposit if claimer is honest", async () => {
mockDeps.transactionHandler = mockTransactionHandler;
mockClaim.honest = ClaimHonestState.CLAIMER;
const result = await checkAndClaim(mockDeps);
expect(result.transactions.withdrawClaimDepositTxn).toEqual(mockTransactions.withdrawClaimDepositTxn);
});
it("should start verification if verification is not started", async () => {
mockDeps.transactionHandler = mockTransactionHandler;
mockClaim.honest = ClaimHonestState.NONE;
const result = await checkAndClaim(mockDeps);
expect(result.transactions.startVerificationTxn).toEqual(mockTransactions.startVerificationTxn);
});
it("should verify snapshot if verification is started", async () => {
mockDeps.transactionHandler = mockTransactionHandler;
mockClaim.honest = ClaimHonestState.NONE;
mockClaim.timestampVerification = 1234;
mockDeps.claim = mockClaim;
const result = await checkAndClaim(mockDeps);
expect(result.transactions.verifySnapshotTxn).toEqual(mockTransactions.verifySnapshotTxn);
});
});
});
});
Loading
Loading