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/happy path(claimer) in validator-cli #396

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
18 changes: 10 additions & 8 deletions validator-cli/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,30 @@ PRIVATE_KEY=
# Devnet RPCs
RPC_CHIADO=https://rpc.chiadochain.net
RPC_ARB_SEPOLIA=https://sepolia-rollup.arbitrum.io/rpc
RPC_GNOSIS=https://rpc.chiadochain.net
RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc
RPC_SEPOLIA=

# Testnet or Mainnet RPCs
RPC_ARB=
RPC_ARB=https://sepolia-rollup.arbitrum.io/rpc
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

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

RPC_ETH=
RPC_GNOSIS=https://rpc.chiadochain.net

# Testnet or Mainnet Addresses
# VEA Arbitrum to Ethereum
VEAINBOX_ARB_TO_ETH_ADDRESS=0xE12daFE59Bc3A996362d54b37DFd2BA9279cAd06
VEAOUTBOX_ARB_TO_ETH_ADDRESS=0x209BFdC6B7c66b63A8382196Ba3d06619d0F12c9
# VEA Arbitrum to GNOSIS
VEAINBOX_ARB_TO_GNOSIS_ADDRESS=0x854374483572FFcD4d0225290346279d0718240b
VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS=0x2f1788F7B74e01c4C85578748290467A5f063B0b
VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193
GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d

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

# Devnet Addresses
VEAINBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410
VEAOUTBOX_ARBSEPOLIA_TO_SEPOLIA_ADDRESS=0x906dE43dBef27639b1688Ac46532a16dc07Ce410

#For arbToGnosis bridge
VEAINBOX_ARB_TO_GNOSIS_ADDRESS=0x854374483572FFcD4d0225290346279d0718240b
VEAOUTBOX_ARB_TO_GNOSIS_ADDRESS=0x2f1788F7B74e01c4C85578748290467A5f063B0b
VEAROUTER_ARB_TO_GNOSIS_ADDRESS=0x5BE03fDE7794Bc188416ba16932510Ed1277b193
GNOSIS_AMB_ADDRESS=0x8448E15d0e706C0298dECA99F0b4744030e59d7d

TRANSACTION_BATCHER_CONTRACT_ADDRESS_SEPOLIA=0xe7953da7751063d0a41ba727c32c762d3523ade8
TRANSACTION_BATCHER_CONTRACT_ADDRESS_CHIADO=0xcC0a08D4BCC5f91ee9a1587608f7a2975EA75d73
10 changes: 10 additions & 0 deletions validator-cli/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Config } from "jest";

const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
collectCoverage: true,
collectCoverageFrom: ["**/*.ts"],
};

export default config;
8 changes: 6 additions & 2 deletions validator-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
"yarn": "4.2.2"
},
"scripts": {
"start": "npx ts-node ./src/ArbToEth/watcher.ts",
"start": "npx ts-node ./src/watcher.ts",
"start-chiado-devnet": "npx ts-node ./src/devnet/arbToChiado/happyPath.ts",
"start-sepolia-devnet": "npx ts-node ./src/devnet/arbToSepolia/happyPath.ts",
"start-sepolia-testnet": "npx ts-node ./src/ArbToEth/watcherArbToEth.ts",
"start-arbitrum-to-gnosis": "npx ts-node ./src/ArbToEth/watcherArbToGnosis.ts"
"start-arbitrum-to-gnosis": "npx ts-node ./src/ArbToEth/watcherArbToGnosis.ts",
"test": "jest --coverage"
},
"dependencies": {
"@arbitrum/sdk": "4.0.1",
Expand All @@ -28,6 +29,9 @@
"web3-batched-send": "^1.0.3"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2"
}
}
164 changes: 164 additions & 0 deletions validator-cli/src/ArbToEth/claimer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { ethers } from "ethers";
import { checkAndClaim } from "./claimer";
import { ClaimHonestState } from "../utils/claim";

describe("claimer", () => {
let veaOutbox: any;
let veaInbox: any;
let veaInboxProvider: any;
let veaOutboxProvider: any;
let emitter: any;
let mockClaim: any;
let mockGetLatestClaimedEpoch: any;
let mockDeps: any;
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();
mockDeps = {
claim: mockClaim,
epoch: 10,
epochPeriod: 10,
veaInbox,
veaInboxProvider,
veaOutboxProvider,
veaOutbox,
transactionHandler: null,
emitter,
fetchLatestClaimedEpoch: mockGetLatestClaimedEpoch,
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe("checkAndClaim", () => {
let mockTransactionHandler: any;
const mockTransactions = {
claimTxn: "0x111",
withdrawClaimDepositTxn: "0x222",
startVerificationTxn: "0x333",
verifySnapshotTxn: "0x444",
};
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();
}),
transactions: {
claimTxn: "0x0",
withdrawClaimDepositTxn: "0x0",
startVerificationTxn: "0x0",
verifySnapshotTxn: "0x0",
},
};
});
it("should return null if no claim is made for a passed epoch", async () => {
mockDeps.epoch = 7; // claimable epoch - 3
mockDeps.claim = null;
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();
});
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);
});
});
});
75 changes: 75 additions & 0 deletions validator-cli/src/ArbToEth/claimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { EventEmitter } from "events";
import { ethers } from "ethers";
import { JsonRpcProvider } from "@ethersproject/providers";
import { getClaim, ClaimHonestState } from "../utils/claim";
import { getLastClaimedEpoch } from "../utils/graphQueries";
import { ArbToEthTransactionHandler } from "./transactionHandler";
import { BotEvents } from "../utils/botEvents";
import { ClaimStruct } from "@kleros/vea-contracts/typechain-types/arbitrumToEth/VeaInboxArbToEth";
interface checkAndClaimParams {
claim: ClaimStruct | null;
epochPeriod: number;
epoch: number;
veaInbox: any;
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

Replace 'any' with specific contract types for 'veaInbox' and 'veaOutbox'

Using any for veaInbox and veaOutbox defeats the purpose of TypeScript's type safety. Replace any with the specific contract types to improve type checking and code maintainability. This change will help catch errors at compile time and provide better IntelliSense support.

Apply this diff to fix the issue:

 interface checkAndClaimParams {
   epochPeriod: number;
   epoch: number;
-  veaInbox: any;
+  veaInbox: VeaInboxArbToEth;
   veaInboxProvider: JsonRpcProvider;
-  veaOutbox: any;
+  veaOutbox: VeaOutboxArbToEth;
   veaOutboxProvider: JsonRpcProvider;
   transactionHandler: ArbToEthTransactionHandler | null;
   emitter: EventEmitter;

Also applies to: 13-13

veaInboxProvider: JsonRpcProvider;
veaOutbox: any;
veaOutboxProvider: JsonRpcProvider;
transactionHandler: ArbToEthTransactionHandler | null;
emitter: EventEmitter;
fetchClaim?: typeof getClaim;
fetchLatestClaimedEpoch?: typeof getLastClaimedEpoch;
}

export async function checkAndClaim({
claim,
epoch,
epochPeriod,
veaInbox,
veaInboxProvider,
veaOutbox,
veaOutboxProvider,
transactionHandler,
emitter,
fetchLatestClaimedEpoch = getLastClaimedEpoch,
}: checkAndClaimParams) {
let outboxStateRoot = await veaOutbox.stateRoot();
const finalizedOutboxBlock = await veaOutboxProvider.getBlock("finalized");
const claimAbleEpoch = Math.floor(finalizedOutboxBlock.timestamp / epochPeriod) - 1;
if (!transactionHandler) {
transactionHandler = new ArbToEthTransactionHandler(
epoch,
veaInbox,
veaOutbox,
veaInboxProvider,
veaOutboxProvider,
emitter,
claim
);
} else {
transactionHandler.claim = claim;
}
if (claim == null && epoch == claimAbleEpoch) {
const [savedSnapshot, claimData] = await Promise.all([veaInbox.snapshots(epoch), fetchLatestClaimedEpoch()]);
const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct the use of 'ethers.ZeroHash' to 'ethers.constants.HashZero'

The property ethers.ZeroHash does not exist. The correct constant for the zero hash in ethers.js is ethers.constants.HashZero. Update the code to use the correct constant.

Apply this diff to fix the issue:

- const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash;
+ const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.constants.HashZero;
📝 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 newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.ZeroHash;
const newMessagesToBridge: boolean = savedSnapshot != outboxStateRoot && savedSnapshot != ethers.constants.HashZero;

const lastClaimChallenged: boolean = claimData.challenged && savedSnapshot == outboxStateRoot;
if (newMessagesToBridge || lastClaimChallenged) {
await transactionHandler.makeClaim(savedSnapshot);
return transactionHandler;
}
} else if (claim != null) {
if (claim.honest == ClaimHonestState.CLAIMER) {
await transactionHandler.withdrawClaimDeposit();
return transactionHandler;
} else if (claim.honest == ClaimHonestState.NONE) {
if (claim.timestampVerification == 0) {
await transactionHandler.startVerification(finalizedOutboxBlock.timestamp);
} else {
await transactionHandler.verifySnapshot(finalizedOutboxBlock.timestamp);
}
return transactionHandler;
}
} else {
emitter.emit(BotEvents.CLAIM_EPOCH_PASSED, epoch);
}
return null;
}
Loading
Loading