State Machine
Why do we need a state machine?
Analogy: package delivery tracking
When you ship a package, it goes through states like this:- Precisely tracks “how far along we are”
- Blocks out-of-order progression at the code level
- Determines the recovery path by knowing the exact state when something goes wrong
QTG v3’s two-level state model
QTG v3 has two levels of state:
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.
Request state (RequestState)
Full transition graph
State descriptions
Terminal state vs recoverable state
Terminal states have no allowed outgoing transitions — once a request reachesCOMPLETED, FAILED, REJECTED, EXPIRED, or CANCELLED, it cannot move anywhere else.
Node state (NodeState)
Full transition graph
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.State descriptions
How state progression differs between Action Node and Observe Node
State progression differs depending on the node type. The difference:- Action Node can pass through the
AWAITING_SIGNATUREstep (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
WhereUNKNOWN can go (only via reconciliation):
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:
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
Priority order:- Everything complete →
COMPLETED - Failed/UNKNOWN + completed node with side effect →
MANUAL_INTERVENTION(most dangerous) - Failed only →
FAILED(safe failure) - Waiting at manual gate →
WAITING_MANUAL_ACTION - Everything else →
None(still in progress, no change to Request state)
Waking the next node
When a node reachesCOMPLETED, what should happen next?
Execution order:
- Find the successor nodes of the completed node
- If a successor exists:
- Transition
BLOCKED→READY(wake it up) - Update
current_frontier - Send
frontier_advancedcallback
- Transition
- If no successor exists (last node):
- Derive the Request state from all node states
Post-transition handling
Deriving the request state is responsible not only for the state transition but also for follow-up processing: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.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.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
- 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_effectflag is the key that distinguishes safe failure vs dangerous failure