> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# State Machine

> QTG's two-level state model — RequestState and NodeState — including transition rules, derivation logic, and the side-effect safety guarantee.

# State Machine

***

## Why do we need a state machine?

### Analogy: package delivery tracking

When you ship a package, it goes through states like this:

```
Intake → Picked up → In transit → Out for delivery → Delivered
```

Without this state tracking, you are left wondering "where did my package go?" Packages are not money, so in the worst case you can just resend — but **crypto transfers mean if you lose track in the middle, the money is gone.**

Cross-exchange transfers also go through multiple stages:

```
Withdrawal requested → Withdrawal processing → Withdrawal complete → Awaiting deposit → Deposit confirmed → Done
```

What a state machine does:

1. **Precisely tracks "how far along we are"**
2. **Blocks out-of-order progression at the code level**
3. **Determines the recovery path by knowing the exact state when something goes wrong**

<Warning>
  **Why a "strict" state machine?** Being free to change state arbitrarily would be convenient, but in a system handling real money, **only permitted transitions should be possible**. For example, going from `COMPLETED` back to `EXECUTING` makes no sense — re-executing a completed transfer would mean a double-send.
</Warning>

***

## QTG v3's two-level state model

QTG v3 has **two levels of state**:

| Level | Name           | Analogy                                    | Role                                                                     |
| ----- | -------------- | ------------------------------------------ | ------------------------------------------------------------------------ |
| Upper | `RequestState` | Overall package delivery status            | Progress of the entire transfer request                                  |
| Lower | `NodeState`    | Processing status at each intermediate hub | Status of each individual execution step (withdrawal, observation, etc.) |

This two-level structure is the core of v3. v2 had only Request-level state, but in v3 a transfer route is a **graph composed of multiple nodes (steps)**, so each node needs its own independent state.

<Tip>
  **Request state is not set directly — it is "derived" by aggregating all Node states.** The request state is always a function of the current node states, never written ad hoc.
</Tip>

***

## Request state (RequestState)

### Full transition graph

```mermaid theme={null}
stateDiagram-v2
    [*] --> RECEIVED
    RECEIVED --> VALIDATED : input validation passed
    RECEIVED --> FAILED : input validation failed

    VALIDATED --> PENDING_APPROVAL : enter pending approval

    PENDING_APPROVAL --> APPROVED : operator approved
    PENDING_APPROVAL --> REJECTED : operator rejected
    PENDING_APPROVAL --> EXPIRED : approval timeout

    APPROVED --> EXECUTING : execution started
    APPROVED --> FAILED : pre-execution error

    EXECUTING --> COMPLETED : all nodes completed
    EXECUTING --> FAILED : node failed (no side-effect)
    EXECUTING --> MANUAL_INTERVENTION : node failed (has side-effect!)
    EXECUTING --> WAITING_MANUAL_ACTION : manual_gate node waiting
    EXECUTING --> CANCELLED : operator cancelled

    WAITING_MANUAL_ACTION --> EXECUTING : manual action completed
    WAITING_MANUAL_ACTION --> CANCELLED : operator cancelled

    MANUAL_INTERVENTION --> EXECUTING : operator recovered and resumed
    MANUAL_INTERVENTION --> FAILED : operator confirmed failure
    MANUAL_INTERVENTION --> COMPLETED : operator confirmed completion

    COMPLETED --> [*]
    FAILED --> [*]
    REJECTED --> [*]
    EXPIRED --> [*]
    CANCELLED --> [*]
```

### State descriptions

| State                   | Meaning                                         | Analogy                                                      |
| ----------------------- | ----------------------------------------------- | ------------------------------------------------------------ |
| `RECEIVED`              | Transfer request received                       | Package picked up                                            |
| `VALIDATED`             | Input validation complete                       | Address and weight verified                                  |
| `PENDING_APPROVAL`      | Awaiting operator approval                      | Expensive shipment requires manager sign-off                 |
| `APPROVED`              | Approved, awaiting execution                    | Sign-off complete                                            |
| `EXECUTING`             | Running (nodes are executing)                   | In transit                                                   |
| `WAITING_MANUAL_ACTION` | Waiting for human input at a `manual_gate` node | Waiting for "leave at door?" confirmation                    |
| `COMPLETED`             | All nodes complete                              | Delivered                                                    |
| `FAILED`                | Failed (safely, without side effects)           | Delivery failed — package is still in the warehouse          |
| `MANUAL_INTERVENTION`   | Failed but side effects occurred                | Package is stuck somewhere — someone needs to go retrieve it |
| `REJECTED`              | Operator rejected approval                      | Sign-off denied                                              |
| `EXPIRED`               | Approval timed out                              | Sign-off deadline passed                                     |
| `CANCELLED`             | Operator cancelled during execution             | In-transit cancellation                                      |

### Terminal state vs recoverable state

```mermaid theme={null}
flowchart LR
    subgraph Terminal["Terminal States (Terminal. No further transitions.)"]
        COMPLETED
        FAILED
        REJECTED
        EXPIRED
        CANCELLED
    end

    subgraph Recoverable["Recoverable (Recoverable. Requires human intervention.)"]
        MANUAL_INTERVENTION
    end

    MANUAL_INTERVENTION -->|operator action| EXECUTING
    MANUAL_INTERVENTION -->|deemed unrecoverable| FAILED
    MANUAL_INTERVENTION -->|manual completion confirmed| COMPLETED
```

Terminal states have **no allowed outgoing transitions** — once a request reaches `COMPLETED`, `FAILED`, `REJECTED`, `EXPIRED`, or `CANCELLED`, it cannot move anywhere else.

<Warning>
  **Why MANUAL\_INTERVENTION exists:** Consider a CCTP scenario. You burned USDC on Ethereum (= the money was burned), but the mint on Arbitrum failed. **The money is already burned — you can't simply end it as FAILED.** A human must intervene to complete the mint manually or contact Circle.

  The same is true in a CEX scenario. Upbit completed the withdrawal (= the money already left), but the deposit on Bithumb is not confirmed. Automatically marking it FAILED is wrong — the money may still be floating on the blockchain.
</Warning>

***

## Node state (NodeState)

### Full transition graph

```mermaid theme={null}
stateDiagram-v2
    [*] --> BLOCKED

    BLOCKED --> READY : predecessor completed
    BLOCKED --> SKIPPED : conditional skip

    READY --> PREPARING : preparation started
    READY --> OBSERVING : observe-kind node
    READY --> SKIPPED : conditional skip
    READY --> FAILED : dispatch-time fatal (allowlist, context build)

    PREPARING --> AWAITING_SIGNATURE : signature needed (on-chain tx)
    PREPARING --> SUBMITTING : no signature needed (CEX API)
    PREPARING --> READY : retry
    PREPARING --> FAILED : preparation failed

    AWAITING_SIGNATURE --> SUBMITTING : signature completed
    AWAITING_SIGNATURE --> READY : retry
    AWAITING_SIGNATURE --> FAILED : signature failed/rejected

    SUBMITTING --> SUBMITTED : submission succeeded
    SUBMITTING --> COMPLETED : sync-terminal submit (no separate observe)
    SUBMITTING --> FAILED : submission failed (certain)
    SUBMITTING --> UNKNOWN : submission result uncertain!

    SUBMITTED --> OBSERVING : observation started
    SUBMITTED --> COMPLETED : immediately completed
    SUBMITTED --> FAILED : post-submit failure confirmed
    SUBMITTED --> UNKNOWN : observation became uncertain!

    OBSERVING --> COMPLETED : observation confirmed success
    OBSERVING --> FAILED : observation confirmed failure
    OBSERVING --> UNKNOWN : state uncertain during observation!

    UNKNOWN --> SUBMITTED : recovered via reconciliation
    UNKNOWN --> COMPLETED : reconciliation confirmed success
    UNKNOWN --> FAILED : reconciliation confirmed failure

    COMPLETED --> [*]
    FAILED --> [*]
    CANCELLED --> [*]
    SKIPPED --> [*]
```

<Note>
  **Both routes into `UNKNOWN` are deliberate.** A side effect that becomes ambiguous *after* submission — `observe()` raises, or returns `UNKNOWN` — must be able to enter recovery without passing through `OBSERVING` first. The observer picks up `SUBMITTED` rows directly and does not pre-flip them, so `SUBMITTED → UNKNOWN` is a real lifecycle edge, symmetric with `SUBMITTING → UNKNOWN`. Either way the recovery worker re-reconciles; nothing with money in flight is auto-terminalized.
</Note>

### State descriptions

| State                | Meaning                                          | When it occurs                        |
| -------------------- | ------------------------------------------------ | ------------------------------------- |
| `BLOCKED`            | Predecessor node not yet complete                | Initial state at creation             |
| `READY`              | Ready to execute (predecessor complete)          | Awakened when a predecessor completes |
| `PREPARING`          | Preparing for execution (preflight checks, etc.) | While calling executor's `prepare()`  |
| `AWAITING_SIGNATURE` | Waiting for signature (on-chain tx only)         | Waiting for external signer response  |
| `SUBMITTING`         | Actually submitting                              | While calling executor's `submit()`   |
| `SUBMITTED`          | Submitted, awaiting result                       | API response received                 |
| `OBSERVING`          | Observing result                                 | While calling executor's `observe()`  |
| `COMPLETED`          | Successfully completed                           | Final confirmation                    |
| `FAILED`             | Failed                                           | Error occurred                        |
| `UNKNOWN`            | Result uncertain (most dangerous)                | Network timeout, etc.                 |
| `CANCELLED`          | Cancelled                                        | Operator cancellation                 |
| `SKIPPED`            | Conditionally skipped                            | Determined execution is not needed    |

### How state progression differs between Action Node and Observe Node

State progression differs depending on the node type.

```mermaid theme={null}
flowchart TD
    subgraph ActionNode["Action Node (withdrawal request, burn tx, etc.)"]
        direction LR
        A1[BLOCKED] --> A2[READY]
        A2 --> A3[PREPARING]
        A3 --> A4[AWAITING_SIGNATURE]
        A4 --> A5[SUBMITTING]
        A5 --> A6[SUBMITTED]
        A6 --> A7[OBSERVING]
        A7 --> A8[COMPLETED]
    end

    subgraph ObserveNode["Observe Node (deposit confirm, attestation wait, etc.)"]
        direction LR
        O1[BLOCKED] --> O2[READY]
        O2 --> O3[PREPARING]
        O3 --> O5[SUBMITTING]
        O5 --> O6[SUBMITTED]
        O6 --> O7[OBSERVING]
        O7 --> O8[COMPLETED]
    end

    style ActionNode fill:#fff3e0
    style ObserveNode fill:#e3f2fd
```

**The difference:**

* **Action Node** can pass through the `AWAITING_SIGNATURE` step (on-chain transactions require signing)
* **Observe Node** typically loops through `PREPARING` → `SUBMITTING` → `OBSERVING`
* **Key point**: Action Nodes have `has_side_effect = true`

### Why UNKNOWN state is special

<Warning>
  **UNKNOWN = "We don't know whether the money was sent or not"** — this is the most dangerous node state.

  Example scenario:

  1. Called the Upbit withdrawal API
  2. Got no response due to a network timeout
  3. The withdrawal may or may not have actually been submitted

  `UNKNOWN` is not terminal — **it must be resolved to `SUBMITTED` or `FAILED` through reconciliation**.
</Warning>

Where `UNKNOWN` can go (only via reconciliation):

| Target      | Reconciliation found              |
| ----------- | --------------------------------- |
| `SUBMITTED` | "it went through, still settling" |
| `COMPLETED` | "it actually finished"            |
| `FAILED`    | "it didn't go through"            |

### Why has\_side\_effect matters

`has_side_effect` is declared per node in the template definition. For example, a CEX template's nodes carry the flag like this:

```text theme={null}
withdraw         has_side_effect: true    # withdrawal: money leaves!
withdraw_observe has_side_effect: false   # observe: read-only
deposit_observe  has_side_effect: false   # observe: read-only
```

The compiler propagates this flag onto each compiled node, so it travels with the plan.

This flag plays a **decisive role** when the request state is derived:

```mermaid theme={null}
flowchart TD
    A{any node FAILED or UNKNOWN?}
    A -->|Yes| B{any COMPLETED node with side-effect?}
    B -->|Yes| C[MANUAL_INTERVENTION<br/>requires human intervention]
    B -->|No| D{any FAILED node?}
    D -->|Yes| E[FAILED<br/>safe failure]
    A -->|No| F{all nodes COMPLETED/SKIPPED?}
    F -->|Yes| G[COMPLETED]
    F -->|No| H[None = still in progress]

    style C fill:#ff6b6b,color:#fff
    style E fill:#ffa07a
    style G fill:#90ee90
```

<Warning>
  **Why this distinction matters:**

  * `withdraw` node `COMPLETED` (money left) + `deposit_observe` node `FAILED` → money already left! → `MANUAL_INTERVENTION`
  * `withdraw` node `FAILED` (money never left) → safely `FAILED`

  **"Failed after money was already sent" and "failed before money was sent" are completely different situations.**
</Warning>

***

## State derivation logic

Request state is "calculated" by aggregating all node states — it is always a pure function of the current node states.

### Derivation priority

```mermaid theme={null}
flowchart TD
    Start["derive request state<br/>(from all node states)"] --> Check1

    Check1{"all nodes<br/>COMPLETED or SKIPPED?"}
    Check1 -->|Yes| R_COMPLETED["return COMPLETED"]

    Check1 -->|No| Check2
    Check2{"any FAILED or<br/>UNKNOWN node?"}
    Check2 -->|Yes| Check3
    Check2 -->|No| Check4

    Check3{"any COMPLETED node<br/>with side-effect?"}
    Check3 -->|Yes| R_MANUAL["return MANUAL_INTERVENTION"]
    Check3 -->|No| Check3b{"any FAILED node?"}
    Check3b -->|Yes| R_FAILED["return FAILED"]
    Check3b -->|No| R_NONE1["return None<br/>(UNKNOWN only, no side-effect)"]

    Check4{"manual_ prefix READY node exists<br/>and no in-progress nodes?"}
    Check4 -->|Yes| R_WAITING["return WAITING_MANUAL_ACTION"]
    Check4 -->|No| R_NONE2["return None<br/>(still in progress)"]

    style R_COMPLETED fill:#90ee90
    style R_MANUAL fill:#ff6b6b,color:#fff
    style R_FAILED fill:#ffa07a
    style R_WAITING fill:#ffe0b2
    style R_NONE1 fill:#e0e0e0
    style R_NONE2 fill:#e0e0e0
```

Priority order:

1. **Everything complete** → `COMPLETED`
2. **Failed/UNKNOWN + completed node with side effect** → `MANUAL_INTERVENTION` (most dangerous)
3. **Failed only** → `FAILED` (safe failure)
4. **Waiting at manual gate** → `WAITING_MANUAL_ACTION`
5. **Everything else** → `None` (still in progress, no change to Request state)

<Tip>
  `None` means "it is not yet time to change the Request state." The caller does nothing when it receives `None`. The Request stays in `EXECUTING` state.
</Tip>

***

## Waking the next node

When a node reaches `COMPLETED`, what should happen next?

```mermaid theme={null}
sequenceDiagram
    participant Worker as Executor Worker
    participant Advance as Advancement logic
    participant DB as Database

    Note over Worker: withdraw node COMPLETED
    Worker->>Advance: advance after completion
    Advance->>DB: find successors of withdraw
    DB-->>Advance: ["withdraw_observe"]

    alt successor nodes exist
        Advance->>DB: withdraw_observe: BLOCKED → READY
        Advance->>DB: current_frontier = ["withdraw_observe"]
        Advance->>DB: callback: frontier_advanced
    else no successor (last node)
        Advance->>Advance: derive request state
        Note over Advance: → derive → COMPLETED
    end
```

**Execution order:**

1. Find the **successor nodes** of the completed node
2. If a successor exists:
   * Transition `BLOCKED` → `READY` (wake it up)
   * Update `current_frontier`
   * Send `frontier_advanced` callback
3. If no successor exists (last node):
   * Derive the Request state from all node states

<Tip>
  **What is current\_frontier?** It is "the list of currently executable nodes." In the package delivery analogy, it is "the next hub this package needs to reach." This information is sent as a callback to external systems (Dashboard, etc.) to show real-time progress.
</Tip>

***

## Post-transition handling

Deriving the request state is responsible not only for the state transition but also for **follow-up processing**:

```mermaid theme={null}
flowchart TD
    Derive["derive request state"] --> Check{derived state?}

    Check -->|COMPLETED| C1["reservation → consumed<br/>frontier → empty array<br/>callback: request_completed"]
    Check -->|"FAILED/CANCELLED<br/>no side-effect"| C2["reservation → released<br/>callback: request_failed"]
    Check -->|"MANUAL_INTERVENTION<br/>or WAITING_MANUAL_ACTION"| C3["callback: request_attention"]
    Check -->|None| C4["do nothing"]

    style C1 fill:#90ee90
    style C2 fill:#ffa07a
    style C3 fill:#ff6b6b,color:#fff
    style C4 fill:#e0e0e0
```

| Derived state           | Reservation handling     | Callback            |
| ----------------------- | ------------------------ | ------------------- |
| `COMPLETED`             | `consumed` (used up)     | `request_completed` |
| `FAILED` (safe)         | `released` (returned)    | `request_failed`    |
| `MANUAL_INTERVENTION`   | Held (not yet finalized) | `request_attention` |
| `WAITING_MANUAL_ACTION` | Held                     | `request_attention` |

<Info>
  **What is a Reservation?** A "limit reservation" held when a transfer request is made in the hardcap reserve system. Consumed (`consumed`) on completion, returned (`released`) on safe failure, held until finalized when there are side effects.
</Info>

***

## Core safety mechanisms

### Transition rule validation

Every transition is checked against an allowed-transitions set before it is applied — both for request state and for node state. A transition is only permitted if it is explicitly listed as a legal edge from the current state.

<Warning>
  **Attempting an unauthorized state transition:** Any transition not in the allowed set is rejected. For example:

  * `COMPLETED → EXECUTING` : not allowed (something already finished cannot restart)
  * `FAILED → READY` : not allowed (a failed node cannot be directly awakened)
  * `OBSERVING → PREPARING` : not allowed (going backwards is not allowed)

  Violating these rules raises an illegal-state-transition error at the upper layer rather than silently corrupting state.
</Warning>

### Event audit trail

Every state transition is recorded as a movement event capturing the old state, the new state, and a detail payload. Who (actor type and id), when, and which state transitioned to which are all recorded. When something goes wrong, this event log lets you trace "what exactly happened."

***

## Summary: the power of the two-level state model

```mermaid theme={null}
flowchart TB
    subgraph Request["Request (full transfer request)"]
        RS["RequestState: EXECUTING"]
    end

    subgraph Nodes["Nodes (individual execution steps)"]
        N1["withdraw<br/>COMPLETED ✅<br/>has_side_effect: true"]
        N2["withdraw_observe<br/>COMPLETED ✅"]
        N3["deposit_observe<br/>OBSERVING 🔄"]
    end

    N1 & N2 & N3 -->|derive request state| RS

    style RS fill:#fff3e0
    style N1 fill:#90ee90
    style N2 fill:#90ee90
    style N3 fill:#bbdefb
```

* Node state fine-grained tracks the **actual progress** of each step
* Request state is derived by aggregating Node states into the **overall transfer status**
* The `has_side_effect` flag is the key that distinguishes **safe failure vs dangerous failure**
