Skip to main content

Observe Executors & Probes

Source files
  • src/qtg/infrastructure/executors/observe/__init__.py
  • src/qtg/infrastructure/executors/observe/registry.py
  • src/qtg/infrastructure/executors/observe/common.py
  • src/qtg/infrastructure/executors/observe/destination_chain_receive.py
  • src/qtg/infrastructure/executors/observe/destination_chain_finality.py
  • src/qtg/infrastructure/executors/observe/protocol.py
  • src/qtg/infrastructure/executors/observe/evm.py
  • src/qtg/infrastructure/executors/observe/cctp.py
  • src/qtg/application/services/observe.py
  • src/qtg/infrastructure/bootstrap.py

1. Architecture overview

The Observe system has a two-layer executor + probe structure.
  • Observe Executor: generic lifecycle management (preflight, prepare, submit, observe, recover)
  • Probe: the actual external system polling logic (chain RPC, protocol API, etc.)
Because the executor looks up the probe at runtime via node_config.probe_key, the same executor can handle a wide range of chains/protocols.

Three Observe Executor families

Registered Executor Keys

EvmBalanceObserveExecutor / EvmFinalityObserveExecutor are registered unconditionally by register_builtin_observe_executors() (alongside the destination-chain and protocol executors). They are EVM-specific observe lanes that read balances / tx finality directly via the runtime RPC client rather than going through the generic chain-probe registry.

2. Probe Registry

File: observe/registry.py Manages three independent registries, each shaped as dict[str, Probe].

Registration functions

Lookup functions

Initialization functions

Bootstrap registration (current)

Probes registered in infrastructure/bootstrap.py:
The same EvmChainReceiveProbe / EvmChainFinalityProbe instance is registered under both the "evm" key and a *.default alias, so a node_config.probe_key of either "evm" or "receive.default" / "finality.default" resolves to the same probe. Currently registered probes:

3. Probe Protocol Definitions

File: domain/protocols.py

3.1 ChainReceiveProbe

3.2 ChainFinalityProbe

3.3 ProtocolProbe

A ProtocolProbe can return generated_artifacts. Used for data that must be persisted, such as attestations.

4. DestinationChainReceiveObserveExecutor

File: observe/destination_chain_receive.py The executor that confirms asset receipt on the destination chain.

4.1 preflight

  1. Check match_mode is present (from node_config)
  2. Extract targets (resolve_destination_targets)
  3. Required-field validation per match_mode:
    • txid -> txid required
    • address -> address required
    • address_memo -> address + memo required
  4. Look up the probe (probe_key) + call validate_context (if present)
Error codes:

4.2 prepare

4.3 submit

Returns SUBMITTED immediately. Actual polling happens in observe.

4.4 observe

4.5 recover

Re-invokes observe directly.

5. DestinationChainFinalityObserveExecutor

File: observe/destination_chain_finality.py The executor that checks whether a transaction’s block has reached the confirmation threshold.

5.1 preflight

  1. Check txid is present (from provider_context or input_params)
  2. Check confirmations_required is present (from node_config)
  3. Look up the probe + validate_context
Error codes:

5.2 prepare

5.3 submit

Returns SUBMITTED immediately.

5.4 observe

5.5 recover

Re-invokes observe directly.

6. ProtocolObserveExecutor

File: observe/protocol.py A generic executor that checks protocol-level proofs (attestation, delivery proof, etc.).

6.1 preflight

  1. Check protocol_ref is present (from provider_context or input_params)
  2. Look up the probe (probe_key) + validate_context
Error codes:

6.2 prepare

6.3 submit

Returns SUBMITTED immediately.

6.4 observe

ProtocolObserveExecutor includes generated_artifacts in the ExecutionResult. The observer worker persists them to the DB.

6.5 recover

Re-invokes observe directly.

7. EVM Probes

File: observe/evm.py

7.1 EvmJsonRpcClient

The JSON-RPC client shared by every EVM probe:
JSON-RPC 2.0 protocol. Sends {"jsonrpc": "2.0", "id": 1, "method": ..., "params": ...} via POST endpoint. Error classification: Default retry wait: DEFAULT_RETRY_AFTER_SECONDS = 15

7.2 EvmChainReceiveProbe

The probe that confirms transaction receipt on an EVM chain.

validate_context

Required validation:
  • chain_id -> RPC endpoint present
  • match_mode = "txid" only
  • txid present
  • destination address present
  • amount_match (optional): "exact" or "at_least" allowed
  • amount_floor_deduction_raw (optional): non-negative integer; positive values require amount_match="at_least"
  • For ERC-20 tokens: token_decimals required

check_receive

  1. Call eth_getTransactionReceipt(txid)
    • receipt missing -> matched=False, provider_state="pending"
    • status == 0x0 -> FatalMovementError(RECEIPT_REVERTED)
  2. Transaction matching:
    • Native transfer (token_contract unset): check to address + value via eth_getTransactionByHash
    • ERC-20 transfer (token_contract set): match the Transfer(from, to, value) event from receipt logs
      • Transfer topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
      • topics[2] (to) = destination address check
      • log.data = transfer amount
  3. Amount validation (optional):
    • amount_match = "exact": observed == expected
    • amount_match = "at_least": observed >= expected

amount_floor_deduction_raw

amount_floor_deduction_raw is an at-least-only raw-unit floor deduction. The expected amount is first scaled from the movement amount, then the probe requires:
This is used when a protocol can deduct a bounded fee before destination receipt. The key is valid only with amount_match: "at_least"; it cannot make an exact-match lane permissive.

node_config requirements

7.3 EvmChainFinalityProbe

The probe that checks the number of block confirmations on an EVM chain.

validate_context

Required validation:
  • chain_id -> RPC endpoint present
  • txid present
  • confirmations_required > 0

check_finality

  1. Call eth_getTransactionReceipt(txid)
    • receipt missing -> finalized=False, confirmations=0, provider_state="pending"
    • status == 0x0 -> FatalMovementError(RECEIPT_REVERTED)
  2. Compute confirmations:
  3. Return:

node_config requirements


8. CCTP Attestation Probe

File: observe/cctp.py The dedicated probe for CCTP attestation. For details, see cctp-lane.md.

node_config requirements


9. Common Helpers

File: observe/common.py

9.1 build_observe_action

Creates a PreparedAction for observe. Canonical JSON serialization + SHA-256 hash.
  • payload_format = "provider_request"
  • signing_required = False
  • prepared_action_id = "{action_type}:{sha256_prefix_12}"

9.2 probe_key

9.3 match_mode

9.4 confirmations_required

9.5 proof_mode

9.6 resolve_destination_targets

Extracts target information per match_mode:

9.7 resolve_txid / resolve_protocol_ref

9.8 Error result helpers

temporary_result is used to keep the observe state while scheduling a retry on transient errors.

10. Observe results and NodeState transitions

How the observer worker (application/services/observe.py) converts an executor’s observe() result into a NodeState:

next_observe_at computation

If the executor returns retry_after_seconds, that value is used; otherwise the per-action_type default applies.

11. DEFAULT_OBSERVE_INTERVALS

File: application/services/observe.py
Unmatched action_types default to 10 seconds.

12. Guide to adding a new Probe

12.1 Adding a ChainReceiveProbe — example

Registration:
node_config:
Use the existing exec.observe.destination_chain_receive executor as-is and just change probe_key.

12.2 Adding a ProtocolProbe — example

Registration:

  • Executor Overview — executor architecture, registry, local/remote distinction
  • CEX Lane — CEX executor details (does not use observe executors)
  • CCTP Lane — how CCTP uses observe probes