Skip to content

feat(bridge): refuse to bridge a pair while a pause switch is set on either side - #86

Open
iamalwaysuncomfortable wants to merge 13 commits into
masterfrom
worktree-pause-gate
Open

iamalwaysuncomfortable wants to merge 13 commits into
masterfrom
worktree-pause-gate

Conversation

@iamalwaysuncomfortable

Copy link
Copy Markdown
Member

Summary

Every bridge deployment has an operator kill switch: the pause mapping on the Aleo warp and USDCx programs, paused() on Circle's xReserve and the CCTP transmitter, a PausableHook / PausableIsm inside the Ethereum Hyperlane Mailbox's hook and ISM trees, and the enrolled-router table of the Solana warp program. The SDK read none of them, so a transfer into a paused far side dispatched, locked the funds on the source chain and stalled at delivery.

quote, execute, resume and complete now read every switch governing the route, on both ends, right before they act, and refuse with RoutePausedError naming the switch. A switch that cannot be read after retries refuses with PauseCheckError. Both subclass RouteUnavailableError, so existing handlers keep working. complete reads the destination side only, since the deposit is already committed.

  • Reads retry twice immediately, then back off 0.5 / 1 / 2 / 4 s, and fail closed. Nothing is cached; there is no opt-out.
  • Bridge.pause_status(route) and a bridge_pause_status agent/MCP tool expose the reads. Existing tool schemas are byte-identical to a snapshot taken before this change (tests/fixtures/agent-tools-2026-10-30.json).
  • Aleo programs use three pause conventions (pause[true], paused[true], ARC-20 pause__[false]); the reader follows what each program declares and refuses a warp program that declares none. A program the node does not serve is a ConfigurationError, never "open" (the node answers null for a missing key, an undeclared mapping and a missing program alike).
  • Far-side EVM reads for an Aleo-origin route resolve a connection in order: the configured one, a URL saved in the profile (Profile.rpc_urls / set_rpc_url), then a registry-pinned public read endpoint (PUBLIC_READ_RPC_URLS, 10 s timeout, pause reads only, never signing). ETHEREUM_RPC_URL alone now yields a read-only connection, matching the other chains.
  • Solana: Hyperlane's Sealevel warp program has no pause flag; the switch is the token PDA's remote_routers[<aleo domain>], decoded against two vendored mainnet fixtures.

Backwards compatible: no existing signature, dataclass field, tool schema or checkpoint format changes. Registry gains an optional constructor keyword and public_read_rpc_urls(); Profile gains an optional rpc_urls key older files simply lack; legacy registry fingerprints are byte-identical.

Live state (mainnet, 2026-10-30)

tests/live/test_pause_reads.py reads every active route over the public endpoints: 28 routes, 0 unknown, 16 paused. Every Hyperlane warp program except USDT is currently paused (ETH, WBTC, BAT, USDG, SOL, ZEC, both directions). xReserve and CCTP are open.

Test plan

  • cd bridge-sdk && python -m pytest -q -m "not live" → 1261 passed
  • python codegen/gen_context.py --check → current
  • BRIDGE_LIVE_READS=1 python -m pytest -m live tests/live/test_pause_reads.py → 28 passed
  • Existing read-only live suites (test_aleo_reads, test_eth_reads, test_sol_reads, test_arc_reads, test_compliance_live, test_frozen_live) → 53 passed, 4 skipped

Deferred (minor, from review): fallbackHook() of a routing hook is not walked when the domain slot is zero; sides= is not validated against typos; an all-zero Solana router entry reads as enrolled; the NULL-ISM probe does not catch web3's empty-response error.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant