Skip to main content

Plan Compilation


What is a Plan?

Plan = a blueprint that defines “what to do and in what order” ahead of time. Think of it like an international travel itinerary:
  • Departure: Incheon Airport (= Upbit)
  • Layover: Singapore transit (= CCTP Attestation)
  • Destination: Sydney, Australia (= Base Chain)
  • Transport for each leg: Korean Air ICN–SIN, Qantas SIN–SYD (= CEX Executor, CCTP Executor)
There are two kinds of travel plans: A Template is a reusable blueprint. Once you create a plan template like “Send XRP from Upbit to Bithumb,” you can stamp out requests from that same template — whether for 25 XRP or 100 XRP.
Template vs Instance A Template contains who executes it — e.g., executor_selector: {"executor_key": "cex-upbit"}. An Instance contains what to send this time — e.g., intent: {"asset": "XRP", "amount": "25"}.

Why do we “compile” a Plan?

A Template is abstract. It contains variables and no concrete amounts or addresses. Compilation is the process of turning it into a concrete execution plan. Analogy: C source code → compiler → executable. Plan Template → Plan Compiler → executable Request. Four things happen sequentially during compilation:
  1. Graph validation — confirms the plan is in an executable form
  2. Binding resolution — determines which executor/signer to attach to each node
  3. Snapshot creation — merges all information into a single immutable document
  4. Hash computation — generates a fingerprint of the snapshot
Why does this order matter? If you execute with an invalid graph, funds get withdrawn but there’s no next node — everything stalls. If bindings aren’t resolved, the executor can’t be found — everything stalls. All problems must be caught at compile time.
The movement-create flow runs these four stages in order: it resolves bindings, computes the frontier (root) nodes, builds the snapshot, then computes the hash from that snapshot. Note that graph validation is already performed at template registration time — when a request is created, only the executable_in_v3_0 flag is checked.

Graph Structure

A Plan is a DAG

A Plan is a DAG (Directed Acyclic Graph) — a directed graph with no cycles.
  • Node = a unit of execution. Things like “withdraw XRP from Upbit,” “poll withdrawal status,” “confirm Bithumb deposit.”
  • Edge = a dependency. “Withdrawal must complete before withdrawal confirmation can start.”
What is has_side_effect? “Does executing this node cause something irreversible?” Submitting a withdrawal moves funds — that’s a side effect. Checking withdrawal status has no side effect (read-only). This flag is critical for determining recovery strategy on error.

Why must it be a DAG?

A cycle causes an infinite loop. “A must finish before B, and B must finish before A…” — you can never start. In a system handling real money, an infinite loop is catastrophic, so cycles are blocked at compile time.

Graph Validation

Validation runs in 6 steps. Failure at any step throws a GraphValidationError and the template registration is rejected outright.

A closer look at each step

1. Duplicate node_key check If two nodes share the same name, it becomes ambiguous which node to execute — duplicate node_keys are rejected. 2. Minimum node count check A plan with zero nodes does nothing. Meaningless, so it’s rejected. 3. Edge reference integrity If an edge says {from: "withdrawal", to: "deposit_check"} but no node named deposit_check exists, execution will fail with “can’t find next node.” 4. Self-loop prohibition {from: "A", to: "A"} — depending on yourself makes no sense. 5. DAG cycle detection (Kahn’s Algorithm) This is the most critical validation step. Kahn’s Algorithm performs a topological sort while simultaneously detecting cycles. Simply put: “Remove nodes one by one, starting with those that have no prerequisites. If all nodes can be removed, there are no cycles. If some nodes remain that can’t be removed, there’s a cycle.” Mechanically: start with every node whose indegree is 0, repeatedly remove a node and decrement its children’s indegree, enqueuing any child that reaches indegree 0. If every node gets processed this way the graph is a DAG; if some nodes can never be reached, a cycle exists and the template is rejected. The order in which nodes come off the queue is the topological sort result, which also becomes the basis for execution order. 6. completion_policy validation Verifies that the criteria for declaring a plan “complete” are correct. Each completion-assurance level requires a matching terminal-node family: Examples:
  • destination_credited: “complete when deposit is confirmed at the destination exchange” → the terminal node must be from the destination_provider_observe family
  • destination_finalized: “complete when N confirmations accumulate on the destination chain” → the terminal node must be destination_chain_finality_observe, and config.confirmations_required must be set
  • protocol_finalized: “complete when confirmed at the protocol level (e.g., CCTP attestation)” → the terminal node must be from the protocol_observe family

compiled_plan_hash

Why is a hash needed?

Analogy: detecting tampering after signing a contract. An operator approved a plan to “send 25 XRP from Upbit to Bithumb.” But what if someone swaps the plan to “2500 XRP” between approval and execution? compiled_plan_hash prevents this. Approval flow:
  1. compiled_plan_hash is computed at request creation → stored in DB
  2. Operator sends compiled_plan_hash along with the approval request
  3. Server compares it against the stored hash → mismatches are rejected
If the hash submitted with the approval does not match the one stored for the request, approval fails with a conflict error and nothing executes.

How is it computed?

Hash computation follows this procedure: Key points:
  • Sorting is mandatory: the same content in a different order produces a different hash. Nodes are sorted by node_key; edges by (from, to, edge_type).
  • Fixed separators: (",", ":") with no spaces — {"a":1} not {"a": 1}
  • ensure_ascii=False: Korean and other Unicode characters may appear — keep them as-is
  • Result format: "sha256:a1b2c3d4..." — the prefix explicitly identifies the algorithm
There are two distinct hashes
  • Plan graph hash — computed at template registration. Hashes the graph structure itself. Includes approval_policy.
  • Compiled plan hash — computed at request creation. Hashes the concrete execution plan. Includes input_params and resolved_bindings.
The former answers “is this the same template structure?”; the latter answers “is this the same execution plan?”

Compiled Snapshot

A snapshot is an immutable document. Once created it never changes, and it serves as the source of truth for the hash. The snapshot bundles everything needed to describe one concrete execution plan into a single object, with these fields:
  • schema_version — snapshot schema version (e.g. "v3.0")
  • template_key / template_version — which template and version this was compiled from
  • completion_policy — the criteria for declaring the plan complete
  • nodes / edges — the validated graph
  • input_params — the concrete values for this instance (asset, amount, addresses)
  • resolved_bindings — the executor/signer attached to each node
  • risk_controls — caps and guards
  • callback_config — where to send status callbacks
Example snapshot (CEX XRP transfer):

Binding Resolution

This is the process of deciding “which executor and signer to attach to each node.” In the current v3.0 it is straightforward — the executor_selector and signer_selector already specified on each node in the template are carried through as-is, producing a per-node map of {executor, signer}.
Why is this so simple? The current behavior starts with explicit assignment. In the future, capability-based dynamic resolution — e.g., executor_selector: {"capabilities": ["cex", "withdrawal"], "prefer": "lowest_fee"} — may be added.

Frontier Computation

Frontier = “the list of nodes that can be executed right now.” These are nodes that are not the successor of any other node — the root nodes. Simple logic: “nodes not referenced as the to of any edge = starting nodes.” In the CEX 3-node lane, only withdrawal is the frontier. Once approved, this node transitions from BLOCKEDREADY and execution begins.

GraphShape

Validation classifies the “shape” of the graph: Classification logic, in plain terms:
  • A node with more than one outgoing edge means the graph branchesBRANCHING if those edges carry conditions, otherwise SPLIT.
  • A node with more than one incoming edge means the graph merges (MERGING).
  • Both branching and merging → HYBRID.
  • Neither → LINEAR.
v3.0 only executes LINEAR The data model accepts DAGs, but the runtime only supports sequential execution. You can register non-LINEAR templates (for future use), but attempting to create a request from a template that is not marked executable_in_v3_0 is rejected. non_executable_reasons records the specific reason (e.g., "graph shape 'branching' requires graph runtime").

End-to-end compilation flow summary

Key takeaways
  1. Plan = a DAG-based execution blueprint (nodes = steps, edges = dependencies)
  2. Compilation = Template + Params → executable Request (validation + binding + snapshot + hash)
  3. compiled_plan_hash = a signed seal on the contract (detects tampering after approval)
  4. v3.0 executes LINEAR graphs only (the model for future DAG parallel execution is already in place)