CCTP Lane Executors
Source files
src/qtg/infrastructure/executors/cctp/__init__.pysrc/qtg/infrastructure/executors/cctp/burn.py- `src/qtg/infrastructure/executors/cctp/mint.py>
src/qtg/infrastructure/executors/cctp/common.pysrc/qtg/infrastructure/executors/cctp/api.pysrc/qtg/infrastructure/executors/observe/cctp.pysrc/qtg/infrastructure/executors/observe/evm.py
1. CCTP 5-node lane overview
A cross-chain USDC transfer via Circle CCTP (Cross-Chain Transfer Protocol) v2 is built from 5 nodes. CCTP 완료 callback = destination에서 expected 주소/자산/금액 이상 수령 관찰 + finality confirmations.Executor key mapping
Nodes 2, 4, and 5 are structured as a generic observe executor with a bound probe. The CCTP-specific code lives in the probe.
The descriptive node keys remain
attestation and mint_receive_observe, but their node action_type values are protocol_observe and destination_chain_receive_observe respectively. The compiler requires every node action_type to exactly equal its executor_selector.action_type.
Registration (bootstrap)
exec.observe.protocol, exec.observe.destination_chain_receive, and exec.observe.destination_chain_finality are registered in register_builtin_observe_executors(), and the CCTP attestation probe is registered separately via register_protocol_probe("cctp_iris", ...).
Signed-recipient invariant (validated == signed)
In addition to thedestination_caller triple-check (see §5.1), CCTP burn/mint enforce QTG’s cross-lane signed-recipient invariant: the recipient and source re-derived from the actual signed transaction must equal the allowlist-validated intent, checked at prepare time (intent-only resolvers) and at submit/broadcast time (RLP-decode of the signed legacy tx). CCTP is a strict source + destination action (cctp_burn / cctp_mint in ONCHAIN_ACTION_TYPES), so both ends are validated.
Mismatch is fail-closed — FatalMovementError → node FAILED:
Enforcement:
src/qtg/infrastructure/executors/recipient_guard.py + src/qtg/infrastructure/executors/signed_evm_tx.py.
2. Provider refs propagation chain (detail)
Key propagation paths:3. CctpBurnExecutor
File:cctp/burn.py
Defined as @dataclass(slots=True). Submits the depositForBurn transaction on the source chain.
3.1 preflight
The most comprehensive on-chain preflight. Six validation stages:-
Parameter validity: extract
source_address,recipient_address,destination_caller,amount_raw,min_finality_threshold,source_chain_id,source_domain_id,destination_domain_id(missing ->FatalMovementError) -
ERC-20 Allowance check:
eth_callofallowance(owner, spender) -
ERC-20 Balance check:
eth_callofbalanceOf(owner) -
CCTP fee check: look up
fee_bpsfrom Circle API and computeminimum_fee -
Fast Allowance check (when threshold == 1000): check Circle API fast burn allowance remaining
-
Gas estimation: pre-validate that the calldata is executable via
eth_estimateGas
3.2 prepare
Builds the EVM transaction payload.- ABI-encode
depositForBurncalldata - Look up nonce / gas / gasPrice
- Return
PreparedAction
signing_required=True, the dispatcher invokes the signer to sign the payload before passing it to submit.
3.3 submit
Broadcasts the signed transaction viaeth_sendRawTransaction.
burn_tx_hash and protocol_ref carry the same value. protocol_ref is used as the IRIS API lookup key by the next node (attestation).
Error handling:
TemporaryMovementError->UNKNOWN(TX broadcast result is uncertain)MISSING_SIGN_RESULT/MISSING_SIGNED_TX->FatalMovementErrorpropagated
3.4 observe
Simple pass-through:3.5 recover
Iftxid or burn_tx_hash is present -> COMPLETED; otherwise FAILED(RECOVERY_FAILED).
4. CCTP Attestation (Node 2)
The attestation node is the combination of the genericProtocolObserveExecutor and the CCTP-specific CctpAttestationProbe.
4.1 ProtocolObserveExecutor
File:observe/protocol.py
A generic observe executor that looks up a probe via probe_key and calls check_proof. For details, see observe-probes.md.
probe_key: "cctp_iris" must be set in node_config.
4.2 CctpAttestationProbe
File:observe/cctp.py
validate_context
Validates thatsource_domain_id is present in node_config and is an integer.
check_proof
- Call
iris_client.get_messages(base_url, source_domain_id, transaction_hash=protocol_ref) - If the response is
Noneor messages is empty ->completed=False(pending) - Select a message:
- If
message_indexis set, pick the message at that index - If not set: pick the only message if there is one; if two or more, raise
MULTIPLE_MESSAGES_AMBIGUOUS
- If
- If the
attestationfield is empty -> pending - If attestation + message_bytes are both present ->
completed=True
GeneratedArtifact and persisted to the DB.
4.3 CctpIrisClient
File:observe/cctp.py
Circle IRIS API v2 client:
Note: on rate limit, the default retry wait is 300 seconds (5 minutes), which is quite long (
DEFAULT_RATE_LIMIT_RETRY_AFTER_SECONDS = 300).
4.4 CircleCctpApiClient
File:cctp/api.py
Looks up CCTP v2 burn fees and fast allowance:
CctpIrisClient, but the retry default is 15 seconds.
5. CctpMintExecutor
File:cctp/mint.py
Submits the receiveMessage transaction on the destination chain.
5.1 preflight
Four validation stages:-
Check that attestation/message_bytes are present:
common.attestation(ctx),common.message_bytes(ctx)— extracted from provider_context -
Look up signer address:
resolve_signer_address_from_binding(ctx.resolved_bindings["signer"])— extracts the address from the signer binding metadata -
destination_caller alignment:
validate_mint_destination_caller_alignment(ctx, signer_address)— verifies that thedestination_callerset at burn matches the signer address that will execute the mint. On mismatch:FAILED(SIGNER_CALLER_MISMATCH)orFAILED(DESTINATION_CALLER_CONFIG_MISMATCH). -
eth_call dry-run: execute
receiveMessage(message, attestation)calldata viaeth_callto pre-check for revert + estimate gas
destination_caller is set at burn time, and only that address can call receiveMessage on the destination chain. MG enforces this twice in preflight:
- Burn stage:
common.destination_caller(ctx)is extracted from node_config - Mint stage: verifies that the signer address matches destination_caller
5.2 prepare
Builds thereceiveMessage EVM transaction payload.
5.3 submit
Broadcasts the signed transaction viaeth_sendRawTransaction.
Provider refs on success:
txid, while mint_tx_hash and dest_txid are kept separately for operational tracking.
5.4 recover (idempotency detection)
Mint recover is the most complex recover logic:-
When tx_hash is present: check via
eth_getTransactionReceipt- receipt missing ->
UNKNOWN(MINT_STATUS_UNKNOWN) - receipt reverted ->
FAILED(TX_REVERTED) - receipt success ->
COMPLETED
- receipt missing ->
-
When tx_hash is absent: dry-run
receiveMessageviaeth_call- executes normally ->
FAILED(MINT_NOT_SUBMITTED)(not yet submitted) - revert messages such as
ALREADY_PROCESSED/already processed/nonce already used->COMPLETED(mint already completed via another path) - Other errors ->
FAILED(MINT_RECOVERY_FAILED)
- executes normally ->
6. Destination Chain Receipt (Node 4)
The combination of the genericDestinationChainReceiveObserveExecutor and EvmChainReceiveProbe. probe_key: "evm", destination chain_id, and match_mode: "txid" are set in node_config; the probe verifies the expected destination address, ERC-20 token, and amount before the lane can reach finality.
6.1 Fast-mode amount floor
Fast CCTP mode (min_finality_threshold=1000) may mint less than the requested amount because minted = amount - fee_executed. Since fee_executed <= max_fee_raw, the receive node uses amount_match: "at_least" and amount_floor_deduction_raw=max_fee_raw to require observed >= scaled(amount) - max_fee_raw. This is a floor, not an exact fee assertion.
7. Destination Chain Finality (Node 5)
The combination of the genericDestinationChainFinalityObserveExecutor and EvmChainFinalityProbe. For details, see observe-probes.md.
probe_key: "evm", chain_id, and confirmations_required must be set in node_config.
The mint tx_hash is propagated via ctx.provider_context["txid"]; once that block’s confirmation count reaches the threshold, the node becomes COMPLETED.
8. CCTP Common Helpers
File:cctp/common.py
8.1 Context extraction functions
8.2 Address conversion
address_to_bytes32_hex is required for CCTP’s mintRecipient and destinationCaller parameters. It left-pads a 20-byte EVM address with zeros to 32 bytes.
8.3 ABI encoding
depositForBurn signature: depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32)
receiveMessage signature: receiveMessage(bytes,bytes)
8.4 EVM utilities
8.5 Fee computation
min_finality_threshold allows 1000 or 2000:
1000= fast transfer (lower finality, separate fast allowance check)2000= standard transfer
9. Node Config requirements (full)
burn node (exec.cctp.burn)
attestation node (exec.observe.protocol)
mint node (exec.cctp.mint)
mint_receive_observe node (exec.observe.destination_chain_receive)
mint_finality node (exec.observe.destination_chain_finality)
10. Canonical template seed CLI
qtg.interfaces.tools.seed_cctp_templates seeds the canonical cctp.single_lane_usdc 5-node lane (burn -> attestation -> mint -> mint_receive_observe -> mint_finality), with completion_assurance=destination_finalized.
--apply가 없으면 항상 dry-run이다. Standard 기본값은--min-finality-threshold 2000,--max-fee-raw 0이다.- latest version이 동일한 canonical content이면
noop; 내용이 다르면add_version으로max(version) + 1을 추가한다. 기존 version row는 갱신하지 않으며 비교에는timeout_policy도 포함된다. - Fast 조합은
--min-finality-threshold 1000와 양수--max-fee-raw를 함께 지정해야 한다. CLI는1000에서 max fee가0이하인 조합을 거부하고, threshold는1000또는2000만 허용한다. - Operator warning: a wrong
--token-decimalsscales the burn amount and receive floor identically, so the receive guard cannot detect a decimals misconfiguration; verifydecimals()on both token contracts before seeding.
11. Observe intervals
Attestation generally takes several minutes; on IRIS API rate limiting
retry_after can rise to as much as 300 seconds.
12. Operator runbook
12.1 DESTINATION_MISMATCH / AMOUNT_MISMATCH
MANUAL_INTERVENTION으로 올라가면 즉시 movement/lane을 pause하고 .claude/skills/qtg-drill-incident-log 관례에 따라 incident log를 남긴다. burn 파라미터의 recipient와 amount 인코딩을 감사한다. 자금은 이미 체인 위에 있으며 reservation은 보존된다.
12.2 OBSERVATION_TIMEOUT
MANUAL_INTERVENTION에서 Iris와 destination RPC 상태를 확인한다. 원인을 해소한 뒤 기존 MANUAL_INTERVENTION -> EXECUTING resume으로 재관찰하거나, 증거를 검토해 수동 완료 판정을 한다.
12.3 Post-completion reorg 의심
완료 후 reorg 감지는 런타임에 없으며 명시적 non-goal이다. 의심 시 즉시 pause하고 incident log를 남긴다. 1차 방어는confirmations_required다.
Related documents
- Executor Overview
- Observe Probes (ProtocolObserveExecutor, EvmChainFinalityProbe details)
- CEX Lane