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.
- Chain-match pin (movement creation and dispatch). A CCIP source or
destination address is allowed only if its
AllowedAddressrow carries the exact on-chainchain_idderived 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. - Submit-time signed-artifact invariant (broadcast boundary). Before the
sidecar broadcasts, QTG independently RLP-decodes the actual signed
ccipSendtransaction 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_JSONmaps every CCIP selector you transact on (source and destination) to its EVMchain_id. - Every CCIP source/destination address has an
AllowedAddressrow withchain_family='evm'andchain_idset 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):
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 anAllowedAddress row with:
chain_family = 'evm'chain_id= the value the address’s selector maps to inMG_CCIP_CHAIN_IDS_JSONis_active = true
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
- Create a CCIP movement. It must compile without
ADDRESS_NOT_ALLOWED/CHAIN_ID_NOT_CONFIGURED. - Approve + dispatch. The node must progress past the address guard
(PREPARING/SUBMITTING), not land in
FAILEDwith an address error.
Error-code → cause → fix
The observable nodeerror_codeis always the exception’s first arg (ccip_signed_artifact_mismatchfor the submit-time guard,ADDRESS_NOT_ALLOWED/CHAIN_ID_NOT_CONFIGUREDfor the address guard). The precise discriminator is indetail.error_codeand is also appended to the node’serror_detailcolumn — 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 inSUBMITTING, 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.