Skip to main content

CCIP Lane (Chainlink Cross-Chain Interoperability Protocol)

What is CCIP?

Chainlink CCIP is a generic cross-chain messaging and token-transfer protocol. Where CCTP burns and mints USDC against a single Circle attestation, CCIP supports arbitrary tokens registered in its router, and the attestation is produced by a Chainlink DON plus an independent Risk Management Network (ARM). The high-level flow is: The “ARM curse” step is what distinguishes CCIP from CCTP and other single-attester bridges: an independent network can reject a transfer even after the DON has committed it.

The CCIP lane in QTG

CCIP is exposed as a single executor keyexec.ccip.send — that owns both the send-side dispatch and the observe-side polling. This differs from CCTP’s 5-node split because the destination-side execution is performed by Chainlink’s off-ramp, not by QTG.

Intent FSM

CCIP intents transition through a persisted status column: The terminal statuses are COMPLETED, FAILED, and TOMBSTONED_PRE_BROADCAST. The tombstone is special: it releases the nonce slot for retry, whereas a post-broadcast FAILED keeps the nonce locked (the tx is on-chain and the nonce is consumed even on revert). Concretely, every status except TOMBSTONED_PRE_BROADCAST is treated as holding an active nonce.

Required bindings

The lane refuses to run unless all of these template bindings are resolved:
  • source_chain_selector
  • dest_chain_selector
  • router
  • sender
  • token
  • amount
  • receiver
Missing bindings fail the node with ccip_missing_binding and a detail listing exactly which bindings are absent. source_chain_selector and dest_chain_selector are the Chainlink-assigned uint64-encoded chain IDs (e.g. Ethereum mainnet = 5009297550715157269). They are distinct from EVM chain IDs.

Preflight: lane registry gating

The first thing preflight does after binding validation is a lane-registry dispatch check. This:
  1. Looks up the approved router for the source chain in the lane registry.
  2. Verifies the (source_chain_selector, router) pair is status="active".
  3. Returns the current snapshot_version — a monotonically increasing version stamp on the registry.
The snapshot version travels with the intent and is re-checked at submit time. If the registry was rotated between prepare and submit, the lane raises ccip_registry_version_drift_at_submit and tombstones the intent — preventing the signed payload from being broadcast against a stale router approval.

Signed-recipient invariant (validated == signed)

CCIP participates in QTG’s cross-lane signed-recipient invariant: the recipient/source re-derived from the actual signed artifact must equal the allowlist-validated intent, fail-closed (SIGNED_RECIPIENT_MISMATCH / SIGNED_SOURCE_MISMATCH). CCIP is a strict source + destination action (ccip_send), and prepare-time intent-only resolution pins the receiver before signing.
The submit path re-derives the signed ccipSend artifact before sidecar broadcast. It pins router, destination chain selector, receiver, token, amount, native fee, signer identity, and source chain id against the trusted intent and fails closed as ccip_signed_artifact_mismatch on mismatch. The registry-version drift recheck below still guards which router snapshot is allowed to sign. See Bridge Lane → Signed-recipient invariant.

Prepare: nonce allocation + payload hash

Key invariants:
  • Nonce is allocated under a Postgres advisory lock to prevent two prepare calls from claiming the same chain nonce.
  • The allocated nonce is max(chain_pending_nonce, db_max_active_nonce + 1) — guards against forgotten in-flight tx.
  • If the chain’s pending nonce has already passed the allocated nonce, the lane performs a log scan over CCIPSendRequested events to detect a tx that was broadcast but lost its DB write. A match resumes from SUBMITTING; no match means the nonce was burned elsewhere and the intent is tombstoned.
  • The payload hash is computed by the sidecar, verified locally, and persisted alongside the intent. It is later re-verified at submit time to guard against payload mutation between prepare and submit.

Submit: registry drift recheck + raw broadcast

submit is more than a thin wrapper around eth_sendRawTransaction. In order, it:
  1. Loads the intent for this node; if there is no row, it fails with ccip_intent_not_found.
  2. Short-circuits if the intent already has a send_tx_hash — it just hydrates the message_id rather than re-broadcasting.
  3. Re-checks registry drift between sign and submit; a rotated registry eagerly tombstones the intent.
  4. Forwards the signed raw tx to the sidecar along with the expected payload hash, expected router, and snapshot version, so the sidecar can reject a mismatch before broadcasting.
  5. Persists the returned send_tx_hash and message_id.
If the sidecar succeeds but the message log isn’t yet indexed, the sidecar raises ccip_message_indexing_timeout (Temporary). QTG persists the send_tx_hash alone so the next observe round can pick up the message_id, and re-raises.

Observe: status mapping

The sidecar’s /observe endpoint returns the Chainlink SDK message status as a string. QTG maps it to a node state (and a terminal flag): The interesting non-terminal stages are:
A cursed status is QTG’s terminal FAILED, not a recoverable state. The Risk Management Network has cryptographically attested that this transfer must not be executed. Recover does not retry — the failure is propagated and operators must investigate why ARM rejected the message.

Polling pacing: lane latency baseline

CCIP messages have wildly different end-to-end latencies — minutes on common L2 pairs, tens of minutes on cross-network paths. Rather than a fixed poll interval, the lane reads a per-lane latency baseline from the registry (falling back to 600 seconds) and classifies elapsed time against it. The polling phase is one of pre_baseline, at_baseline, over_baseline, or fatal. The fatal phase fires when elapsed exceeds a hard multiple of the baseline — at that point the lane raises ccip_stalled_no_execution and FAILs the node so the operator can investigate.

Error code reference

Curated subset of CCIP-specific terminal errors:

Why a sidecar?

The Chainlink CCIP SDK is a TypeScript library. Running it in-process from Python would mean shipping a Node runtime inside the QTG image. The sidecar approach instead:
  • Keeps QTG free of a heavyweight TS dependency.
  • Lets the sidecar evolve independently (it’s the only place that needs CCIP SDK upgrades).
  • Centralizes Chainlink network connection state.
  • Gives the executor a single well-defined HTTP contract (/prepare, /submit, /observe, /health, /network-info, /internal/decode-ccip-sent-log).
The trade-off is operational: the sidecar must be reachable for any CCIP movement to proceed, and its auth_token is a deployment-level secret (configured via MG_CCIP_SIDECAR_BASE_URL and MG_CCIP_SIDECAR_AUTH_TOKEN).