security(canceler): map RPC failure to Invalid and cancel #191

Open
opened 2026-09-12 13:14:56 +00:00 by PlasticDigits · 0 comments

Summary

packages/canceler/src/verifier.rs maps a successful getDeposit with timestamp = 0 to VerificationResult::Invalid (watcher submits cancel) but maps getDeposit Err, HTTP/LCD transport failure, timeout, invalid RPC URL parse, and exhausted Solana RPC fallback to VerificationResult::Pending with a "will retry" log. packages/canceler/src/watcher.rs then inserts the hash into pending_retry_queue (C12) and does not call submit_cancel.

That inverts watchtower policy. Prefer a wrongful cancel over a missed cancel. Cancel can be reversed (EVM withdrawUncancel, Terra execute_withdraw_uncancel, Solana withdraw_reenable). An uncancelled fraudulent dest WithdrawApprove that executes cannot. A down or flaky source RPC therefore burns cancel-window time instead of cancelling.

Internal review id: RS-H3. Policy still in source as of 2026-09-12 (packages/canceler/src/verifier.rs on main). The residual is inverted relative to watchtower: empty-deposit is cancel; transport failure is wait.

This is not #114 / #115 (sequential RPC fallback, eth_blockNumber quorum, confirmation depth, /health idle, eth_chainId, HTTPS gate). Those were closed as non-launch-blocking after the 2026-04 RPC-hardening merge. Do not reopen them to add quorum-before-cancel or Pending-on-disagreement. This is not #177 (on-chain cancel-window floor). This is not #188 (pause vs dest approve). Keyword overlap on “RPC” / “cancel” / “Pending” is not this bug.

Bundle (same ticket, do not split):

  1. RPC query failure, timeout, and provider disagreement map to Invalid and submit cancel on EVM, Terra, and Solana deposit verify.
  2. Keep hash-mismatch and missing-deposit (timestamp = 0, Terra data: null, Solana missing PDA) as Invalid.
  3. Pending remains only for unknown source chain (C6, including unknown EVM V2 id in known_evm_chains).
  4. Operator uncancel stays the recovery path for a wrongful cancel. Tests: getDeposit transport error submits cancel; uncancel still restores.

Founder-required canceler / wallet watchtower. No community autoland. Do not add ready.

Impact (today vs hypothetical)

Funds at risk today in source whenever dest approval verification cannot reach a healthy source RPC inside the cancel window. Invalid is the only verifier result that submits cancel. Pending retries. A fraudulent or mismatched approval whose source getDeposit (or Terra DepositHash / Solana getAccountInfo) errors, times out, or disagrees across providers is treated like “try later,” not fraud.

Asymmetric mapping already in verify_evm_deposit:

  • Ok(deposit) and deposit.timestamp.is_zero() → Invalid (“No deposit found…”) → cancel.
  • Err(e) → Pending ("Failed to query EVM deposit - will retry").

The same wait path exists for:

  • EVM RPC URL parse failure ("Invalid EVM RPC URL").
  • EVM bridge Address::from_str failure ("Invalid EVM bridge address").
  • Terra LCD send() Err ("Failed to query Terra deposit - will retry").
  • Terra HTTP non-success (400/500 treated as “transient,” return Pending).
  • Solana: all configured RPC URLs fail, return non-success, JSON-RPC error, or unparseable account data → Pending after the fallback loop.
  • Solana chain id matched but solana_config is None → Pending.

C12 queues Pending for retry. There is no verifier path that promotes a persistent transport error to Invalid before the dest cancel window elapses. If the source RPC stays down or rate-limits for the window, dest execute can proceed on an unverified approval.

Hypothetical-only if every production source RPC is always reachable, always agrees, and the cancel window is longer than any outage. Source still encodes wait-on-error. Sticky until transport / timeout / disagreement are Invalid and a unit test proves a getDeposit transport error submits cancel.

Do not publish a live-RPC recipe, a fraudulent approve payload, or operator key material.

Watchtower invariant (why wait is the vuln)

Cancel is reversible by the operator. Execute of an uncancelled fraudulent approval is not. Multi-RPC quorum or Pending-on-disagreement that delays cancel is the wrong control for this service. #114 EVM-01 taught sequential fallback / quorum for block reads; that must not be reused here as “do not cancel until providers agree.”

C6 must stay Pending

Unknown src_chain_id (not in configured EVM / Terra / Solana sets) must remain Pending, not Invalid. Returning Invalid there cancels every valid approval when V2 chain ids are misconfigured (catastrophic false-positive). Hash mismatch on a known chain stays Invalid (parameters do not match claimed hash). That split is already documented in verifier.rs C6 comments and test_c6_unknown_chain_returns_pending.

Invariants

  • INV-CAN-V1 (new): On a known source chain, deposit-verify transport error, timeout, HTTP/JSON-RPC failure, invalid RPC URL parse, and provider disagreement are VerificationResult::Invalid. Watcher must submit_cancel (same path as missing-deposit).
  • INV-CAN-V2 (keep): Hash mismatch and missing deposit (timestamp = 0, Terra null data, Solana null PDA / stored-hash mismatch) stay Invalid → cancel.
  • INV-CAN-V3 (keep C6): Unknown source chain (and unknown EVM V2 id in known_evm_chains) stays Pending. No cancel. Alert / unknown_source_chain_count unchanged in spirit.
  • INV-CAN-V4 (new): Do not require multi-RPC quorum or Pending-on-disagreement on the deposit-verify path if that delays cancel. Disagreement among configured endpoints is Invalid.
  • INV-CAN-V5 (keep): Operator uncancel restores a wrongful cancel: EVM withdrawUncancel, Terra execute_withdraw_uncancel, Solana withdraw_reenable. This ticket does not add auto-uncancel.
  • Do not weaken dest WithdrawApprove itself, on-chain cancel-window seconds (#177), or C6. Do not put live RPC URLs, operator keys, or HMAC secrets in fixtures.

Constraints / guardrails

  • Prefer mapping Err / timeout / disagreement to Invalid { reason } at the verifier. Do not add a “retry until window almost elapsed then maybe cancel” heuristic as the fix.
  • Invalid RPC URL parse and invalid EVM bridge address parse are configuration failures on a known chain, not C6. They must not stay Pending.
  • Terra non-success LCD status is not a substitute for “deposit missing.” Missing deposit is data: null after a successful query. Transport / 5xx / timeout → Invalid (cancel), not Pending.
  • Solana sequential endpoint retry may still try the next URL on a single endpoint failure. If all endpoints fail, timeout, or disagree on deposit existence/hash → Invalid, not Pending. Do not treat “first responsive RPC” as truth when another configured RPC reports a different deposit record.
  • Do not reopen #114 / #115 to restore or tighten quorum-before-cancel. Do not require EVM_RPC_AGREEMENT_QUORUM agreement before submit_cancel on this path.
  • Keep C12 retry queue for C6 Pending only (unknown chain). Transport-error hashes must not sit in pending_retry_queue instead of cancel.
  • Do not change Solidity / CosmWasm / Solana programs in this ticket except if an existing unit test must call uncancel to prove restore. Uncancel already exists; use it.
  • Founder-required canceler / watchtower. No community autoland. Do not add ready. No public fraud-approve recipe.

Relevant files

Path Why
packages/canceler/src/verifier.rs getDeposit Err / URL parse / Terra LCD Err / Solana all-RPC fail → Pending; timestamp = 0 already Invalid; C6 unknown chain Pending
packages/canceler/src/watcher.rs VerificationResult::Invalid → submit_cancel; Pending → pending_retry_queue only
packages/canceler/src/evm_client.rs Shared EVM RPC helpers; do not reintroduce quorum-before-cancel on verify
packages/canceler unit tests in verifier.rs C6 Pending vs hash-mismatch Invalid; add transport-error → Invalid / cancel
EVM withdrawUncancel (Bridge.sol) Recovery: operator reverses a wrongful cancel
Terra execute_withdraw_uncancel Same recovery on CosmWasm
Solana withdraw_reenable Same recovery on Solana
  1. In verify_evm_deposit, map getDeposit Err, RPC URL parse Err, and bridge address parse Err to Invalid with an explicit reason (transport / parse / timeout). Keep timestamp = 0, amount mismatch, and nonce mismatch as Invalid. Keep unknown V2 id in known_evm_chains as C6 Pending.
  2. In verify_terra_deposit, map LCD send() Err, timeout, and non-success status to Invalid. Keep data.is_null() and nonce/amount mismatch as Invalid.
  3. In verify_solana_deposit, keep trying the next URL on a single-endpoint failure. After all URLs fail, or if two successful responses disagree on PDA existence / stored hash, return Invalid. Missing solana_config while the chain id matched is not C6; treat as Invalid (cannot attest the deposit) unless product intent is that unmatched-config is equivalent to unknown chain — default is cancel.
  4. Watcher: no new branch required if verifier returns Invalid; existing submit_cancel path is enough. Ensure C12 does not re-queue those hashes as Pending.
  5. Provider disagreement: if the implementation queries more than one RPC for the same getDeposit / DepositHash / getAccountInfo, any disagreement is Invalid. Do not wait for quorum. Do not add a new env that pins “Pending until N providers agree.”
  6. Tests: mock getDeposit transport error → Invalid and watcher submit_cancel (or a verifier+watcher unit that asserts cancel is attempted). Existing C6 test still expects Pending. Add or reuse an uncancel test that a cancelled-then-uncancelled withdrawal can proceed (operator restore). Invert any test that asserts RPC Err is Pending.

Acceptance criteria

  • AC1. EVM getDeposit transport error / timeout → VerificationResult::Invalid and cancel is submitted (not C12 Pending).
  • AC2. Invalid EVM RPC URL parse and invalid bridge address parse → Invalid (cancel), not Pending.
  • AC3. Terra LCD transport error, timeout, and non-success status → Invalid (cancel). Terra data: null stays Invalid.
  • AC4. Solana: all RPC endpoints fail or timeout → Invalid (cancel). Null PDA and stored-hash mismatch stay Invalid. Cross-endpoint disagreement on the same nonce/hash → Invalid.
  • AC5. Hash mismatch on a known chain stays Invalid. Unknown source chain (C6) stays Pending and does not cancel. test_c6_unknown_chain_returns_pending remains green.
  • AC6. Provider disagreement on deposit-verify does not wait for quorum and does not return Pending.
  • AC7. Operator uncancel still restores: after a cancel submitted from AC1, uncancel (EVM / Terra / Solana as covered by existing tests) re-enables execute. No auto-uncancel.
  • AC8. No ready label. No Given/When/Then autoland shortcut. Do not reopen #114 / #115.

Test plan (functional paths)

# Path Expect
T1 EVM getDeposit returns timestamp = 0 Invalid; watcher submits cancel (existing)
T2 EVM getDeposit transport / timeout error Invalid; cancel submitted; not in pending_retry_queue
T3 EVM RPC URL parse failure Invalid; no silent retry
T4 EVM amount or nonce mismatch Invalid (unchanged)
T5 EVM matching deposit Valid; no cancel
T6 Terra LCD send() error or timeout Invalid; cancel
T7 Terra data: null Invalid; cancel
T8 Solana all RPCs fail Invalid; cancel
T9 Solana null PDA Invalid; cancel
T10 Unknown src_chain_id (C6) Pending; no cancel; retry queue OK
T11 Hash mismatch on known chain Invalid; cancel
T12 Two configured RPCs disagree on deposit existence or hash Invalid; cancel; no quorum wait
T13 After T2 cancel, operator uncancel Withdrawal can be restored (existing uncancel tests stay green)
T14 Existing canceler unit tests Stay green except inverted Pending-on-RPC-error cases

Test plan (attack, hack, and abuse)

Non-exploitative. Canceler unit / in-process mocks only. Do not use these as a live dest-approve recipe.

# Vector Expect
A1 Source RPC down for the whole cancel window (mocked timeout) Cancel submitted; dest execute is not left unverified-open
A2 Intermittent RPC: first call Err, later Ok matching deposit First result is already Invalid + cancel; uncancel is the restore if the deposit was real
A3 Hostile dest approve + source RPC that errors instead of returning empty deposit Same as missing-deposit: cancel, not retry-until-window
A4 Misconfigured V2 chain id (unknown source) C6 Pending; must not mass-cancel
A5 Two RPCs: one reports empty deposit, one reports a matching deposit Invalid + cancel (disagreement); not Pending
A6 Invalid RPC URL string in known-chain config Invalid; not an infinite Pending loop
A7 Regression: timestamp = 0 still Invalid (must not flip empty-deposit to Pending) Empty deposit still cancels

Verification criteria

  • Canceler package tests: T1–T14 and A1–A7. Grep that verify_evm_deposit no longer returns Pending on getDeposit Err / URL parse Err.
  • Grep that Terra LCD Err / non-success and Solana all-RPC failure are not the final Pending return except C6 unknown-chain.
  • test_c6_unknown_chain_returns_pending (or equivalent) still asserts unknown chain is Pending.
  • A test named for transport error asserts Invalid and that cancel is attempted.
  • Uncancel restore test still passes (wrongful cancel is reversible).
  • Do not verify by driving a live dest approve against production RPCs or by publishing a fraud payload.

Out of scope

  • #114 / #115 blockNumber quorum, confirmation depth, /health idle 503, HTTPS gate, eth_chainId startup checks. Do not reopen.
  • #177 on-chain cancel-window seconds / 15s floor.
  • #188 pause vs dest approve.
  • Auto-uncancel from the canceler process.
  • Changing AccessManager roles or operator API auth (#190).
  • Live canceler redeploy (ops).

First-pass model recommendation

Recommendation: grok-high

Rationale: Security class and founder-required watchtower / canceler path (funds-at-risk if dest execute races a Pending retry). Composer is disallowed (High/security; not a low-risk first pass). Even if the production edit is likely verifier.rs plus focused watcher/tests, file count does not establish safety: inverting Pending vs Invalid can either miss a fraudulent approve or mass-cancel on C6 misconfiguration. Verify with mocked RPC transport errors and the existing C6 + uncancel tests, not a live chain probe.

## Summary `packages/canceler/src/verifier.rs` maps a successful `getDeposit` with `timestamp = 0` to `VerificationResult::Invalid` (watcher submits cancel) but maps `getDeposit` `Err`, HTTP/LCD transport failure, timeout, invalid RPC URL parse, and exhausted Solana RPC fallback to `VerificationResult::Pending` with a `"will retry"` log. `packages/canceler/src/watcher.rs` then inserts the hash into `pending_retry_queue` (C12) and does **not** call `submit_cancel`. That inverts watchtower policy. Prefer a wrongful cancel over a missed cancel. Cancel can be reversed (EVM `withdrawUncancel`, Terra `execute_withdraw_uncancel`, Solana `withdraw_reenable`). An uncancelled fraudulent dest `WithdrawApprove` that executes cannot. A down or flaky source RPC therefore burns cancel-window time instead of cancelling. Internal review id: RS-H3. Policy still in source as of 2026-09-12 (`packages/canceler/src/verifier.rs` on `main`). The residual is inverted relative to watchtower: empty-deposit is cancel; transport failure is wait. This is not [#114](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/114) / [#115](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/115) (sequential RPC fallback, `eth_blockNumber` quorum, confirmation depth, `/health` idle, `eth_chainId`, HTTPS gate). Those were closed as non-launch-blocking after the 2026-04 RPC-hardening merge. Do **not** reopen them to add quorum-before-cancel or Pending-on-disagreement. This is not [#177](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/177) (on-chain cancel-window floor). This is not [#188](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/188) (pause vs dest approve). Keyword overlap on “RPC” / “cancel” / “Pending” is not this bug. Bundle (same ticket, do not split): 1. RPC query failure, timeout, and provider disagreement map to `Invalid` and submit cancel on EVM, Terra, and Solana deposit verify. 2. Keep hash-mismatch and missing-deposit (`timestamp = 0`, Terra `data: null`, Solana missing PDA) as `Invalid`. 3. `Pending` remains **only** for unknown source chain (C6, including unknown EVM V2 id in `known_evm_chains`). 4. Operator uncancel stays the recovery path for a wrongful cancel. Tests: `getDeposit` transport error submits cancel; uncancel still restores. Founder-required canceler / wallet watchtower. No community autoland. Do not add `ready`. ## Impact (today vs hypothetical) Funds at risk **today** in source whenever dest approval verification cannot reach a healthy source RPC inside the cancel window. `Invalid` is the only verifier result that submits cancel. `Pending` retries. A fraudulent or mismatched approval whose source `getDeposit` (or Terra `DepositHash` / Solana `getAccountInfo`) errors, times out, or disagrees across providers is treated like “try later,” not fraud. Asymmetric mapping already in `verify_evm_deposit`: - `Ok(deposit)` and `deposit.timestamp.is_zero()` → `Invalid` (“No deposit found…”) → cancel. - `Err(e)` → `Pending` (`"Failed to query EVM deposit - will retry"`). The same wait path exists for: - EVM RPC URL `parse` failure (`"Invalid EVM RPC URL"`). - EVM bridge `Address::from_str` failure (`"Invalid EVM bridge address"`). - Terra LCD `send()` `Err` (`"Failed to query Terra deposit - will retry"`). - Terra HTTP non-success (400/500 treated as “transient,” return `Pending`). - Solana: all configured RPC URLs fail, return non-success, JSON-RPC error, or unparseable account data → `Pending` after the fallback loop. - Solana chain id matched but `solana_config` is `None` → `Pending`. C12 queues `Pending` for retry. There is no verifier path that promotes a persistent transport error to `Invalid` before the dest cancel window elapses. If the source RPC stays down or rate-limits for the window, dest execute can proceed on an unverified approval. Hypothetical-only if every production source RPC is always reachable, always agrees, and the cancel window is longer than any outage. Source still encodes wait-on-error. Sticky until transport / timeout / disagreement are `Invalid` and a unit test proves a `getDeposit` transport error submits cancel. Do not publish a live-RPC recipe, a fraudulent approve payload, or operator key material. ### Watchtower invariant (why wait is the vuln) Cancel is reversible by the operator. Execute of an uncancelled fraudulent approval is not. Multi-RPC quorum or Pending-on-disagreement that **delays** cancel is the wrong control for this service. #114 EVM-01 taught sequential fallback / quorum for block reads; that must not be reused here as “do not cancel until providers agree.” ### C6 must stay Pending Unknown `src_chain_id` (not in configured EVM / Terra / Solana sets) must remain `Pending`, not `Invalid`. Returning `Invalid` there cancels every valid approval when V2 chain ids are misconfigured (catastrophic false-positive). Hash mismatch on a **known** chain stays `Invalid` (parameters do not match claimed hash). That split is already documented in `verifier.rs` C6 comments and `test_c6_unknown_chain_returns_pending`. ## Invariants - INV-CAN-V1 (new): On a **known** source chain, deposit-verify transport error, timeout, HTTP/JSON-RPC failure, invalid RPC URL parse, and provider disagreement are `VerificationResult::Invalid`. Watcher must `submit_cancel` (same path as missing-deposit). - INV-CAN-V2 (keep): Hash mismatch and missing deposit (`timestamp = 0`, Terra null `data`, Solana null PDA / stored-hash mismatch) stay `Invalid` → cancel. - INV-CAN-V3 (keep C6): Unknown source chain (and unknown EVM V2 id in `known_evm_chains`) stays `Pending`. No cancel. Alert / `unknown_source_chain_count` unchanged in spirit. - INV-CAN-V4 (new): Do not require multi-RPC quorum or Pending-on-disagreement on the deposit-verify path if that delays cancel. Disagreement among configured endpoints is `Invalid`. - INV-CAN-V5 (keep): Operator uncancel restores a wrongful cancel: EVM `withdrawUncancel`, Terra `execute_withdraw_uncancel`, Solana `withdraw_reenable`. This ticket does not add auto-uncancel. - Do not weaken dest `WithdrawApprove` itself, on-chain cancel-window seconds ([#177](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/177)), or C6. Do not put live RPC URLs, operator keys, or HMAC secrets in fixtures. ## Constraints / guardrails - Prefer mapping `Err` / timeout / disagreement to `Invalid { reason }` at the verifier. Do not add a “retry until window almost elapsed then maybe cancel” heuristic as the fix. - Invalid RPC URL parse and invalid EVM bridge address parse are configuration failures on a known chain, not C6. They must not stay `Pending`. - Terra non-success LCD status is not a substitute for “deposit missing.” Missing deposit is `data: null` after a successful query. Transport / 5xx / timeout → `Invalid` (cancel), not `Pending`. - Solana sequential endpoint retry may still try the next URL on a **single** endpoint failure. If **all** endpoints fail, timeout, or disagree on deposit existence/hash → `Invalid`, not `Pending`. Do not treat “first responsive RPC” as truth when another configured RPC reports a different deposit record. - Do not reopen [#114](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/114) / [#115](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/115) to restore or tighten quorum-before-cancel. Do not require `EVM_RPC_AGREEMENT_QUORUM` agreement before `submit_cancel` on this path. - Keep C12 retry queue for **C6 Pending only** (unknown chain). Transport-error hashes must not sit in `pending_retry_queue` instead of cancel. - Do not change Solidity / CosmWasm / Solana programs in this ticket except if an existing unit test must call uncancel to prove restore. Uncancel already exists; use it. - Founder-required canceler / watchtower. No community autoland. Do not add `ready`. No public fraud-approve recipe. ## Relevant files | Path | Why | | --- | --- | | `packages/canceler/src/verifier.rs` | `getDeposit` `Err` / URL parse / Terra LCD `Err` / Solana all-RPC fail → `Pending`; `timestamp = 0` already `Invalid`; C6 unknown chain `Pending` | | `packages/canceler/src/watcher.rs` | `VerificationResult::Invalid` → `submit_cancel`; `Pending` → `pending_retry_queue` only | | `packages/canceler/src/evm_client.rs` | Shared EVM RPC helpers; do not reintroduce quorum-before-cancel on verify | | `packages/canceler` unit tests in `verifier.rs` | C6 Pending vs hash-mismatch Invalid; add transport-error → Invalid / cancel | | EVM `withdrawUncancel` (Bridge.sol) | Recovery: operator reverses a wrongful cancel | | Terra `execute_withdraw_uncancel` | Same recovery on CosmWasm | | Solana `withdraw_reenable` | Same recovery on Solana | ## Recommended direction 1. In `verify_evm_deposit`, map `getDeposit` `Err`, RPC URL parse `Err`, and bridge address parse `Err` to `Invalid` with an explicit reason (transport / parse / timeout). Keep `timestamp = 0`, amount mismatch, and nonce mismatch as `Invalid`. Keep unknown V2 id in `known_evm_chains` as C6 `Pending`. 2. In `verify_terra_deposit`, map LCD `send()` `Err`, timeout, and non-success status to `Invalid`. Keep `data.is_null()` and nonce/amount mismatch as `Invalid`. 3. In `verify_solana_deposit`, keep trying the next URL on a single-endpoint failure. After all URLs fail, or if two successful responses disagree on PDA existence / stored hash, return `Invalid`. Missing `solana_config` while the chain id matched is not C6; treat as `Invalid` (cannot attest the deposit) unless product intent is that unmatched-config is equivalent to unknown chain — default is cancel. 4. Watcher: no new branch required if verifier returns `Invalid`; existing `submit_cancel` path is enough. Ensure C12 does not re-queue those hashes as `Pending`. 5. Provider disagreement: if the implementation queries more than one RPC for the same `getDeposit` / `DepositHash` / `getAccountInfo`, any disagreement is `Invalid`. Do not wait for quorum. Do not add a new env that pins “Pending until N providers agree.” 6. Tests: mock `getDeposit` transport error → `Invalid` and watcher `submit_cancel` (or a verifier+watcher unit that asserts cancel is attempted). Existing C6 test still expects `Pending`. Add or reuse an uncancel test that a cancelled-then-uncancelled withdrawal can proceed (operator restore). Invert any test that asserts RPC `Err` is `Pending`. ## Acceptance criteria - AC1. EVM `getDeposit` transport error / timeout → `VerificationResult::Invalid` and cancel is submitted (not C12 `Pending`). - AC2. Invalid EVM RPC URL parse and invalid bridge address parse → `Invalid` (cancel), not `Pending`. - AC3. Terra LCD transport error, timeout, and non-success status → `Invalid` (cancel). Terra `data: null` stays `Invalid`. - AC4. Solana: all RPC endpoints fail or timeout → `Invalid` (cancel). Null PDA and stored-hash mismatch stay `Invalid`. Cross-endpoint disagreement on the same nonce/hash → `Invalid`. - AC5. Hash mismatch on a known chain stays `Invalid`. Unknown source chain (C6) stays `Pending` and does not cancel. `test_c6_unknown_chain_returns_pending` remains green. - AC6. Provider disagreement on deposit-verify does not wait for quorum and does not return `Pending`. - AC7. Operator uncancel still restores: after a cancel submitted from AC1, uncancel (EVM / Terra / Solana as covered by existing tests) re-enables execute. No auto-uncancel. - AC8. No `ready` label. No Given/When/Then autoland shortcut. Do not reopen #114 / #115. ## Test plan (functional paths) | # | Path | Expect | | --- | --- | --- | | T1 | EVM `getDeposit` returns `timestamp = 0` | `Invalid`; watcher submits cancel (existing) | | T2 | EVM `getDeposit` transport / timeout error | `Invalid`; cancel submitted; not in `pending_retry_queue` | | T3 | EVM RPC URL parse failure | `Invalid`; no silent retry | | T4 | EVM amount or nonce mismatch | `Invalid` (unchanged) | | T5 | EVM matching deposit | `Valid`; no cancel | | T6 | Terra LCD `send()` error or timeout | `Invalid`; cancel | | T7 | Terra `data: null` | `Invalid`; cancel | | T8 | Solana all RPCs fail | `Invalid`; cancel | | T9 | Solana null PDA | `Invalid`; cancel | | T10 | Unknown `src_chain_id` (C6) | `Pending`; no cancel; retry queue OK | | T11 | Hash mismatch on known chain | `Invalid`; cancel | | T12 | Two configured RPCs disagree on deposit existence or hash | `Invalid`; cancel; no quorum wait | | T13 | After T2 cancel, operator uncancel | Withdrawal can be restored (existing uncancel tests stay green) | | T14 | Existing canceler unit tests | Stay green except inverted Pending-on-RPC-error cases | ## Test plan (attack, hack, and abuse) Non-exploitative. Canceler unit / in-process mocks only. Do not use these as a live dest-approve recipe. | # | Vector | Expect | | --- | --- | --- | | A1 | Source RPC down for the whole cancel window (mocked timeout) | Cancel submitted; dest execute is not left unverified-open | | A2 | Intermittent RPC: first call `Err`, later `Ok` matching deposit | First result is already `Invalid` + cancel; uncancel is the restore if the deposit was real | | A3 | Hostile dest approve + source RPC that errors instead of returning empty deposit | Same as missing-deposit: cancel, not retry-until-window | | A4 | Misconfigured V2 chain id (unknown source) | C6 `Pending`; must **not** mass-cancel | | A5 | Two RPCs: one reports empty deposit, one reports a matching deposit | `Invalid` + cancel (disagreement); not Pending | | A6 | Invalid RPC URL string in known-chain config | `Invalid`; not an infinite Pending loop | | A7 | Regression: `timestamp = 0` still `Invalid` (must not flip empty-deposit to Pending) | Empty deposit still cancels | ## Verification criteria - Canceler package tests: T1–T14 and A1–A7. Grep that `verify_evm_deposit` no longer returns `Pending` on `getDeposit` `Err` / URL parse `Err`. - Grep that Terra LCD `Err` / non-success and Solana all-RPC failure are not the final `Pending` return except C6 unknown-chain. - `test_c6_unknown_chain_returns_pending` (or equivalent) still asserts unknown chain is `Pending`. - A test named for transport error asserts `Invalid` and that cancel is attempted. - Uncancel restore test still passes (wrongful cancel is reversible). - Do not verify by driving a live dest approve against production RPCs or by publishing a fraud payload. ## Out of scope - [#114](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/114) / [#115](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/115) blockNumber quorum, confirmation depth, `/health` idle 503, HTTPS gate, `eth_chainId` startup checks. Do not reopen. - [#177](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/177) on-chain cancel-window seconds / 15s floor. - [#188](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/188) pause vs dest approve. - Auto-uncancel from the canceler process. - Changing AccessManager roles or operator API auth ([#190](https://git.cl8y.com/code/cl8y-bridge-monorepo/issues/190)). - Live canceler redeploy (ops). ## First-pass model recommendation Recommendation: grok-high Rationale: Security class and founder-required watchtower / canceler path (funds-at-risk if dest execute races a Pending retry). Composer is disallowed (High/security; not a low-risk first pass). Even if the production edit is likely `verifier.rs` plus focused watcher/tests, file count does not establish safety: inverting `Pending` vs `Invalid` can either miss a fraudulent approve or mass-cancel on C6 misconfiguration. Verify with mocked RPC transport errors and the existing C6 + uncancel tests, not a live chain probe.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#191
No description provided.