Skip to main content

CCIP Bridge Operator Surface

Source files
  • src/qtg/interfaces/api/routes/ccip_registry.py
  • src/qtg/application/services/ccip_refresh.py
  • src/qtg/infrastructure/executors/ccip/__init__.py
  • src/qtg/infrastructure/executors/ccip/action.py
  • src/qtg/infrastructure/executors/ccip/sidecar_client.py
  • src/qtg/infrastructure/executors/ccip/repository.py
  • src/qtg/infrastructure/executors/ccip/token_registry_repository.py

1. Architecture overview

The CCIP lane is one executor (exec.ccip.send) backed by: Unlike CCTP and Stargate (Pro), CCIP does not expose row-level admin CRUD routes for the registry. The registry is rebuilt by calling POST /v3/executors/ccip/refresh-registry, which delegates to the sidecar’s /network-info endpoint to discover the current canonical CCIP routers and lanes, diff against the DB, and apply (or dry-run) the result.

2. Admin route table

2.1 Auth boundary

Single write surface, gated by HMAC admin purpose:
  • Route module: src/qtg/interfaces/api/routes/ccip_registry.py
  • Guard: require_admin_hmacrequire_request_auth
  • Accepted purposes: admin, all
  • Canonical mapping source: interfaces/api/middleware/auth/hmac.py

2.2 Routes

RouterDiff fields:
  • added: list[dict] — routers newly discovered on a chain
  • changed: list[dict] — routers whose address changed under the same selector
  • removed: list[str] — selectors no longer present
LaneDiff fields:
  • added: list[dict] — new (src_selector, dst_selector, token) lanes
  • changed: list[dict] — lanes whose router or trust tier flipped
  • orphaned: list[dict] — lanes still in DB but no longer in network-info
  • skipped: list[dict] — lanes the diff intentionally did not touch
requires_reapproval: list[dict] is the operator-actionable result: lanes whose registry_version changed and which the operator must explicitly re-approve before they become dispatchable again.

2.3 Typical operator workflow

  1. Dry-run first. Always.
  2. Review diff, lane_diff, and requires_reapproval in the response.
  3. If the changes look right, repeat with dry_run: false to apply.
  4. For every lane in requires_reapproval, re-approve out-of-band (the registry write-side keeps prior status until an operator confirms the new snapshot).

2.4 Idempotency and audit semantics

The refresh endpoint always writes an audit row via audit_log_post_commit(descriptor_for_route('POST', '/v3/executors/ccip/refresh-registry'), ...). The audit captures dry_run, operator_label, applied, and sdk_version so postmortems can correlate registry mutation with a specific SDK build. If dry_run=true, applied=false is recorded and no DB rows change. If the sidecar’s network_info_hash matches the prior refresh result, the diff is empty and the response carries the same new_snapshot_version as before — the operation is safely re-runnable.

2.5 Failure semantics

The refresh request propagates sidecar HTTP errors with the source error_code:

3. Lane registry semantics

The lane registry is the approval surface that gates exec.ccip.send at runtime. Three column states matter: The registry_version invariant is the most subtle: when prepare() allocates a nonce and writes the intent, it stamps snapshot_version_at_dispatch. At submit() time, the executor re-reads the row with for_update=True and refuses to broadcast if the version changed. This is what makes the refresh endpoint safe to run mid-flight — any in-flight intent against an updated lane will eager-tombstone instead of broadcasting against a stale router approval.

Signed-recipient invariant (validated == signed)

CCIP now participates in QTG’s cross-lane signed-recipient invariant at the broadcast boundary: QTG independently RLP-decodes the actual signed artifact, ABI-decodes ccipSend, and asserts router, destination chain selector, receiver, and token against trusted intent/binding state before the sidecar broadcasts. Failures close the node with ccip_signed_artifact_mismatch and a specific detail.error_code such as SIGNED_RECIPIENT_MISMATCH or SIGNED_TARGET_MISMATCH. The dispatch allowlist is also now chain-scoped for CCIP only: source and destination addresses must match an AllowedAddress row whose chain_id equals the selector-derived EVM chain id. Chain-NULL allowlist rows no longer pass for CCIP, but they remain valid for CCTP/Gateway lanes that still use chain-agnostic matching.

Runbook: chain-match setup and recovery

For the operator setup and cutover steps, use CCIP allowlist chain-match setup. Keep this reference page as the lane surface and the runbook as the configuration/recovery guide.

4. Sidecar contract

The sidecar exposes six HTTP routes consumed by the executor: The auth header is Authorization: Bearer $MG_CCIP_SIDECAR_AUTH_TOKEN. The token is a deployment-level secret — rotating it requires both QTG and sidecar to receive the new value, so coordinate via a runbook.

4.1 Status classification

CCIPSidecarClient._request partitions HTTP responses:
  • 200 → success, parse JSON.
  • 400 / 401 / 409 / 422FatalMovementError with the sidecar’s error_code.
  • 404 / 429 / 500 / 502 / 503 / 504TemporaryMovementError (executor will retry).
  • Anything else → TemporaryMovementError("ccip_sidecar_unknown_status") — treat as transient pending diagnosis.

5. Intent lifecycle and observability

A CCIP movement node’s lifecycle in QTG matches the intent FSM (see CCIP Lane):
The executor surfaces these provider refs on every observe round:
polling_phase is computed from (elapsed_seconds, lane_latency_seconds) and persisted on every observe call — operators can query the field directly to find intents stuck over_baseline before they tip into fatal.

6. Settings reference

If MG_CCIP_SIDECAR_BASE_URL is empty at bootstrap, register_builtin_ccip_executors returns None and the executor is not registered. CCIP movements then fail at preflight with an unrecognized-executor error rather than running against a missing sidecar.