Skip to main content

CCTP Lane Executors

Source files
  • src/qtg/infrastructure/executors/cctp/__init__.py
  • src/qtg/infrastructure/executors/cctp/burn.py
  • `src/qtg/infrastructure/executors/cctp/mint.py>
  • src/qtg/infrastructure/executors/cctp/common.py
  • src/qtg/infrastructure/executors/cctp/api.py
  • src/qtg/infrastructure/executors/observe/cctp.py
  • src/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 the destination_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-closedFatalMovementError → 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:
  1. 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)
  2. ERC-20 Allowance check: eth_call of allowance(owner, spender)
  3. ERC-20 Balance check: eth_call of balanceOf(owner)
  4. CCTP fee check: look up fee_bps from Circle API and compute minimum_fee
  5. Fast Allowance check (when threshold == 1000): check Circle API fast burn allowance remaining
  6. Gas estimation: pre-validate that the calldata is executable via eth_estimateGas
Preflight error codes:

3.2 prepare

Builds the EVM transaction payload.
  1. ABI-encode depositForBurn calldata
  2. Look up nonce / gas / gasPrice
  3. Return PreparedAction
Since signing_required=True, the dispatcher invokes the signer to sign the payload before passing it to submit.

3.3 submit

Broadcasts the signed transaction via eth_sendRawTransaction.
Provider refs on success:
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 -> FatalMovementError propagated

3.4 observe

Simple pass-through:

3.5 recover

If txid or burn_tx_hash is present -> COMPLETED; otherwise FAILED(RECOVERY_FAILED).

4. CCTP Attestation (Node 2)

The attestation node is the combination of the generic ProtocolObserveExecutor 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 that source_domain_id is present in node_config and is an integer.

check_proof

  1. Call iris_client.get_messages(base_url, source_domain_id, transaction_hash=protocol_ref)
  2. If the response is None or messages is empty -> completed=False (pending)
  3. Select a message:
    • If message_index is 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
  4. If the attestation field is empty -> pending
  5. If attestation + message_bytes are both present -> completed=True
Return on completed:
The attestation is also stored as a GeneratedArtifact and persisted to the DB.

4.3 CctpIrisClient

File: observe/cctp.py Circle IRIS API v2 client:
HTTP status code handling: 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:
Error handling is similar to 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:
  1. Check that attestation/message_bytes are present: common.attestation(ctx), common.message_bytes(ctx) — extracted from provider_context
  2. Look up signer address: resolve_signer_address_from_binding(ctx.resolved_bindings["signer"]) — extracts the address from the signer binding metadata
  3. destination_caller alignment: validate_mint_destination_caller_alignment(ctx, signer_address) — verifies that the destination_caller set at burn matches the signer address that will execute the mint. On mismatch: FAILED(SIGNER_CALLER_MISMATCH) or FAILED(DESTINATION_CALLER_CONFIG_MISMATCH).
  4. eth_call dry-run: execute receiveMessage(message, attestation) calldata via eth_call to pre-check for revert + estimate gas
Security: destination_caller enforcement CCTP’s 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 the receiveMessage EVM transaction payload.

5.3 submit

Broadcasts the signed transaction via eth_sendRawTransaction. Provider refs on success:
Reason for storing the same tx_hash under three keys: the next node (finality observe) looks up 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:
  1. When tx_hash is present: check via eth_getTransactionReceipt
    • receipt missing -> UNKNOWN(MINT_STATUS_UNKNOWN)
    • receipt reverted -> FAILED(TX_REVERTED)
    • receipt success -> COMPLETED
  2. When tx_hash is absent: dry-run receiveMessage via eth_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)
This idempotency detection is the core CCTP safety mechanism. Once a message nonce has been used, the same message cannot be minted again, so the revert message can confirm that it has already completed. Error codes (mint):

6. Destination Chain Receipt (Node 4)

The combination of the generic DestinationChainReceiveObserveExecutor 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 generic DestinationChainFinalityObserveExecutor 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-decimals scales the burn amount and receive floor identically, so the receive guard cannot detect a decimals misconfiguration; verify decimals() 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다.