Plan Compilation
Source of truth: src/qtg/compiler/
This document describes the compilation pipeline that converts a movement plan template into an executable request.
It covers graph validation, topological ordering, binding resolution, snapshot creation, and deterministic hashing.
Table of Contents
- Overview: what is Plan Compilation
- Compilation pipeline
- Graph validation (
validate.py) - Binding resolution (
resolve.py) - Compiled Plan Snapshot (
snapshot.py) - Deterministic Hash (
hash.py) - Frontier node computation
- Valid graphs vs invalid graphs
- Completion Policy validation
- v3.0 executability decision
Overview: what is Plan Compilation
Plan Compilation is the process of combining the abstract node/edge graph defined by a template with theinput_params provided by the user, producing an immutable snapshot in which all information required for approval and execution has been finalized.
A compiled plan guarantees the following:
- Graph integrity: DAG (directed acyclic graph) property, no duplicate nodes
- Bindings finalized: the executor/signer to use for each node is decided
- Hash fixed: the plan at approval time and the plan at execution time can be verified as identical
- Topological order: the execution order of nodes is deterministic
Compilation pipeline
Public API
Source: src/qtg/compiler/__init__.py
Step-by-step call order
Graph validation (validate.py)
Source: src/qtg/compiler/validate.py
validate_template_graph()
nodes— the list of nodes defined by the template. Each node must contain at leastnode_key,node_kind,action_type,executor_selector, andhas_side_effect.edges— connections between nodes. Each edge containsfrom,to, andedge_type, optionally withcondition_expr.completion_policy— completion policy (optional). Thecompletion_assurancefield is required.
GraphValidationResult dataclass.
Validation rules
Graph validation is performed in the following order.1. Duplicate node_key check
node_key, validation fails immediately.
1-A. Dots prohibited in node_key
$ref:node_key.field syntax splits node and field on the first ., a node_key cannot contain ..
2. Minimum node count check
3. Edge reference integrity
from/to references a non-existent node.
4. Self-loop prohibition
5. DAG validation (cycle detection)
Topological ordering is performed using Kahn’s algorithm. If the resulting ordering contains fewer nodes than the total node count, a cycle exists.sorted(), and outbound edges are also ordered by (to, edge_type). This ensures the same topological order is produced for the same input.
6. Config $ref validation
Immediately after topological order generation, each node’s top-level config string values are scanned for $ref:node_key.field.
- A malformed expression raises
GraphValidationError. - A reference to a node missing from the template raises
GraphValidationError. - A reference to a node that is not a DAG ancestor of the current node raises
GraphValidationError.
provider_refs, but those keys are not included in the provider_context that the executor sees.
7. Completion Policy validation
Whencompletion_policy is provided, additional validation is performed (see the Completion Policy validation section below).
Graph Shape classification
After validation passes, the graph shape is classified:Topological ordering
Thetopological_order produced by Kahn’s algorithm provides a deterministic order for node execution:
- Predecessor nodes come first
- Nodes at the same level are ordered alphabetically by
node_key - Edges are also ordered by
(to, edge_type)
GraphValidationResult.topological_order as a list[str] (list of node keys).
Binding resolution (application.services.binding)
Source: src/qtg/application/services/binding.py
Current behavior
At movement creation time,executor_selector / signer_selector are resolved to concrete bindings against the DB registry.
Executor selector contract:
action_typerequiredprovideroptionalchain_familyoptional
chain_familyrequiredprofileoptional
- active registry rows are candidates
- every non-null selector field must match
- 0 matches ->
BindingResolutionError - 2 or more matches ->
AmbiguousBindingError
node_key -> {"executor": concrete binding, "signer": concrete binding | None}
Example:
Compiled Plan Snapshot (snapshot.py)
Source: src/qtg/compiler/snapshot.py
Snapshot structure
Full schema of the returned dict:Field descriptions
Immutability guarantee
After creation, this snapshot is not modified. It is persisted to the DB atMovementRequest creation and is referenced throughout approval and execution. The compiled_plan_hash can be used to verify whether it has been tampered with.
Deterministic Hash (hash.py)
Source: src/qtg/compiler/hash.py
Two hash functions exist; they serve different purposes.
compute_plan_graph_hash()
Purpose: a template-level graph structure hash. Identical template structures produce identical hashes.
Characteristics:
input_params and resolved_bindings are not included. The same template structure produces the same graph hash regardless of input values.
compute_compiled_plan_hash()
Purpose: full hash of a compiled plan for a specific request. Core mechanism for approval integrity verification.
Differences vs
compute_plan_graph_hash:
input_paramsandresolved_bindingsare included — request-specific- Edge
condition_expris excluded — runtime conditions are already finalized in the compiled plan approval_policyis excluded — the approval hash is managed externally
Hash computation method
Both functions follow the same pattern:- Node ordering: alphabetical by
node_key - Edge ordering: composite key
(from, to, edge_type) - JSON normalization:
sort_keys=True— dict keys ordered - Fixed separators:
separators=(",", ":")— compact JSON with no whitespace - Unicode preserved:
ensure_ascii=False— non-ASCII characters such as Hangul are preserved - UTF-8 encoding: hashing is done at the byte level
"sha256:" prefix + 64-character hex digest
Example: "sha256:a1b2c3d4e5f6..."
Why the hash matters
- Approval integrity: verifies that the plan approved by the approver is the same plan being executed
- Tamper detection: detects whether the compiled snapshot has been altered in the DB
- Audit trail: tracks which plan construction was executed via the hash
- ExecutionContext propagation:
compiled_plan_hashis propagated to every executor, allowing the executor to verify it is executing as part of the correct plan
Frontier node computation
Source: src/qtg/compiler/validate.py
READY state.
Logic: collect every node that appears as an edge’s to target; nodes not in that set are frontier (root) nodes.
Example:
advance_after_completion().
Valid graphs vs invalid graphs
Valid graph examples
1. Linear (executable on v3.0)
- shape:
LINEAR executable_in_v3_0:Truetopological_order:["withdrawal_action", "withdrawal_observe", "deposit_observe"]
2. Split (parallel, not executable on v3.0)
- shape:
SPLIT executable_in_v3_0:False- reason:
"graph shape 'split' requires graph runtime"
3. Diamond / Hybrid (not executable on v3.0)
- shape:
HYBRID executable_in_v3_0:False
Invalid graph examples
1. Cycle
- Error:
GraphValidationError("template graph must be a DAG")
2. Duplicate node_key
- Error:
GraphValidationError("duplicate node_key detected")
3. Reference to a non-existent node
- Error:
GraphValidationError("edge references unknown to node: nonexistent")
4. Self-loop
- Error:
GraphValidationError("self-loop edges are not allowed")
5. Empty graph
- Error:
GraphValidationError("template must define at least one node")
Completion Policy validation
Whencompletion_policy is provided, _validate_completion_policy() performs additional validation.
CompletionAssurance mapping
Depending on theCompletionAssurance value, a terminal node must belong to a specific “family”:
Node family classification
The mapping table that determines the node family fromaction_type:
Validation rules
- The
completion_assurancefield must be present. - It must be a valid
CompletionAssuranceenum value. - At least one terminal node (a node with no outbound edges) must belong to the required family.
- Family-specific additional configuration validation:
destination_provider_observe/destination_chain_receive_observe:config.match_moderequireddestination_chain_finality_observe:config.confirmations_required> 0 required
Validation failure examples
v3.0 executability decision
Theexecutable_in_v3_0 field on GraphValidationResult indicates whether the current v3.0 runtime can execute this graph.
Conditions that block execution
Both conditions are appended to
non_executable_reasons.
Summary of executable conditions
To execute on v3.0:- The graph is LINEAR (single path, no branch/merge)
- Every edge’s
condition_exprisNone
Cross-References
- Executor/Signer protocols: domain/executor-signer-protocols.md
- State transitions: domain/states-and-transitions.md
- Types and enumerations: domain/types-and-enums.md
- CEX executor lane: executors/cex-lane.md
- CCTP executor lane: executors/cctp-lane.md
- DB model: infrastructure/data-model.md