CCIP Message Lifecycle
This page follows a single cross-chain message from send to delivery, demonstrating how CCIP's secure-by-default message path works. For component descriptions and architecture context, see the CCIP v2 Architecture page.
Overview
When the application sends a cross-chain message on a lane, four phases run in order:
- Send — on the source chain
- Verify — offchain attestation
- Index — collect attestations into one store
- Execute — on the destination chain
End-to-end CCIP v2 message lifecycle.
Phase 1 — Source-chain send
1.1 Quote the fee
The application calls Router.getFee(destChainSelector, message).
- The Router resolves the active OnRamp for the destination and delegates to
OnRamp.getFee(). - If
feeTokenis empty, the Router treats payment as the native token (internally using the wrapped native address for pricing). OnRamp.getFee()builds the same verifier and executor list that send will use, then queries:- Fee Quoter — converts gas and byte overheads into a total fee in the fee token.
- Each CCV —
getFee()per verifier (flat USD cents plus gas/byte components). - Token pool —
getFee()if the pool implements the V2 interface (skipped if no token transfer). - Executor —
getFee()unless the no-execution address is selected (zero executor fee).
getFee()must match the send path — the same logic runs again insideforwardFromRouter(), so a mismatch between quote and send causes the send to revert.
If paying in an ERC-20, the application approves the Router for the quoted amount before sending.
1.2 Send via the Router
The application calls Router.ccipSend(destChainSelector, message) with the payload, tokens (if any), fee token, and extraArgs in the message. New integrations should use ExtraArgsV3 (CCVs, executor, finality, gas limit, token receiver, token/executor args). Existing applications can continue sending legacy extra args on 2.0 rails — the OnRamp normalizes them internally before building the message.
Supported extraArgs formats (EVM source → EVM destination):
| Format | When used | What the sender controls | v2 defaults applied |
|---|---|---|---|
ExtraArgsV3 | New integrations | CCVs, executor, finality, gas limit, token receiver, token/executor args | Lane default CCVs/executor only when the sender passes zero-value placeholders |
GenericExtraArgsV2 | Existing v1.x-style apps | Gas limit (allowOutOfOrderExecution was deprecated) | Chainlink Committee Verifier; Chainlink executor; messages wait for full finality on source |
EVMExtraArgsV1 | Existing v1.x-style apps | Gas limit only | Same as above |
Empty extraArgs | Apps that omit extra args | Nothing | Same as above |
Router (ccipSend):
- Reverts if the destination lane is not supported (
isChainSupported) or the RMN contract reports a curse. - Re-computes the fee via
OnRamp.getFee()(validates that the fee token is allowed). - Native fee token: wraps
msg.valueand transfers the wrapped native to the OnRamp. - ERC-20 fee token:
transferFromthe application to the OnRamp;msg.valuemust be zero. - For each token in the message:
transferFromthe application to the token pool (pool resolved viaOnRamp.getPoolBySourceToken()). - Calls
OnRamp.forwardFromRouter(destChainSelector, message, feeTokenAmount, msg.sender)—msg.senderbecomes the recorded source sender in the message.
1.3 OnRamp processing (forwardFromRouter)
1. Parse extra args and apply defaults
- If
extraArgscarries the V3 tag (0xa69dd4aa), decodesExtraArgsV3directly. - Otherwise, calls
FeeQuoter.resolveLegacyArgs()to extract the gas limit from V1/V2 legacy formats, then applies 2.0 defaults for everything else. - Resolves CCVs, executor, finality, gas limit, token receiver, and args into an internal V3-shaped struct.
- Applies the lane default executor when the sender omits one; applies lane default CCVs, as applicable, when the sender omits a verifier list.
- Validates the destination receiver address length for the destination chain family.
2. Merge verifier lists
Final CCV list = sender preferences + lane defaults (where applicable) + lane-mandated (always) + pool-required (from getRequiredCCVs() on V2 pools, or lane defaults for V1 pools).
Computes ccvAndExecutorHash from the final CCV list and executor. This hash is embedded in the message for offchain validation.
3. Compute and distribute fees
- Calls
getFee()on each CCV, the pool (if applicable), and the Executor — the same participants as the quote path. - Sums
executionGasLimitfrom the receipt gas components (CCV verification gas, pool overhead, executor execution cost, user gas limit). - Reverts with
InsufficientFeeTokenAmountif the fee paid is less than computed. _distributeFees()transfers fee-token payments to each receipt issuer (CCVs, pool, executor; the network fee is retained on the OnRamp).
4. Lock or burn tokens
- At most one token per message.
- Calls the pool's
lockOrBurn()with finality and token args; pool output (destPoolData) is encoded into the message. - Validates that the pool extra data fits within the byte overhead quoted in the fee receipt.
5. Encode message and compute message ID
- Assigns the next per-lane
messageNumber(monotonic, starts at 1). - Encodes
MessageV1and setsmessageId = keccak256(encodedMessage).
6. Register with each CCV (outbound)
For each CCV in the final list:
- Resolves the outbound implementation via
ICrossChainVerifierResolver.getOutboundImplementation(). - Calls
ICrossChainVerifierV1.forwardToVerifier()— runs the CCV's outbound send hook and returns a verifier blob: CCV-specific bytes the OnRamp includes inCCIPMessageSent.verifierBlobs. Offchain verifier nodes use this blob to match the message and build attestations. Example: the Committee Verifier returns its 4-byte version tag (prepended to the hash nodes sign).
7. Emit event
Emits CCIPMessageSent with:
messageId,encodedMessage,receipts(fee breakdown per issuer), andverifierBlobs(per-CCV hints).sender(original application address),feeToken, and the token amount before pool fees.
Curse status is checked at both the Router and the OnRamp — a cursed destination (on send) causes a revert.
Phase 2 — Offchain verification
2.1 Shared verifier pipeline
Each CCV type's offchain service:
- Monitors the source chain for
CCIPMessageSent. - Filters events where
receiptsinclude an entry issued by that CCV's onchain resolver address. - Decodes
encodedMessageand confirmsmessageIdmatcheskeccak256(encodedMessage). - Reads the requested finality from the message and waits until the source chain meets that verifier's onchain policy (full finality by default).
- Checks RMN curse status — cursed source/destination subjects are not attested.
- Reads
verifierBlobsfrom the event — the CCV-specific bytes returned byforwardToVerifieron send — and uses them to match the message and build attestations (Committee Verifier: 4-byte version tag prepended to the hash nodes sign).
2.2 Committee Verifier (default)
For each message:
- Each committee node independently signs a hash over the verifier version tag and
messageId(which commits to the full encoded payload). - Posts the signature to the Aggregator over an authenticated channel.
- The Aggregator validates each ECDSA signature, persists it, and checks quorum for the source chain.
- When quorum is reached, it stores the combined
VerifierResultand exposes it via a read API.
2.3 Additional CCVs
Each additional verifier service operates independently — separate monitoring, finality wait, and publication path. Results are stored in that verifier's own database and exposed via its REST API (not the Aggregator). Some apply only when their specific token is transferred.
Attestations are bound to messageId — offchain services do not re-derive a different identity than the onchain message.
Phase 3 — Indexing
3.1 Discovery
- The Indexer polls the Aggregator using a sequence cursor to discover newly verified messages without scanning by message ID.
- For each discovered message ID, it checks local storage for which CCV attestations are still missing.
3.2 Collection
- Committee Verifier results — fetched from the Aggregator (by message ID or via the cursor stream).
- Additional CCVs — fetched from each configured verifier's API.
- Missing attestations are retried until present, or until operational timeout policies apply.
3.3 Storage
- Attestations are stored keyed by message ID and CCV identity.
- Only CCV types in the Indexer's configuration are collected — if a message requires an untracked CCV, automated execution may stall until the configuration is updated.
Executor services may bypass the Indexer and query the Aggregator and verifier APIs directly.
Phase 4 — Destination-chain execution
4.1 Prepare submission
Once the required attestations exist:
- The application or any party calls
OffRamp.execute(encodedMessage, ccvs, verifierResults, gasLimitOverride)on the destination chain. - The
ccvsandverifierResultsarrays must be the same length — each entry pairs a CCV resolver address with its proof bytes. - A
gasLimitOverrideof0means use the gas limit from the message; a non-zero override must be ≥ccipReceiveGasLimitin the message. Example: if the message specifies200_000, pass0to use200_000, or300_000to grant more headroom;100_000reverts.
The executor chosen at send time governs fees — it does not restrict who may call execute.
4.2 Default executor service (offchain)
The default executor service runs these checks before submitting:
- Reads
OffRamp.getCCVsForMessage()(view) — re-derives the required and optional CCVs from the receiver, pool, and lane config for the encoded message (the same logic as onchain execution). - Confirms all required CCVs have attestation results and that the optional quorum is met.
- Checks curse status and the
OffRampexecution state for the message ID (skips if already SUCCESS). - Submits
execute(); retries on FAILURE within a configured window (currently 8h).
If another party has already reached SUCCESS, the service skips the message.
4.3 OffRamp entry checks (execute)
Before execution begins, the OffRamp validates:
- The source chain is not cursed and is enabled in lane config.
onRampAddressin the message matches an allowlisted OnRamp for that source chain.offRampAddressin the message matches this OffRamp contract.destChainSelectormatches the local chain.- The message has not already SUCCESSfully executed (allows a retry from FAILURE, or a first attempt from UNTOUCHED).
Sets the execution state to IN_PROGRESS, then calls executeSingleMessage() internally (with a gas buffer so state can be updated even if inner execution runs out of gas).
4.4 OffRamp message processing (executeSingleMessage)
1. Resolve required CCVs (independent of submitter)
Re-derives the required set via _getCCVsForMessage() from:
- Receiver —
getCCVsAndFinalityConfig(sourceChainSelector, sender)if the receiver implements the v2 interface (required + optional CCVs with an optional threshold). - Token pool — inbound
getRequiredCCVs()(or lane defaults for V1 pools). - Lane configuration — lane-mandated CCVs are always included.
For token-only transfers, receiver CCVs and sender-level defaults are omitted; pool and lane-mandated verifiers still apply.
2. Verify CCV quorum
This step runs before token release or receiver delivery. The OffRamp first confirms quorum at the CCV-set level, then calls each CCV's inbound verification.
CCV-set quorum (OffRamp):
- Confirms every required CCV resolver address is present in the submitted
ccvsarray — revertsRequiredCCVMissingif not. - Confirms the optional CCV threshold is met when the receiver specified optional verifiers — reverts
OptionalCCVQuorumNotReachedif not. - Extra CCV entries beyond what is required or optional are ignored (not verified).
- Only CCVs that pass this set-level check proceed to
verifyMessage.
Per-CCV verification (resolver → implementation):
For each CCV to verify:
getInboundImplementation(verifierResults[i])on the CCV resolver — reads the 4-byte version tag prefix from the proof and returns the inbound implementation address. RevertsInboundImplementationNotFoundif unregistered.verifyMessage(message, messageId, verifierResults[i])on that implementation — must succeed (not revert) for execution to continue.
The messageId passed here is keccak256(encodedMessage) — the same ID committee nodes signed offchain and the Aggregator keyed results on.
Checks inside CommitteeVerifier.verifyMessage:
The Committee Verifier is the baseline CCV on every lane. Its destination-side verifyMessage performs onchain checks on the decoded message and the proof bytes. Offchain, each committee node signs keccak256(versionTag ‖ messageId) and posts to the Aggregator. When quorum is reached, the Aggregator packs the quorum signatures (sorted by signer address) and exposes them as CcvData in the VerifierResult. Executors pass this blob as verifierResults[i] at execution time.
- RMN curse — reverts if the message's source chain selector is cursed.
- Minimum length — the proof must contain at least the version tag and signature-length prefix; otherwise
InvalidVerifierResults. - Version tag match — the first 4 bytes must equal this implementation's immutable
versionTag(); otherwiseInvalidCCVVersion. This binds the proof to this verifier type and prevents cross-CCV replay. - Signature length sanity — the declared signature length must not exceed the bytes available; otherwise
InvalidVerifierResults. - ECDSA quorum validation — recomputes the signed hash as
keccak256(bytes.concat(verifierVersion, messageId))and validates signatures:- Loads the signer allowlist and threshold configured for this message's source chain selector on the destination Committee Verifier. Reverts
SourceNotConfiguredif none. - Rejects if the chain ID at deployment differs from the current chain ID (
ForkedChain) — a defense against replay on a forked network. - Requires at least threshold signatures in the blob; otherwise
WrongNumberOfSignatures. - For the first threshold signatures (in order): recovers the signer with
ecrecover, checks the signer is in the allowlist (UnauthorizedSigner), and enforces strictly increasing signer addresses (NonOrderedOrNonUniqueSignatures) to prevent duplicate votes. - Additional signatures beyond the threshold may be present; only the first threshold are validated (a gas optimization).
- Loads the signer allowlist and threshold configured for this message's source chain selector on the destination Committee Verifier. Reverts
What this means:
- The proof authorizes this exact message ID, which commits to the full encoded payload (receiver, tokens, data, finality, etc.) — not a different message or a partial edit.
- At least a quorum of independent, allowlisted committee signers attested to that ID for this verifier version.
- The attestation cannot be applied while the source chain is cursed, or through the wrong CCV implementation.
Failure behavior:
Any revert from verifyMessage fails the entire executeSingleMessage call. No tokens are released and the receiver is not called. The execution state may be recorded as FAILURE (retryable) depending on where the revert occurred.
Additional CCV types use the same resolver routing and OffRamp call pattern, but their verifyMessage validates their own proof formats (for example, Circle CCTP attestation bytes or Lombard attestation bytes) instead of committee ECDSA quorum.
3. Release or mint tokens
- If the message includes a token transfer: calls the pool's
releaseOrMint()with the source sender, source chain, and finality from the message. - Delivers to
tokenReceiverfrom the encoded message (or the message receiver if none is specified). - Confirms the delivered amount via balance delta (unless the pool itself is the receiver).
4. Deliver to receiver (if not token-only)
- Skipped when
datais empty,ccipReceiveGasLimitis zero, or the receiver does not implement the receiver interface — the pure token-only path. - Otherwise calls
Router.routeMessage()→ the receiver'sccipReceive(Any2EVMMessage)with exact gas enforcement. - The
execute()caller (executor or manual submitter) never calls the receiver directly — the Router does. InccipReceive, verify thatmsg.senderis the Router. The original source-chain application ismessage.senderin the decoded message.
If ccipReceive reverts, the entire execution attempt fails atomically (tokens are not successfully delivered in that attempt).
4.5 Execution outcome
- SUCCESS — recorded in
s_executionStates[messageId]; the message cannot be executed again. EmitsExecutionStateChanged. - FAILURE — recorded; the message may be retried. Emits
ExecutionStateChangedwith error data. - A retry that fails again leaves the message in
FAILURE, ready to be retried once the underlying issue is fixed.