-
Couldn't load subscription status.
- Fork 7
Fix/handle sent snapshots #435
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
base: dev
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for veascan ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
WalkthroughThis pull request introduces support for VEA outbox channels in snapshot and claim resolution logic, adjusts epoch period configurations, adds snapshot saving period constraints, refactors transaction submission checks, and includes snapshot hash verification against stored onchain data. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
validator-cli/src/watcher.ts (1)
144-170: Handler key mismatch causes repeated re-instantiation (state loss).You read with
transactionHandlers[currentEpoch](line 146) but store withtransactionHandlers[txnHandlerKey](line 168). This drops state between iterations and can trigger duplicate submissions.const txnHandlerKey = `${routeConfig[network].veaInbox.address}_${currentEpoch}`; const transactionHandler = - transactionHandlers[currentEpoch] || + transactionHandlers[txnHandlerKey] || new TransactionHandler({ network, epoch: currentEpoch, veaInbox, veaOutbox, veaInboxProvider, veaOutboxProvider, veaRouterProvider, emitter, });validator-cli/src/helpers/snapshot.test.ts (1)
103-116: fetchLastSavedMessage mock shape likely incorrect vs production.isSnapshotNeeded expects an object with
{ id, stateRoot }(used to derive message index and last saved snapshot). Returning a string like "message-0" can mask issues.Please update test mocks to the expected shape.
- fetchLastSavedMessage = jest.fn().mockResolvedValue("message-0"); + fetchLastSavedMessage = jest.fn().mockResolvedValue({ + id: "message-0", + stateRoot: "0xdeadbeef", +});Based on snapshot.ts snippet.
🧹 Nitpick comments (15)
validator-cli/src/utils/graphQueries.ts (2)
164-171: Guard against empty results and limit nested results to 1 item.Accessing
result.snapshots[0].fallback[0]will throw when no snapshots/fallbacks exist. Also, you can ask The Graph for only the latest fallback.Apply:
- snapshots(where: {epoch: ${epoch}, inbox_: { id: "${veaInbox}" }}) { - fallback(orderBy: timestamp, orderDirection: desc){ + snapshots(where: {epoch: ${epoch}, inbox_: { id: "${veaInbox}" }}) { + fallback(first: 1, orderBy: timestamp, orderDirection: desc){ txHash } }And add a null-safe return:
- return result.snapshots[0].fallback[0]; + const [firstSnapshot] = result.snapshots ?? []; + const [firstFallback] = firstSnapshot?.fallback ?? []; + return firstFallback;
178-185: Consider backward-compatibility forstateRoot.If older snapshots lack
stateRoot, the static type should reflect that to avoid accidentalundefinedaccess at runtime.type SnapshotSavedResponse = { snapshots: { - stateRoot: string; + stateRoot?: string; messages: { id: string; }[]; }[]; };validator-cli/src/consts/bridgeRoutes.ts (3)
90-95: TypesnapshotSavingPeriodexplicitly and document units.Improves clarity and prevents accidental key drift.
-// For the remaining time in an epoch the bot should save snapshots -const snapshotSavingPeriod = { +// For the remaining time (seconds) in an epoch the bot should save snapshots +const snapshotSavingPeriod: Record<Network, number> = { [Network.DEVNET]: 90, // 1m 30s [Network.TESTNET]: 600, // 10 mins };
96-100: Improve error message and return the local variable.Tiny polish; aids debugging without adding new validation.
const getBridgeConfig = (chainId: number): Bridge => { const bridge = bridges[chainId]; - if (!bridge) throw new Error(`Bridge not found for chain`); - return bridges[chainId]; + if (!bridge) throw new Error(`Bridge not found for chainId=${chainId}`); + return bridge; };Based on learnings.
14-23: Optional: tightenBridgeshapes (env-var fields).If
strictNullChecksis on,process.env.*arestring | undefined. Consider asserting at load time or widening types here tostring | undefinedand validate upstream. Leaving as-is is acceptable if upper layers already validate.Based on learnings.
validator-cli/src/watcher.ts (2)
17-17: Make RPC block range configurable (and possibly per-chain).100 may be too conservative/too aggressive depending on provider. Suggest env override and per-network defaults.
-const RPC_BLOCK_LIMIT = 100; // RPC_BLOCK_LIMIT is the limit of blocks that can be queried at once +const RPC_BLOCK_LIMIT = Number(process.env.RPC_BLOCK_LIMIT ?? 100); // blocks per getLogs/queryOptionally, add a per-chain map if providers differ.
139-141: Optional: derivecurrentEpochfrom chain time, not local clock.Local clock skew can misalign epoch. You already have outbox timestamps available.
-const currentEpoch = Math.floor(Date.now() / (1000 * routeConfig[network].epochPeriod)); +const latestOutboxBlock = await veaOutboxProvider.getBlock("latest"); +const currentEpoch = Math.floor(latestOutboxBlock.timestamp / routeConfig[network].epochPeriod);validator-cli/README.md (2)
32-33: Example command should include the script path for consistency.Use the same invocation style in both examples to avoid confusion.
-`pm2 start -- --saveSnapshot` +`pm2 start dist/watcher.js -- --saveSnapshot`
18-27: Clarify defaults and flag interactions.Document the default for --path and how --saveSnapshot behaves in challenger-only mode (ignored or error).
- Add: “Default: --path=both.”
- Add: “--saveSnapshot is only used when the bridger path is active.”
validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts (1)
63-67: Guard against zero/too‑low fee caps from profitability heuristic.If deposit/(gas*6) underflows to 0, tx may never land. Provide a sane floor or fallback to provider fee data.
-const maxFeePerGas = deposit / (toBigInt(gasEstimate) * BigInt(6)); -let maxPriorityFeePerGas = BigInt(6_667_000_000_000); +const profitabilityCap = deposit / (toBigInt(gasEstimate) * BigInt(6)); +const minFee = BigInt(1_000_000_000); // 1 gwei floor; tune per network +const maxFeePerGas = profitabilityCap > 0n ? profitabilityCap : minFee; +let maxPriorityFeePerGas = 6_667_000_000_000n; if (maxPriorityFeePerGas > maxFeePerGas) { maxPriorityFeePerGas = maxFeePerGas; }Confirm deposit is in wei (bigint) for all routes so the division stays in consistent units.
validator-cli/src/utils/claim.test.ts (1)
97-99: Typo in comment.There’s a stray “ß” in the VerificationStarted comment; remove to avoid noise.
- .mockImplementationOnce(() => Promise.resolve([])); // For VerificationStartedß + .mockImplementationOnce(() => Promise.resolve([])); // For VerificationStartedvalidator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts (1)
68-75: Fee cap heuristic: add floor/ceil to avoid non‑includable txs.Same concern as Arb→Eth: profitability division can yield 0; set a minimum and optionally cap priority fee.
-const maxFeePerGasProfitable = deposit / (toBigInt(gasEstimate) * BigInt(6)); -let maxPriorityFeePerGasMEV = BigInt(6667000000000); +const cap = deposit / (toBigInt(gasEstimate) * 6n); +const minFee = 1_000_000_000n; // 1 gwei floor +const maxFeePerGasProfitable = cap > 0n ? cap : minFee; +let maxPriorityFeePerGasMEV = 6_667_000_000_000n; if (maxPriorityFeePerGasMEV > maxFeePerGasProfitable) { maxPriorityFeePerGasMEV = maxFeePerGasProfitable; }Verify deposit unit is wei across networks to ensure fee math correctness.
validator-cli/src/helpers/snapshot.test.ts (2)
124-125: Prefer ethers.ZeroHash over "0x0" to match contract zeros.Literal "0x0" may not equal ZeroHash. Use the canonical constant to prevent false negatives.
- veaInbox.snapshots.mockResolvedValue("0x0"); + const { ZeroHash } = await import("ethers"); + veaInbox.snapshots.mockResolvedValue(ZeroHash);
193-201: Test name reads opposite of its assertion.It asserts that no snapshot is saved when snapshotNeeded is false. Rename for clarity.
-it("should not save snapshot if snapshot is needed", async () => { +it("should not save snapshot if snapshot is NOT needed", async () => {validator-cli/src/helpers/validator.ts (1)
77-85: Early-exit is fine; add stale-claim guard before withdrawing.
claim.honest !== 0short‑circuits correctly, butclaimmay be stale. Before callingwithdrawChallengeDeposit(Line 81), re‑fetchhonestor rely on a quick resolve-state check to avoid unnecessary reverts if state flipped post-fetch.Would you prefer a lightweight re-query of
claimHashes/claimhere, or to let the handler’s idempotency handle no-ops?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
validator-cli/README.md(2 hunks)validator-cli/src/consts/bridgeRoutes.ts(6 hunks)validator-cli/src/helpers/snapshot.test.ts(11 hunks)validator-cli/src/helpers/snapshot.ts(4 hunks)validator-cli/src/helpers/validator.ts(5 hunks)validator-cli/src/utils/botEvents.ts(1 hunks)validator-cli/src/utils/claim.test.ts(5 hunks)validator-cli/src/utils/claim.ts(5 hunks)validator-cli/src/utils/graphQueries.ts(3 hunks)validator-cli/src/utils/logger.ts(1 hunks)validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts(5 hunks)validator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts(5 hunks)validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts(5 hunks)validator-cli/src/watcher.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (11)
📚 Learning: 2024-12-02T10:16:56.825Z
Learnt from: madhurMongia
PR: kleros/vea#359
File: validator-cli/src/ArbToEth/watcherArbToGnosis.ts:829-840
Timestamp: 2024-12-02T10:16:56.825Z
Learning: In the `validator-cli/src/ArbToEth/watcherArbToGnosis.ts` file, avoid adding redundant error handling in functions like `reconstructChallengeProgress` when error handling is already adequately managed.
Applied to files:
validator-cli/src/utils/transactionHandlers/arbToEthHandler.tsvalidator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts
📚 Learning: 2025-06-05T12:17:22.931Z
Learnt from: mani99brar
PR: kleros/vea#424
File: validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts:128-131
Timestamp: 2025-06-05T12:17:22.931Z
Learning: In the BaseTransactionHandler class, all transaction properties are expected to be initialized to null. When subclasses like ArbToEthDevnetTransactionHandler use spread syntax to extend the transactions object (e.g., `{ ...this.transactions, devnetAdvanceStateTxn: null }`), there's no issue with state loss since the base transactions are null initially.
Applied to files:
validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts
📚 Learning: 2024-12-09T09:42:34.067Z
Learnt from: mani99brar
PR: kleros/vea#370
File: bridger-cli/src/utils/transactionHandler.ts:64-77
Timestamp: 2024-12-09T09:42:34.067Z
Learning: In the `TransactionHandler` class (`bridger-cli/src/utils/transactionHandler.ts`), it's acceptable to let methods like `makeClaim` fail without additional error handling.
Applied to files:
validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts
📚 Learning: 2024-11-27T04:18:05.872Z
Learnt from: mani99brar
PR: kleros/vea#344
File: validator-cli/src/ArbToEth/watcherArbToEth.ts:732-745
Timestamp: 2024-11-27T04:18:05.872Z
Learning: In `validator-cli/src/ArbToEth/watcherArbToEth.ts`, input validation inside the `hashClaim` function is unnecessary because the upper layer ensures valid input before calling it.
Applied to files:
validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts
📚 Learning: 2025-06-05T12:17:21.782Z
Learnt from: mani99brar
PR: kleros/vea#424
File: validator-cli/src/helpers/claimer.ts:72-76
Timestamp: 2025-06-05T12:17:21.782Z
Learning: In validator-cli/src/helpers/claimer.ts, the outboxStateRoot captured at the beginning of the checkAndClaim function won't change for the epoch being claimed, so there's no race condition concern when reusing it in makeClaim/makeClaimDevnet functions.
Applied to files:
validator-cli/src/utils/transactionHandlers/arbToEthHandler.tsvalidator-cli/src/helpers/snapshot.test.tsvalidator-cli/src/watcher.tsvalidator-cli/src/utils/claim.tsvalidator-cli/src/utils/transactionHandlers/arbToGnosisHandler.tsvalidator-cli/src/utils/claim.test.tsvalidator-cli/src/helpers/validator.ts
📚 Learning: 2024-11-20T11:50:15.304Z
Learnt from: madhurMongia
PR: kleros/vea#359
File: contracts/src/test/ArbToGnosis/VeaInboxArbToGnosisMock.sol:27-38
Timestamp: 2024-11-20T11:50:15.304Z
Learning: In `VeaInboxArbToGnosisMock.sendSnapshot`, `epochPeriod` is guaranteed to be non-zero, so a check for `epochPeriod > 0` is unnecessary.
Applied to files:
validator-cli/src/helpers/snapshot.tsvalidator-cli/src/helpers/snapshot.test.ts
📚 Learning: 2024-12-09T10:54:57.068Z
Learnt from: mani99brar
PR: kleros/vea#370
File: bridger-cli/src/utils/claim.ts:56-69
Timestamp: 2024-12-09T10:54:57.068Z
Learning: In the TypeScript file 'bridger-cli/src/utils/claim.ts', the upper layers handle input validation for the 'hashClaim' function. Therefore, explicit input validation within 'hashClaim' is not necessary.
Applied to files:
validator-cli/src/utils/claim.ts
📚 Learning: 2024-12-10T04:59:10.224Z
Learnt from: mani99brar
PR: kleros/vea#370
File: bridger-cli/src/utils/claim.ts:23-32
Timestamp: 2024-12-10T04:59:10.224Z
Learning: In `bridger-cli/src/utils/claim.ts`, within the `fetchClaim` function, when `claimData` is undefined, avoid initializing it with default values, as this can result in incorrect claim values and make issues harder to identify.
Applied to files:
validator-cli/src/utils/claim.ts
📚 Learning: 2024-11-27T10:48:48.433Z
Learnt from: madhurMongia
PR: kleros/vea#359
File: validator-cli/src/ArbToEth/watcherArbToGnosis.ts:24-24
Timestamp: 2024-11-27T10:48:48.433Z
Learning: In the `validator-cli` codebase, both `arbitrumToEth` and `arbitrumToGnosis` modules use the same `ClaimStruct`, so importing `ClaimStruct` from either module is acceptable.
Applied to files:
validator-cli/src/utils/claim.tsvalidator-cli/src/utils/claim.test.ts
📚 Learning: 2024-12-10T08:00:35.645Z
Learnt from: mani99brar
PR: kleros/vea#370
File: bridger-cli/src/consts/bridgeRoutes.ts:28-30
Timestamp: 2024-12-10T08:00:35.645Z
Learning: In `bridger-cli/src/consts/bridgeRoutes.ts`, additional validation in the `getBridgeConfig` function is unnecessary because error handling and validation are managed by upper layers in the application.
Applied to files:
validator-cli/src/consts/bridgeRoutes.ts
📚 Learning: 2024-12-09T09:40:28.400Z
Learnt from: mani99brar
PR: kleros/vea#370
File: bridger-cli/src/utils/transactionHandler.ts:13-13
Timestamp: 2024-12-09T09:40:28.400Z
Learning: In `bridger-cli/src/utils/transactionHandler.ts`, the `veaOutbox` implementations differ for each chain, but a common interface should be defined for type checks to enhance type safety.
Applied to files:
validator-cli/src/consts/bridgeRoutes.ts
🧬 Code graph analysis (2)
validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts (1)
validator-cli/src/utils/errors.ts (1)
ClaimNotSetError(70-70)
validator-cli/src/helpers/snapshot.test.ts (2)
validator-cli/src/helpers/snapshot.ts (2)
isSnapshotNeeded(59-98)saveSnapshot(28-57)validator-cli/src/consts/bridgeRoutes.ts (2)
snapshotSavingPeriod(102-102)Network(102-102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Analyze (javascript)
- GitHub Check: dependency-review
- GitHub Check: test
🔇 Additional comments (13)
validator-cli/src/utils/logger.ts (1)
144-146: LGTM: added log for already-resolved claims.Event wiring and message are clear and consistent.
validator-cli/src/utils/botEvents.ts (1)
35-35: LGTM: new BotEvents member.
CLAIM_ALREADY_RESOLVEDis well-named and used by logger/validator.validator-cli/src/watcher.ts (1)
160-165: LGTM: passveaOutboxinto saveSnapshot.Matches the broader PR changes adding outbox-aware snapshot logic.
validator-cli/src/utils/claim.test.ts (2)
235-279: Good: pass veaOutbox and fetchSentSnapshotData through resolve-state paths.This aligns tests with production signatures and covers the cross‑channel check.
299-307: Nice negative test for incorrect snapshot hash.Covers the regression where a wrong snapshot was sent but execution remains pending.
validator-cli/src/utils/transactionHandlers/arbToGnosisHandler.ts (1)
44-46: Unified gating applied correctly.Early returns keep methods idempotent and avoid duplicate submissions.
Also applies to: 60-62, 88-90, 101-103, 137-139
validator-cli/src/helpers/snapshot.test.ts (2)
7-7: Good: include veaOutbox in tests.State root checks across inbox/outbox are now exercised.
Also applies to: 21-23
220-247: Devnet time-window logic LGTM.Dynamic calculation using snapshotSavingPeriod improves robustness across config changes.
validator-cli/src/utils/transactionHandlers/arbToEthHandler.ts (1)
29-31: Centralized gating pattern verified—no legacy status checks found outside base class.The script output confirms that direct status comparisons are properly encapsulated within
baseTransactionHandler.ts, and no orphanedTransactionStatus.NOT_MADE|EXPIREDor directcheckTransactionStatus()calls exist in handler implementations. The early-return pattern viatoSubmitTransaction()is consistently applied as intended.validator-cli/src/helpers/validator.ts (1)
106-112: veaOutbox propagation through resolve flow looks good.Signature and call-site updates are consistent, enabling cross‑channel checks during resolution.
Also applies to: 154-160, 168-174
validator-cli/src/helpers/snapshot.ts (2)
40-46: Time gating reads well.Epoch boundary wait using
snapshotSavingPeriod[network]is sensible to avoid early saves.
47-53: Good: include epochPeriod in decision; cross‑check with current outbox stateRoot.Passing
epochPeriodand checkinginbox.snapshots(epochNow)vsoutbox.stateRoot()is a solid additional guard.Also applies to: 89-96
validator-cli/src/utils/claim.ts (1)
66-74: Reject the review comment - it conflicts with the established codebase pattern.The current implementation is correct and aligns with the documented learning for this codebase. Here's why:
Contract Type:
veaOutboxis typed asany(line 24), not a properly typed contract interface.Current Implementation is Correct: The direct log property access correctly extracts event data:
claimLogs[0].data→_stateRoot(not indexed, stored in data field) ✓claimLogs[0].topics[1]→_claimer(indexed, topics[1]) ✓challengeLogs[0].topics[2]→_challenger(indexed, topics[2]) ✓Learning Applies: Per the retrieved learning from mani99brar (PR #396), "direct access to log properties is preferred when the contract variable is typed as
any, rather than using interface parsing methods like parseLog()." This is exactly the case here.Why parseLog() Isn't Recommended Here:
- The contract is
anytyped, so callingveaOutbox.interface.parseLog()would fail or be unreliable- Direct property access is the practical solution for
anytyped variables- The current implementation correctly handles the indexed/non-indexed field distinction
The code does not need changes. The review comment misses the specific context that this codebase intentionally uses direct log access for
anytyped contracts.Likely an incorrect or invalid review comment.
| it.only("should return true if claim was missed in previous epoch", async () => { | ||
| count = 1; | ||
| let currentCount = 3; | ||
| veaInbox.count.mockResolvedValue(currentCount); | ||
| fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3"); | ||
| veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed")); | ||
| veaOutbox.stateRoot.mockResolvedValue("0xabcde"); | ||
| veaInbox.snapshots.mockResolvedValue("0x0"); | ||
| const params = { | ||
| chainId, | ||
| veaInbox, | ||
| veaOutbox, | ||
| count, | ||
| fetchLastSavedMessage, | ||
| } as any; | ||
| expect(isSnapshotNeeded(params)).resolves.toEqual({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Remove it.only to avoid running a single test only.
This will cripple the test suite.
-it.only("should return true if claim was missed in previous epoch", async () => {
+it("should return true if claim was missed in previous epoch", async () => {Run this to detect any lingering “only” in tests:
🏁 Script executed:
#!/bin/bash
rg -nP '\b(it|describe|test)\.only\(' -g 'validator-cli/**' -SLength of output: 132
Remove it.only() from test at line 117 to restore full test suite execution.
The it.only() prevents other tests from running and will break the test suite. Change line 117 from it.only(...) to it(...).
-it.only("should return true if claim was missed in previous epoch", async () => {
+it("should return true if claim was missed in previous epoch", async () => {📝 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.
| it.only("should return true if claim was missed in previous epoch", async () => { | |
| count = 1; | |
| let currentCount = 3; | |
| veaInbox.count.mockResolvedValue(currentCount); | |
| fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3"); | |
| veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed")); | |
| veaOutbox.stateRoot.mockResolvedValue("0xabcde"); | |
| veaInbox.snapshots.mockResolvedValue("0x0"); | |
| const params = { | |
| chainId, | |
| veaInbox, | |
| veaOutbox, | |
| count, | |
| fetchLastSavedMessage, | |
| } as any; | |
| expect(isSnapshotNeeded(params)).resolves.toEqual({ | |
| it("should return true if claim was missed in previous epoch", async () => { | |
| count = 1; | |
| let currentCount = 3; | |
| veaInbox.count.mockResolvedValue(currentCount); | |
| fetchLastSavedMessage = jest.fn().mockResolvedValue("message-3"); | |
| veaInbox.queryFilter.mockRejectedValue(new Error("queryFilter failed")); | |
| veaOutbox.stateRoot.mockResolvedValue("0xabcde"); | |
| veaInbox.snapshots.mockResolvedValue("0x0"); | |
| const params = { | |
| chainId, | |
| veaInbox, | |
| veaOutbox, | |
| count, | |
| fetchLastSavedMessage, | |
| } as any; | |
| expect(isSnapshotNeeded(params)).resolves.toEqual({ |
🤖 Prompt for AI Agents
In validator-cli/src/helpers/snapshot.test.ts around lines 117 to 132, the test
is marked with it.only which prevents the rest of the test suite from running;
change the test declaration from it.only(...) to it(...) so the test runs
normally with the full suite, save the file, and run the test runner to verify
all tests execute.
| let lastSavedSnapshot: string; | ||
| try { | ||
| const saveSnapshotLogs = await veaInbox.queryFilter(veaInbox.filters.SnapshotSaved()); | ||
| lastSavedCount = Number(saveSnapshotLogs[saveSnapshotLogs.length - 1].args[2]); | ||
| lastSavedSnapshot = saveSnapshotLogs[saveSnapshotLogs.length - 1].args[1]; | ||
| } catch { | ||
| const veaInboxAddress = await veaInbox.getAddress(); | ||
| const lastSavedMessageId = await fetchLastSavedMessage(veaInboxAddress, chainId); | ||
| const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage( | ||
| veaInboxAddress, | ||
| chainId | ||
| ); | ||
| const messageIndex = extractMessageIndex(lastSavedMessageId); | ||
| lastSavedSnapshot = lastSavedStateRoot; | ||
| // adding 1 to the message index to get the last saved count | ||
| lastSavedCount = messageIndex + 1; | ||
| lastSavedCount = messageIndex; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Handle empty Graph response and align comment with logic.
- If
fetchLastSavedMessagereturns null/undefined, destructuring throws. Provide safe defaults (lastSavedCount = 0,lastSavedSnapshot = ZeroHash). - The comment says “adding 1 to the message index,” but code doesn’t add 1. Either remove the note or implement the +1 if correct.
Apply this diff:
} catch {
const veaInboxAddress = await veaInbox.getAddress();
- const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage(
- veaInboxAddress,
- chainId
- );
- const messageIndex = extractMessageIndex(lastSavedMessageId);
- lastSavedSnapshot = lastSavedStateRoot;
- // adding 1 to the message index to get the last saved count
- lastSavedCount = messageIndex;
+ const lastSaved = await fetchLastSavedMessage(veaInboxAddress, chainId);
+ if (!lastSaved) {
+ lastSavedSnapshot = ZeroHash;
+ lastSavedCount = 0;
+ } else {
+ const messageIndex = extractMessageIndex(lastSaved.id);
+ lastSavedSnapshot = lastSaved.stateRoot;
+ // If the index is zero-based, keep as-is; otherwise, add +1.
+ lastSavedCount = messageIndex; // update comment accordingly if needed
+ }
}Confirm whether the graph’s message index is zero- or one-based so we can fix the comment or add +1 appropriately.
I need to verify the concerns raised in the review comment. Let me explore the codebase to understand the context.
🏁 Script executed:
#!/bin/bash
# Find and display the snapshot.ts file content around the target lines
fd snapshot.ts --type f | head -5Length of output: 37
🏁 Script executed:
#!/bin/bash
# Look for fetchLastSavedMessage function definition
rg -n "fetchLastSavedMessage" --type ts -A 10 -B 2Length of output: 12707
🏁 Script executed:
#!/bin/bash
# Search for ZeroHash constant
rg -n "ZeroHash" --type ts -B 2 -A 2Length of output: 5106
🏁 Script executed:
#!/bin/bash
# Search for tests related to snapshot
fd -e test.ts -e spec.ts | xargs grep -l "snapshot" 2>/dev/null | head -3Length of output: 128
🏁 Script executed:
#!/bin/bash
# Check for Graph-related imports and message index usage
rg -n "messageIndex\|message.*index" --type ts -C 3Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find getLastMessageSaved implementation
rg -n "getLastMessageSaved" --type ts -A 15 -B 2Length of output: 5164
🏁 Script executed:
#!/bin/bash
# Find extractMessageIndex implementation
rg -n "extractMessageIndex" --type ts -A 10 -B 2Length of output: 2109
🏁 Script executed:
#!/bin/bash
# Check graphQueries file
fd graphQueries --type fLength of output: 39
Fix unsafe destructuring and misleading comment in catch block.
The code at lines 80-88 has two issues:
-
Unsafe destructuring: getLastMessageSaved can return undefined when the graph query returns fewer than 2 snapshots or no messages, but the current code destructures without checking for null, which will throw an error.
-
Misleading comment: The comment at line 86 states "adding 1 to the message index" but the code assigns lastSavedCount = messageIndex directly without incrementing.
Apply the suggested diff to add a null check and provide safe defaults (using the already-imported ZeroHash constant and initializing count to 0). Update the comment to reflect the actual logic or clarify if +1 is needed based on message index semantics.
| sentSnapshotLogs.sort((a, b) => | ||
| a.blockNumber !== b.blockNumber ? b.blockNumber - a.blockNumber : b.logIndex - a.logIndex | ||
| ); | ||
| // Add logic to check if the sent message has the actual claimHash or not | ||
| const expectedClaimHash = await fetchSentSnapshotData( | ||
| sentSnapshotLogs[0].transactionHash, | ||
| veaInboxProvider, | ||
| veaInbox.interface | ||
| ); | ||
| const claimHash = await veaOutbox.claimHashes(epoch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Guard against null expectedClaimHash and fail fast when mismatched.
If getSentSnapshotData can return null, comparisons must short‑circuit. Also, return early on mismatch to avoid ambiguous state.
Apply this diff:
- const expectedClaimHash = await fetchSentSnapshotData(
+ const expectedClaimHash = await fetchSentSnapshotData(
sentSnapshotLogs[0].transactionHash,
veaInboxProvider,
veaInbox.interface
);
- const claimHash = await veaOutbox.claimHashes(epoch);
-
- if (claimHash === expectedClaimHash) {
+ if (!expectedClaimHash) return claimResolveState;
+ const claimHash = await veaOutbox.claimHashes(epoch);
+ if (claimHash === expectedClaimHash) {
claimResolveState.sendSnapshot.status = true;
claimResolveState.sendSnapshot.txHash = sentSnapshotLogs[0].transactionHash;
} else {
return claimResolveState;
}
@@
- const expectedClaimHash = await fetchSentSnapshotData(
+ const expectedClaimHash = await fetchSentSnapshotData(
sentSnapshotFromGraph.txHash,
veaInboxProvider,
veaInbox.interface
);
- const claimHash = await veaOutbox.claimHashes(epoch);
- if (claimHash === expectedClaimHash) {
+ if (!expectedClaimHash) return claimResolveState;
+ const claimHash = await veaOutbox.claimHashes(epoch);
+ if (claimHash === expectedClaimHash) {
claimResolveState.sendSnapshot.status = true;
claimResolveState.sendSnapshot.txHash = sentSnapshotFromGraph.txHash;
} else {
return claimResolveState;
}Also applies to: 178-184, 190-201
🤖 Prompt for AI Agents
In validator-cli/src/utils/claim.ts around lines 167-176 (also apply same checks
to 178-184 and 190-201), guard against a null expectedClaimHash returned from
fetchSentSnapshotData by checking if expectedClaimHash is falsy and immediately
throwing or returning an error; then compare expectedClaimHash to the claimHash
and if they differ fail fast (throw or return) instead of proceeding, ensuring
any log messages include both values for clarity and avoiding further processing
when the hashes don’t match.
| const getSentSnapshotData = async (txHash: string, provider: JsonRpcProvider, inboxInterface: any): Promise<string> => { | ||
| const tx = await provider.getTransaction(txHash); | ||
| if (!tx) return null; | ||
|
|
||
| // Parse the transaction calldata to identify function + args | ||
| const parsed = inboxInterface.parseTransaction({ data: tx.data }); | ||
| const args = parsed.args; | ||
| const claimTuple = args[1] as ClaimStruct; | ||
| const expectedClaimHash = hashClaim(claimTuple); | ||
| return expectedClaimHash; | ||
| }; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix return type and make parsing robust in getSentSnapshotData.
It returns null on missing tx but the signature promises string. Also, assuming the claim tuple at args[1] is brittle.
Apply this diff:
-const getSentSnapshotData = async (txHash: string, provider: JsonRpcProvider, inboxInterface: any): Promise<string> => {
+const getSentSnapshotData = async (
+ txHash: string,
+ provider: JsonRpcProvider,
+ inboxInterface: any
+): Promise<string | null> => {
const tx = await provider.getTransaction(txHash);
- if (!tx) return null;
+ if (!tx) return null;
- // Parse the transaction calldata to identify function + args
- const parsed = inboxInterface.parseTransaction({ data: tx.data });
- const args = parsed.args;
- const claimTuple = args[1] as ClaimStruct;
- const expectedClaimHash = hashClaim(claimTuple);
- return expectedClaimHash;
+ try {
+ // Parse the transaction calldata to identify function + args
+ const parsed = inboxInterface.parseTransaction({ data: tx.data });
+ const args = parsed.args;
+ // Support both sendSnapshot(epoch, claim) and sendSnapshot(claim) shapes
+ const maybeClaim = Array.isArray(args) && args.length > 0
+ ? (args.length > 1 ? args[1] : args[0])
+ : null;
+ if (!maybeClaim) return null;
+ const expectedClaimHash = hashClaim(maybeClaim as ClaimStruct);
+ return expectedClaimHash;
+ } catch {
+ return null;
+ }
};🤖 Prompt for AI Agents
In validator-cli/src/utils/claim.ts around lines 236 to 247, the function
getSentSnapshotData declares a Promise<string> but returns null when the
transaction is missing and assumes args[1] exists; update the signature to
Promise<string | null>, return null when tx is not found, wrap parsing in a
try/catch to handle parseTransaction errors, verify parsed and parsed.args exist
and that args.length > 1 before accessing args[1], validate that args[1] matches
the expected ClaimStruct shape (or bail with null), and only then compute and
return hashClaim(claimTuple).
| const getLastMessageSaved = async (veaInbox: string, chainId: number): Promise<{ id: string; stateRoot: string }> => { | ||
| const subgraph = getInboxSubgraphUrl(chainId); | ||
| const result: SnapshotSavedResponse = await request( | ||
| `${subgraph}`, | ||
| `{ | ||
| snapshots(first:2, orderBy:timestamp,orderDirection:desc, where:{inbox:"${veaInbox}"}) { | ||
| stateRoot | ||
| messages(first: 1,orderBy:timestamp,orderDirection:desc){ | ||
| id | ||
| } | ||
| } | ||
| }` | ||
| ); | ||
| if (result.snapshots.length < 2 || result.snapshots[1].messages.length === 0) return; | ||
| return result.snapshots[1].messages[0].id; | ||
| return { id: result.snapshots[1].messages[0].id, stateRoot: result.snapshots[1].stateRoot }; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Return type mismatch and inconsistent where filter; add null-safety.
Function can return undefined, but signature forbids it. Also align where with the other query (inbox_: { id: ... }) and guard array access.
-const getLastMessageSaved = async (veaInbox: string, chainId: number): Promise<{ id: string; stateRoot: string }> => {
+const getLastMessageSaved = async (
+ veaInbox: string,
+ chainId: number
+): Promise<{ id: string; stateRoot: string } | undefined> => {
const subgraph = getInboxSubgraphUrl(chainId);
const result: SnapshotSavedResponse = await request(
`${subgraph}`,
`{
- snapshots(first:2, orderBy:timestamp,orderDirection:desc, where:{inbox:"${veaInbox}"}) {
+ snapshots(first: 2, orderBy: timestamp, orderDirection: desc, where: { inbox_: { id: "${veaInbox}" } }) {
stateRoot
messages(first: 1,orderBy:timestamp,orderDirection:desc){
id
}
}
}`
);
- if (result.snapshots.length < 2 || result.snapshots[1].messages.length === 0) return;
- return { id: result.snapshots[1].messages[0].id, stateRoot: result.snapshots[1].stateRoot };
+ const secondLatest = result.snapshots?.[1];
+ const messageId = secondLatest?.messages?.[0]?.id;
+ const stateRoot = secondLatest?.stateRoot;
+ if (!messageId || !stateRoot) return undefined;
+ return { id: messageId, stateRoot };
};Run to find call sites expecting the old string return to update them:
🏁 Script executed:
#!/bin/bash
rg -nP --type=ts -C2 '\bgetLastMessageSaved\s*\('Length of output: 0
I need to verify whether the file and function exist in the codebase, and locate the call sites. Let me search more broadly:
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the graphQueries.ts file
fd -type f -name "graphQueries.ts"
Length of output: 170
🏁 Script executed:
#!/bin/bash
# Search for getLastMessageSaved function definition and calls
rg -nP "getLastMessageSaved" --type=tsLength of output: 513
🏁 Script executed:
#!/bin/bash
# Check file structure around validator-cli
fd -type f "graphQueries" Length of output: 170
---
🏁 Script executed:
```shell
#!/bin/bash
# View the snapshot.ts file to understand how getLastMessageSaved is used
cat validator-cli/src/helpers/snapshot.ts
Length of output: 3668
🏁 Script executed:
#!/bin/bash
# Get context around the function definition and the return statement
cat validator-cli/src/utils/graphQueries.ts | sed -n '192,207p'Length of output: 657
🏁 Script executed:
#!/bin/bash
# Search for how the result of getLastMessageSaved or fetchLastSavedMessage is used
rg -nP "fetchLastSavedMessage" validator-cli/src/helpers/snapshot.ts -A5 -B2Length of output: 874
---
🏁 Script executed:
```shell
#!/bin/bash
# Verify snapshot.ts is the only direct or indirect caller
rg -n "getLastMessageSaved\|fetchLastSavedMessage" validator-cli/src --type=ts -C1
Length of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# Simple search for all references to getLastMessageSaved
rg "getLastMessageSaved" validator-cli/src
Length of output: 497
🏁 Script executed:
#!/bin/bash
# Also search for fetchLastSavedMessage usage patterns
rg "fetchLastSavedMessage" validator-cli/srcLength of output: 1529
Return type mismatch requires updating caller; function can return undefined.
The function getLastMessageSaved (graphQueries.ts:192) can return undefined at line 203, but its type signature forbids this. The caller in snapshot.ts (line 80–83) destructures the result without null-checks and will crash at runtime.
Update the return type to Promise<{ id: string; stateRoot: string } | undefined> and guard the caller:
const getLastMessageSaved = async (veaInbox: string, chainId: number): Promise<{ id: string; stateRoot: string } | undefined> => {In snapshot.ts, add null-safety where fetchLastSavedMessage is called (line 80–83):
- const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = await fetchLastSavedMessage(
+ const result = await fetchLastSavedMessage(
veaInboxAddress,
chainId
);
+ if (!result) return { snapshotNeeded: false, latestCount: currentCount };
+ const { id: lastSavedMessageId, stateRoot: lastSavedStateRoot } = result;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In validator-cli/src/utils/graphQueries.ts around lines 192 to 207 the function
getLastMessageSaved can return undefined (it returns nothing when snapshots are
missing) but its signature is Promise<{ id: string; stateRoot: string }>, so
update the signature to Promise<{ id: string; stateRoot: string } | undefined>
and keep the existing behavior; then in snapshot.ts where fetchLastSavedMessage
is consumed (around lines 80–83) add a null-check before destructuring the
result (handle the undefined case by early return or error handling) to avoid
runtime crashes.
| public async toSubmitTransaction( | ||
| trnx: Transaction | null, | ||
| contract: ContractType, | ||
| currentTime: number | ||
| ): Promise<boolean> { | ||
| const status = await this.checkTransactionStatus(trnx, contract, currentTime); | ||
| if (status === TransactionStatus.PENDING || status === TransactionStatus.NOT_FINAL) return false; | ||
| return true; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent re‑submitting after a FINAL tx (gas burn / revert risk).
toSubmitTransaction returns true for FINAL, enabling duplicate submissions of already-finalized actions (e.g., withdraws, verifications, saves). Block on FINAL instead; only allow when NOT_MADE or EXPIRED.
Apply this diff:
public async toSubmitTransaction(
trnx: Transaction | null,
contract: ContractType,
currentTime: number
): Promise<boolean> {
const status = await this.checkTransactionStatus(trnx, contract, currentTime);
- if (status === TransactionStatus.PENDING || status === TransactionStatus.NOT_FINAL) return false;
- return true;
+ // Submit only if nothing was sent yet or the previous attempt expired.
+ if (status === TransactionStatus.NOT_MADE || status === TransactionStatus.EXPIRED) return true;
+ // Block while pending, not-final, or already final.
+ return false;
}No call‑site changes needed; behavior becomes safe by default.
Also applies to: 206-209, 235-237, 259-265, 279-285, 298-300
🤖 Prompt for AI Agents
In validator-cli/src/utils/transactionHandlers/baseTransactionHandler.ts around
lines 192-201 (and similarly update the other occurrences at 206-209, 235-237,
259-265, 279-285, 298-300), the method currently treats FINAL as allowable and
returns true, which permits re-submission of already-finalized transactions;
change the condition so the function only returns true when the status is
NOT_MADE or EXPIRED and returns false for PENDING, NOT_FINAL, and FINAL; update
each affected toSubmitTransaction (and equivalent) locations to explicitly check
for NOT_MADE || EXPIRED -> return true, otherwise return false.
Added logic to handle failed resolution scenario and fallback for missed claims in
validator-cliFiles updated:
snapshot.ts: updated to save snapshot for now new messages if the inbox stateroot is not claimed on outbox.claim.ts: previously sent snapshot's claim is verified againt the outbox claim.validator.ts: outscoped challenge withdrawal transaction before resolving to prevent sending snapshots after a claim is resolved due to claimHash changes.graphQueries.ts: added stateRoot field for lastSavedSnapshot queryPR-Codex overview
This PR introduces enhancements to the
validator-cliproject, focusing on improving claim resolution handling, snapshot management, and event emissions for better bot functionality.Detailed summary
logger.ts.CLAIM_ALREADY_RESOLVEDevent inbotEvents.ts.RPC_BLOCK_LIMITfrom 1000 to 100 inwatcher.ts.claim.tsto verify claim hashes against sent snapshots.veaOutboxsupport in various functions and interfaces.snapshotSavingPeriodconstants.Summary by CodeRabbit
New Features
--saveSnapshotand--pathflags supporting challenger, bridger, or both modes.Bug Fixes
Documentation
Optimization