A Kaspa vault holds funds behind a rule: "you can withdraw, but you must wait 48 hours, and an alarm key can cancel the withdrawal while it's pending." An escrow deal holds funds between a buyer and a seller with the rule: "either party can release or refund, but if a dispute is opened, only an arbitrator can settle it."
These sound like smart contracts. But Kaspa's UTXO model has no persistent contract storage — no equivalent of an Ethereum account that remembers its state between transactions. So how does a vault UTXO know whether a withdrawal has been initiated? How does an escrow UTXO know it's in dispute?
The answer is a design pattern: encode the state inside the UTXO itself, and require every state transition to destroy the old UTXO and create a new one. Kaspa's Toccata covenants make this possible by letting scripts inspect the transaction that spends them and enforce rules about what the new outputs must look like.
The problem: stateless money, stateful rules
A standard Kaspa UTXO is simple: it locks a value to a public key. Whoever can provide a valid signature for that key can spend it. Once spent, the UTXO disappears.
There is no built-in mechanism to say "this UTXO is in phase 2 of a three-phase protocol." The UTXO either exists (unspent) or doesn't (spent). Yet the tools users interact with — a time-locked vault, a dispute-capable escrow, a dead-man-switch inheritance contract — all need to track which phase they're in.
In account-based chains like Ethereum, the contract address holds persistent storage: a mapping, a counter, a boolean flag. Between transactions, the state sits in the contract's key-value store. In a UTXO chain, no such store exists. The state must live somewhere else entirely.
The solution: state in the scriptPubKey
Kaspa covenants — introduced to mainnet by the Toccata fork — solve this by embedding state directly in the UTXO's locking script, the scriptPubKey. When a covenant-spending transaction consumes a UTXO, the covenant script checks both the current state (read from the input being spent) and the new state (inspected from the transaction's outputs). If the transition is valid, the transaction is accepted and the new UTXO carries the updated state forward.
Think of it like a relay race: the baton (state) is passed from one UTXO to the next. Each runner (transaction) must follow specific rules about how and where to pass it. Drop the baton, or pass it to the wrong lane, and the transaction is rejected by every node.
The Kaspa wiki notes that Toccata introduced transaction introspection — the ability for a script to examine the spending transaction's inputs, outputs, values, and signatures. This is the mechanism that makes state-bearing UTXOs possible: the script can enforce not just "who signs," but "what does the resulting transaction look like."
Anatomy of a state field
In practice, the state is typically one or two small integer or byte fields embedded in the covenant's script data. Consider a vault:
state = { mode: int, dest: bytes[36] }
mode 0 = VAULT // funds locked, normal operation
mode 1 = UNVAULTING // withdrawal initiated, timer running
dest holds the version-prefixed scriptPublicKey of the withdrawal destination. It starts as zeros and gets fixed when the owner initiates a withdrawal.
An escrow is simpler — just the mode:
state = { mode: int }
mode 0 = ACTIVE // normal operation, dispute window open
mode 1 = DISPUTED // dispute filed, waiting for arbitrator
These fields are set during the constructor transaction (when the covenant UTXO is first created) and checked — and rewritten — on every subsequent spend.
Vault transitions: a step-by-step walkthrough
Every spending path in a covenant script is a state transition. The script checks three things: what is the current mode, what conditions are met, and what does the new output look like.
Here is the vault state machine:
VAULT → VAULT (checkin): The owner signs with the hot key. The UTXO is consumed and a new vault UTXO is recreated with mode = 0. The critical effect: the new UTXO's age resets to zero. This is the "I'm alive" signal that resets the inheritance timer.
VAULT → UNVAULTING (initiate): The owner signs with the hot key AND provides a destination address. The new UTXO carries mode = 1 and the chosen dest is now locked into the script. The delay clock starts ticking from the new UTXO's age.
UNVAULTING → VAULT (cancel): The alarm key signs. The withdrawal is cancelled: mode returns to 0 and dest is wiped to zeros. This is the anti-theft mechanism — if someone steals the hot key, the owner has the entire delay window to cancel with the alarm key.
UNVAULTING → destination (complete): No signature required. Anyone can broadcast this transaction once the UTXO's age exceeds the delay parameter. The funds go exactly to the pre-committed dest — the covenant enforces the output address, so even an attacker broadcasting early gets nothing until the timer expires.
A fifth path — migrate — requires both the hot and alarm keys, allowing the owner to move funds to a new vault with different parameters in a single transaction. This is full-authority: both signatures mean the owner dictates all outputs freely.
Escrow transitions
The escrow state machine runs on a separate covenant with its own rules:
ACTIVE → ACTIVE (release, refund, mutual): Any cooperative path can execute while the escrow is in its normal mode. release pays the seller (buyer signs); refund pays the buyer (seller signs); mutual lets both parties agree on any split.
ACTIVE → DISPUTED (dispute): The buyer signs a dispute claim. The UTXO is consumed and recreated with mode = 1; its age resets, starting the arbitrator's deadline countdown.
DISPUTED → destination (arbitrateToBuyer, arbitrateToSeller, arbitrateSplit, or timeout): Either the arbitrator signs a ruling, or the deadline expires and a keyless timeout path releases funds to the preconfigured party. The timeout path charges no service fee — it exists precisely for the case where the arbitrator disappears.
A key invariant across both contracts: no spending path can send funds to any address other than the ones declared in the constructor (buyer, seller, fee address for escrow; hot/dest/heir for vault). The arbitrator has decision power but zero extraction power.
What the script actually checks
Each transition enforces its rules through transaction introspection — the script examines the spending transaction and rejects violations:
- Input count:
require(tx.inputs.length == 1)— prevents multi-UTXO economic attacks where combining UTXOs leaks dust to miner fees. - Output value: the script checks that
outputs[0].valuemeets the expected minimum, ensuring the full balance carries forward (minus the allowed fee budget). - Output address: the script requires
outputs[0].scriptPublicKeyto match the pre-committed destination (the vault'sdest, the escrow's buyer/seller/fee address). - Fee cap:
require(feeBudget > 0 && feeBudget <= 10_000_000)— caps the network fee at 0.1 KAS on keyless paths, preventing an attacker from draining the contract through inflated fees. - Signature: different paths require different keys —
checkSig(sig, hotKey)for normal operations,checkSig(sig, alarmKey)for cancellation, both for migration.
The keyless paths — complete, timeout, and autoRelease — are notable. They require no signature at all. Anyone can broadcast them. Safety comes entirely from the covenant's logic: the timer must have expired, the mode must be correct, and the output must go exactly where the contract specifies. This is what makes Kaspa Safe resilient even if the tooling service disappears — a user can broadcast the keyless transaction directly from a command-line tool against any Kaspa node running version 2 or later.
How Kaspa Forge runs state machines in production
The on-chain script is half the story. A production tool needs to observe the current state and react to changes — all without holding user keys.
Building transactions: Kaspa Forge's WASM core contains dedicated builder functions for every state transition. When a user clicks "Initiate withdrawal" in the vault interface, the browser-side WASM code derives the user's keys from the encrypted Desk profile (keys never leave the browser), selects the vault UTXO, constructs a transaction that transitions the UTXO from mode = 0 to mode = 1 with the chosen destination, signs it with the hot key, and submits the raw bytes to the Kaspa node via gRPC. Each of the seven vault paths and ten escrow paths has its own builder — build_initiate_tx, build_cancel_tx, build_release_tx, build_dispute_tx, and so on — each hardcoding the correct mode transition and output constraints.
Watching state changes: The backend runs a watcher loop that polls the node's UTXO index on a regular cycle. For each registered vault or escrow address, it snapshots the current UTXO set and diffs it against the previous snapshot. A new UTXO at the same covenant address means a state transition occurred (a withdrawal was initiated, a dispute was filed); a disappearing UTXO means the contract was finalized. This drives:
- Alerts: "Withdrawal initiated — you have 48 hours to cancel" via Telegram, email, and Web Push.
- Auto-actions: Broadcasting the keyless
completetransaction once the delay expires, or theautoReleasefor escrows whose dispute window passes without any claim. - Pre-warnings: Notifications at 80% of the inheritance delay ("check in or your heir inherits") and 50% of the escrow dispute window.
The critical property: the watcher can broadcast keyless transactions but cannot steal funds. The covenant's output constraints ensure money flows only to pre-committed addresses, regardless of who broadcasts the transaction.
Restoring state from a seed: Because the covenant's rules live on-chain, restoring a user's position requires only the master seed (to derive keys) and a scan of on-chain addresses. The gap-scan restore mechanism derives addresses from the seed and checks the node's UTXO index for matches. A single .age-encrypted backup made at vault creation covers all future vaults — the HD derivation path includes an incrementing index, and the on-chain state is the source of truth.
See a state machine in action. Kaspa Safe implements the vault state machine described above — deposit KAS, choose your delay and alarm key, and watch the UTXO transition between VAULT and UNVAULTING states on-chain. On-chain operations are free forever; Kaspa Escrow uses the same pattern for P2P deals with dispute resolution.
The constructor: rules baked at creation time
A covenant's behavior is fixed the moment the UTXO is created. The constructor parameters — passed when the funding transaction builds the scriptPubKey — lock in the rules for the lifetime of that contract instance.
For a vault: the hot key, alarm key, delay duration, heir key (or zeros to disable), inheritance delay, auto-inherit flag, fee budget cap, and the initial mode/destination. For an escrow: buyer, seller, and arbitrator keys, dispute window length, arbitrator deadline, timeout direction, fee addresses and fee amounts, and the initial mode.
Once the constructor transaction is mined, these parameters are part of the script bytecode. They cannot be patched, upgraded, or overridden — the only way to change the rules is to spend the UTXO under the existing rules and create a new covenant with different parameters. This immutability is the point: the user doesn't need to trust that the tool provider won't change the terms.
Trade-offs: what this pattern can and cannot do
The state-machine-in-a-UTXO pattern has clear strengths:
- Transparency: The state and all transition rules live on-chain, in the script. Anyone can read and verify them.
- Non-custodial safety: No server, no oracle, and no third party holds funds or can unilaterally alter the state.
- Graceful degradation: If the tooling service dies, the covenant UTXO persists on-chain with its rules intact. Keyless paths remain executable by anyone with access to a Kaspa node.
- Isolation: Each covenant UTXO is independent. A compromised escrow cannot affect a vault, and vice versa.
And honest limitations:
- No persistent storage: There is no key-value store, no mapping, no counter. Each transition must carry all necessary state in the new output's scriptPubKey. Complex state with many fields would bloat the script.
- Compute budget: Each spending path is bounded — 20 units for normal vault paths, 40 for the mutual escrow path that checks up to three signatures and three outputs. This rules out loops, complex arithmetic, and large data structures.
- Consume-and-recreate overhead: Every state change is a full transaction. A vault checkin — just to signal "I'm alive" — requires a network fee. In Kaspa's high-throughput, low-fee environment this is practical, but it is fundamentally more expensive than a storage write on an account-based chain.
- Known v3 boundary: The current vault contract checks the required output value but not the total number of outputs. A transaction author could direct leftover value (within the fee budget) to an additional output. The official builders always produce canonical single-output transactions, and a stricter output-count guard is planned for a future version alongside an external audit.
The broader context
The state-machine-in-a-UTXO pattern is not unique to Kaspa — Bitcoin's own covenant proposals (CTV, APO) explore similar territory. But Kaspa's Toccata covenants bring it to a high-throughput proof-of-work chain running at 10 blocks per second, where confirmations arrive in under a second and fees are low enough that consuming and recreating UTXO state on every transition is economically viable.
For developers, the pattern requires thinking differently from account-model development. There is no setState() call. Every state change is a transaction. Every transaction must fully specify its inputs and outputs. The contract's logic is enforced by the script, not by a runtime. This is both the constraint and the strength: what is on-chain is exactly what executes, with no hidden state, no admin override, and no server dependency.
For users, the practical takeaway is simpler: when you lock KAS in a vault or escrow on Kaspa, the rules governing your funds are not stored on a server — they are encoded in the UTXO itself, enforced by every node that validates the transaction, and executable by anyone who follows the contract's logic.
FAQ
What is a covenant state machine in Kaspa?
A pattern where a UTXO's scriptPubKey encodes a mode field (for example, 0 = VAULT, 1 = UNVAULTING) that determines which spending paths are valid — turning a stateless UTXO into a stateful on-chain contract.
How does the vault UTXO track its state?
The vault stores two values: a mode (0 = VAULT, 1 = UNVAULTING) and a destination address. Each valid spending path reads the current mode and produces a new UTXO with the updated mode — or finalizes the funds to the destination.
Can a Kaspa covenant hold arbitrary persistent data?
No. Covenant state is limited to what fits in the scriptPubKey and a small number of embedded fields, constrained by compute budget. There is no account-style storage. Each state change requires consuming and recreating the UTXO.
What happens to the old UTXO during a state transition?
It is fully destroyed (spent). A new UTXO is created with the updated state encoded in its script. This is how every UTXO-based state transition works — there is no in-place update.
How does Toccata enable these state machines?
Toccata brought covenants to Kaspa mainnet, adding the ability for a script to examine the transaction that spends it — checking inputs, outputs, values, and signatures — which is what makes state-bearing UTXOs possible.
What are the main trade-offs of UTXO-based state machines?
They provide censorship resistance, transparency and non-custodial guarantees, but offer no persistent storage, are bounded by compute budget, and require a full spend-and-recreate cycle for every state change.
Put your KAS where theft can be cancelled
A covenant vault on Kaspa mainnet: your keys, your rules, our tooling. Free on-chain, forever.
Create a vault
