Observe Executors & Probes
Source files
src/qtg/infrastructure/executors/observe/__init__.pysrc/qtg/infrastructure/executors/observe/registry.pysrc/qtg/infrastructure/executors/observe/common.pysrc/qtg/infrastructure/executors/observe/destination_chain_receive.pysrc/qtg/infrastructure/executors/observe/destination_chain_finality.pysrc/qtg/infrastructure/executors/observe/protocol.pysrc/qtg/infrastructure/executors/observe/evm.pysrc/qtg/infrastructure/executors/observe/cctp.pysrc/qtg/application/services/observe.pysrc/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.)
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 ininfrastructure/bootstrap.py:
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
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
- Check
match_modeis present (from node_config) - Extract targets (
resolve_destination_targets) - Required-field validation per match_mode:
txid-> txid requiredaddress-> address requiredaddress_memo-> address + memo required
- Look up the probe (
probe_key) + callvalidate_context(if present)
4.2 prepare
4.3 submit
ReturnsSUBMITTED immediately. Actual polling happens in observe.
4.4 observe
4.5 recover
Re-invokesobserve 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
- Check
txidis present (from provider_context or input_params) - Check
confirmations_requiredis present (from node_config) - Look up the probe +
validate_context
5.2 prepare
5.3 submit
ReturnsSUBMITTED immediately.
5.4 observe
5.5 recover
Re-invokesobserve directly.
6. ProtocolObserveExecutor
File:observe/protocol.py
A generic executor that checks protocol-level proofs (attestation, delivery proof, etc.).
6.1 preflight
- Check
protocol_refis present (from provider_context or input_params) - Look up the probe (
probe_key) +validate_context
6.2 prepare
6.3 submit
ReturnsSUBMITTED immediately.
6.4 observe
ProtocolObserveExecutor includes
generated_artifacts in the ExecutionResult. The observer worker persists them to the DB.
6.5 recover
Re-invokesobserve directly.
7. EVM Probes
File:observe/evm.py
7.1 EvmJsonRpcClient
The JSON-RPC client shared by every EVM probe:{"jsonrpc": "2.0", "id": 1, "method": ..., "params": ...} via POST endpoint.
Error classification:
Default retry wait:
DEFAULT_RETRY_AFTER_SECONDS = 15
7.2 EvmChainReceiveProbe
validate_context
Required validation:chain_id-> RPC endpoint presentmatch_mode="txid"onlytxidpresentdestination addresspresentamount_match(optional):"exact"or"at_least"allowedamount_floor_deduction_raw(optional): non-negative integer; positive values requireamount_match="at_least"- For ERC-20 tokens:
token_decimalsrequired
check_receive
-
Call
eth_getTransactionReceipt(txid)- receipt missing ->
matched=False, provider_state="pending" status == 0x0->FatalMovementError(RECEIPT_REVERTED)
- receipt missing ->
-
Transaction matching:
- Native transfer (
token_contractunset): checktoaddress +valueviaeth_getTransactionByHash - ERC-20 transfer (
token_contractset): match theTransfer(from, to, value)event from receipt logs- Transfer topic:
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef topics[2](to) = destination address checklog.data= transfer amount
- Transfer topic:
- Native transfer (
-
Amount validation (optional):
amount_match = "exact": observed == expectedamount_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:
amount_match: "at_least"; it cannot make an exact-match lane permissive.
node_config requirements
7.3 EvmChainFinalityProbe
validate_context
Required validation:chain_id-> RPC endpoint presenttxidpresentconfirmations_required> 0
check_finality
-
Call
eth_getTransactionReceipt(txid)- receipt missing ->
finalized=False, confirmations=0, provider_state="pending" status == 0x0->FatalMovementError(RECEIPT_REVERTED)
- receipt missing ->
-
Compute confirmations:
-
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
PreparedAction for observe. Canonical JSON serialization + SHA-256 hash.
payload_format = "provider_request"signing_required = Falseprepared_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
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
retry_after_seconds, that value is used; otherwise the per-action_type default applies.
11. DEFAULT_OBSERVE_INTERVALS
File:application/services/observe.py
12. Guide to adding a new Probe
12.1 Adding a ChainReceiveProbe — example
exec.observe.destination_chain_receive executor as-is and just change probe_key.
12.2 Adding a ProtocolProbe — example
Related documents
- 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