> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# CCIP Bridge

> Operator surface for the Chainlink CCIP send executor and lane registry

# 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:

| Layer         | Owner                                | Role                                                                                         |
| ------------- | ------------------------------------ | -------------------------------------------------------------------------------------------- |
| Sidecar       | `CCIPSidecarClient` over HTTP        | Builds payloads via the Chainlink TS SDK, broadcasts signed raw tx, polls SDK message status |
| Lane registry | `ccip_routers` + `ccip_lanes` tables | Approved `(src_selector, router)` pairs and approved `(src, dst, token)` lanes               |
| Intent ledger | `ccip_send_intents` table            | Per-node row with nonce reservation, payload hash, status FSM                                |

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_hmac` → `require_request_auth`
* Accepted purposes: `admin`, `all`
* Canonical mapping source: `interfaces/api/middleware/auth/hmac.py`

### 2.2 Routes

| Method | Path                                  | Request                                                           | Response                                                                                                                                                                                  | Purpose                                    |
| ------ | ------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `POST` | `/v3/executors/ccip/refresh-registry` | `RefreshRegistryRequest` — `dry_run: bool`, `operator_label: str` | `RefreshRegistryResponse` — `applied`, `diff` (RouterDiff), `lane_diff` (LaneDiff), `network_info_hash`, `sdk_version`, `sdk_version_hash`, `requires_reapproval`, `new_snapshot_version` | rebuild registry from sidecar network-info |

`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.
   ```bash theme={null}
   curl -X POST "$QTG_BASE/v3/executors/ccip/refresh-registry" \
     -H "$(qtg-hmac-sign POST /v3/executors/ccip/refresh-registry '{"dry_run":true,"operator_label":"ops.ccip-refresh"}')" \
     -d '{"dry_run": true, "operator_label": "ops.ccip-refresh"}'
   ```
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`:

| Status                              | Meaning                                                           |
| ----------------------------------- | ----------------------------------------------------------------- |
| `400 / 401 / 409 / 422`             | Fatal — sidecar rejected the request (bad auth, schema violation) |
| `404 / 429 / 500 / 502 / 503 / 504` | Temporary — sidecar transient failure; retry safely               |
| `403` from QTG                      | HMAC purpose is not `admin` or `all`                              |

***

## 3. Lane registry semantics

The lane registry is the **approval surface** that gates `exec.ccip.send` at runtime. Three column states matter:

| Column             | Required value to dispatch             | Failure error\_code                                |
| ------------------ | -------------------------------------- | -------------------------------------------------- |
| `status`           | `"active"`                             | `ccip_token_lane_inactive_pre_broadcast`           |
| `trust_tier`       | `"trusted"`                            | `ccip_token_quarantined_pre_broadcast` (Temporary) |
| `registry_version` | matches `snapshot_version_at_dispatch` | `ccip_registry_version_drift_at_submit` (Fatal)    |

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](/runbooks/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:

| Method | Path                             | Caller                     | Purpose                                                       |
| ------ | -------------------------------- | -------------------------- | ------------------------------------------------------------- |
| `POST` | `/prepare`                       | `CCIPSendExecutor.prepare` | Build canonical payload for `sendToken`                       |
| `POST` | `/submit`                        | `CCIPSendExecutor.submit`  | Broadcast signed raw tx, return `send_tx_hash` + `message_id` |
| `POST` | `/observe`                       | `CCIPSendExecutor.observe` | Poll SDK `getMessageStatus`                                   |
| `GET`  | `/health`                        | bootstrap                  | Return `ccip_send_topic0` for log scanning                    |
| `GET`  | `/network-info`                  | refresh service            | Return canonical routers + lanes + `sdk_version`              |
| `POST` | `/internal/decode-ccip-sent-log` | log scanner                | Decode a `CCIPSendRequested` log into structured fields       |

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 / 422` → `FatalMovementError` with the sidecar's `error_code`.
* `404 / 429 / 500 / 502 / 503 / 504` → `TemporaryMovementError` (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](/concepts/bridges/ccip-lane#intent-fsm)):

```
PREPARED → SUBMITTING → BROADCASTED → MESSAGE_INDEXED → COMPLETED
                ↓             ↓                ↓
       TOMBSTONED_PRE_BROADCAST              FAILED
```

The executor surfaces these provider refs on every observe round:

```json theme={null}
{
  "execution_intent_id": "<uuid>",
  "source_chain_selector": "<uint64 string>",
  "dest_chain_selector":   "<uint64 string>",
  "send_tx_hash":          "0x...",
  "message_id":            "0x...",
  "sdk_message_status":    "<raw SDK string>",
  "off_ramp_address":      "0x...",
  "last_tx_hash":          "0x...",
  "polling_phase":         "pre_baseline|at_baseline|over_baseline|fatal"
}
```

`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

| Env                                | Default                  | Meaning                                                       |
| ---------------------------------- | ------------------------ | ------------------------------------------------------------- |
| `MG_CCIP_SIDECAR_BASE_URL`         | *empty*                  | Sidecar base URL. Empty → CCIP executor not registered.       |
| `MG_CCIP_SIDECAR_AUTH_TOKEN`       | *empty*                  | Bearer token for sidecar auth                                 |
| `MG_CCIP_EVM_RPC_ENDPOINTS_JSON`   | `""` (empty, not `"{}"`) | Per-selector RPC endpoint map                                 |
| `MG_EVM_RPC_TIMEOUT_SECONDS`       | `10`                     | Per-call RPC timeout (shared with other EVM lanes)            |
| `MG_CCIP_LOG_SCAN_LOOKBACK_BLOCKS` | (settings default)       | Block window for `find_matching_onchain_send` orphan recovery |

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.

***

## 7. Related Docs

* [CCIP Lane concepts](/concepts/bridges/ccip-lane) — the design and protocol-level story
* [Bridge Lane overview](/concepts/bridge-lane) — comparison with CCTP and LayerZero
* [CCTP Lane reference](/reference/executors/bridges/cctp-lane) — sibling lane
* [Executors overview](/reference/executors/overview) — common executor protocol
