> ## 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 allowlist chain match setup

# CCIP Allowlist Chain-Match Setup — Operator Runbook

**When to run:** Once per deployment before the first CCIP movement after the
signed-recipient invariant + chain-match pin lands, and again whenever you add a
new CCIP `(source, destination)` lane or a new source/destination address.

**Why this exists:** The CCIP lane now enforces two fund-outflow safety gates on
top of the shared address allowlist. Both are **CCIP-only** — CCTP, Gateway, and
CEX lanes are unchanged.

1. **Chain-match pin** (movement creation *and* dispatch). A CCIP source or
   destination address is allowed only if its `AllowedAddress` row carries the
   exact on-chain `chain_id` derived from the lane's CCIP selector. Chain-agnostic
   rows (`chain_id IS NULL`, the dev-quickstart / admin default) no longer pass
   for CCIP. Dispatch-time is authoritative; creation-time is defense-in-depth so
   a misconfigured movement is rejected before approval instead of failing a node
   after funds are committed to the plan.
2. **Submit-time signed-artifact invariant** (broadcast boundary). Before the
   sidecar broadcasts, QTG independently RLP-decodes the *actual signed*
   `ccipSend` transaction and asserts its router, destination chain selector,
   receiver, and token equal the trusted intent / binding. Any divergence fails
   the node closed — the sidecar never broadcasts. This is the first QTG-side
   cross-check of the sidecar-built receiver against the movement intent.

> Executor surface: `docs/reference/executors/bridges/ccip-bridge.md`.

***

## Quick checklist

* [ ] `MG_CCIP_CHAIN_IDS_JSON` maps **every** CCIP selector you transact on
  (source *and* destination) to its EVM `chain_id`.
* [ ] Every CCIP source/destination address has an `AllowedAddress` row with
  `chain_family='evm'` **and** `chain_id` set to the mapped value.
* [ ] A test CCIP movement creates and dispatches past the address guard.

***

## 1. Configure the selector → chain\_id map

`MG_CCIP_CHAIN_IDS_JSON` is a JSON object mapping each CCIP chain selector to its
EVM `chain_id`. It already feeds the `evm_finality` probe; this slice makes it
**also** the authority for the allowlist chain-match. The default is empty
(`{}`), which fails closed.

Testnet example (Ethereum Sepolia + Base Sepolia):

```bash theme={null}
MG_CCIP_CHAIN_IDS_JSON='{"16015286601757825753":11155111,"10344971235874465080":84532}'
```

Mainnet example (subset — match to the selectors your lanes use):

```bash theme={null}
MG_CCIP_CHAIN_IDS_JSON='{"4949039107694359620":42161,"15971525489660198786":8453,"4051577828743386545":137,"3734403246176062136":10}'
```

Every selector that appears as a CCIP `source_chain_selector` or
`dest_chain_selector` in your lane registry must be present. A missing selector
fails the lane closed (see the error table below). Restart the API/worker
processes after changing the value.

## 2. Chain-scope the allowlist rows

Every CCIP source and destination address needs an `AllowedAddress` row with:

* `chain_family = 'evm'`
* `chain_id` = the value the address's selector maps to in
  `MG_CCIP_CHAIN_IDS_JSON`
* `is_active = true`

Rows added via the dev-quickstart or the generic admin path default to
`chain_id IS NULL` and **will fail closed for CCIP**. Backfill them with the
correct `chain_id` (the same address may legitimately have multiple rows, one per
chain it operates on). CCTP/Gateway rows can stay chain-NULL — they are
unaffected.

## 3. Verify

1. Create a CCIP movement. It must compile without `ADDRESS_NOT_ALLOWED` /
   `CHAIN_ID_NOT_CONFIGURED`.
2. Approve + dispatch. The node must progress past the address guard
   (PREPARING/SUBMITTING), not land in `FAILED` with an address error.

***

## Error-code → cause → fix

| Surface            | Symptom                                                   | `detail.error_code`          | Cause                                                                                                   | Fix                                                                  |
| ------------------ | --------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Creation           | `MovementValidationError` (movement create rejected)      | `CHAIN_ID_NOT_CONFIGURED`    | A lane selector is missing from `MG_CCIP_CHAIN_IDS_JSON`                                                | Add the selector→chain\_id entry; restart                            |
| Creation           | `MovementValidationError`                                 | `INVALID_CHAIN_IDS_JSON`     | `MG_CCIP_CHAIN_IDS_JSON` is not valid JSON, or maps a selector to a non-integer / non-positive chain id | Fix the JSON value (chain ids must be positive integers); restart    |
| Creation           | `MovementValidationError`                                 | `ADDRESS_NOT_ALLOWED`        | No allowlist row for `(address, chain_id)` — row missing, `chain_id` NULL, or wrong `chain_id`          | Add/backfill the row with the correct `chain_id`                     |
| Dispatch           | Node `FAILED`, `error_code=CHAIN_ID_NOT_CONFIGURED`       | `CHAIN_ID_NOT_CONFIGURED`    | Same as creation, but reached at dispatch (e.g. config drifted after create)                            | Same as creation                                                     |
| Dispatch           | Node `FAILED`, `error_code=ADDRESS_NOT_ALLOWED`           | `ADDRESS_NOT_ALLOWED`        | Same as creation, reached at dispatch                                                                   | Same as creation                                                     |
| Submit (broadcast) | Node `FAILED`, `error_code=ccip_signed_artifact_mismatch` | `SIGNED_TARGET_MISMATCH`     | The signed tx's router, dest chain selector, or token diverged from the trusted intent/binding          | Investigate the sidecar build; **do not blind-retry** (see recovery) |
| Submit (broadcast) | `ccip_signed_artifact_mismatch`                           | `SIGNED_RECIPIENT_MISMATCH`  | The signed tx's `ccipSend` receiver ≠ `intent.destination.address`                                      | Same as above                                                        |
| Submit (broadcast) | `ccip_signed_artifact_mismatch`                           | `INVALID_SIGN_RESULT_FORMAT` | The signer returned a non-`evm_raw_tx` sign result                                                      | Check the signer configuration for the CCIP lane                     |
| Submit (broadcast) | `ccip_signed_artifact_mismatch`                           | `SIGNED_TX_DECODE_FAILED`    | The signed payload is not a decodable legacy tx (e.g. an EIP-1559/typed-tx envelope)                    | CCIP requires a legacy tx; check the signer                          |

> The observable node `error_code` is always the exception's first arg
> (`ccip_signed_artifact_mismatch` for the submit-time guard,
> `ADDRESS_NOT_ALLOWED` / `CHAIN_ID_NOT_CONFIGURED` for the address guard). The
> precise discriminator is in `detail.error_code` and is also appended to the
> node's `error_detail` column — e.g. `error_code=ccip_signed_artifact_mismatch`,
> `error_detail="ccip_signed_artifact_mismatch (SIGNED_TARGET_MISMATCH)"` — so the
> specific reason is visible directly from the failed node record (and the logs).

***

## Recovery — submit-time signed-artifact mismatch

A signed-artifact mismatch means the transaction that *would have been broadcast*
does not match the approved intent. On detection, QTG tombstones the intent and
releases the nonce reservation atomically (mirroring the registry-version-drift
terminal pattern) before failing the node — so no broadcast occurs and the
reservation is freed.

If the atomic tombstone affects **0 rows** (the intent was not in `SUBMITTING`, or
the reservation id no longer matches), the reservation may be stranded; a
`ccip_eager_tombstone_rowcount_0` warning is logged. Follow
`docs/runbooks/ccip-recover-cutover-and-residuals.md` and let the stranded-row
reaper reclaim it.

**Do not retry the same payload.** A mismatch is evidence the sidecar or signer
produced a tx that diverges from the intent; the correct action is to investigate
that divergence, not to resubmit.

***

## What is NOT affected

CCTP, Gateway, and CEX lanes retain chain-agnostic allowlist matching
(`require_chain_match=False`). No `MG_CCIP_CHAIN_IDS_JSON` or `chain_id` backfill
is required for them. This runbook applies to the CCIP lane only.
