Tic-tac-toe with stakes
Take the simplest game that can carry money, and put money on it: tic-tac-toe, for stakes.
Two strangers lock funds into a pot on the Kaspa network and play a multi-round match. When the match ends, the winner takes the pot; a draw splits it. There is no casino and no escrow agent. The loser cannot refuse to pay, and the winner cannot claim more than they earned. The operator cannot rewrite the scoreboard either, because the scoreboard is a proof, not a statement by anyone.
This is vprog-tictactoe (we’ll call it tt), a working example built on vprogs, a framework for based computation on Kaspa (Based rollup on Kaspa explains the word). The game is chosen for size: tic-tac-toe is small enough to hold in your head and still exercises every part of the framework, a proof of concept, not a claim that this game needs a rollup. In tt, the game’s own actions, accounts, and rules are the only application-specific code; everything else is reused machinery: executing, proving, settling, exiting. The moves you make in a browser are signed user actions; the match state lives in a verifiable program; the payout lands on the real Kaspa chain.
The rest of this short book explains the machine:
- what a based rollup is, and what it demonstrates Kaspa can do,
- the small set of transaction types the whole machine is built from,
- how proofs chain user actions, deposits, and settlements across many blocks of the Kaspa chain (its L1),
- what a zkVM has to do with it,
- how this compares to a smart-contract platform like Solana,
- and what you can build on it today.
A live demo of tt runs alongside this material.
Two things to state up front. First, everything the machine needs is live on Kaspa mainnet; the demo itself runs on the public testnet, a demonstration network with looser security assumptions (Based rollup on Kaspa gives the details). Second, safety and liveness are different guarantees. The chain guarantees that no one can falsify state or steal funds, but moving money requires the machine to keep running. Who runs it, what can stall it, and what happens when nobody does are covered in The machinery and Building an app on it.
The words you need
Blockchain writing uses a small vocabulary. Here is all of it, in plain words; the rest of the book assumes these. Entries marked * simplify where the full rule would weigh more than the book needs; each links the appendix that carries the complete version.
- Kaspa: the proof-of-work network this book runs on; its coin is KAS. What matters here is its blockdag and its UTXO model, both below.
- Blockdag*: the shape of Kaspa’s history: a web, not a line. Where a classic chain discards parallel blocks, a Kaspa block names several parents, so blocks mined at nearly the same time all count, and consensus orders the web into one agreed order of transactions, roughly ten blocks a second. The machine consumes that order; the shallow reorg churn the web leaves behind is why the confirmation window exists (the Reorg entry below). The appendix has the ordering in full.
- Sompi: the smallest unit of KAS; one KAS is 100,000,000 sompi.
- DAA score*: a depth counter every block carries, counting blocks in its past. It only moves forward, so the program reads it as its clock: deadlines are differences in DAA score, not wall-clock time (The transaction vocabulary). Kaspa also paces mining difficulty and emission by it. Its sibling, the blue score, is the number of blue blocks in a block’s past, the blockdag’s version of block height; Kaspa counts confirmations in it, and the program sees it only as block context (The transaction vocabulary). The appendix separates the two exactly.
- L1: “layer 1”, the Kaspa network itself. The layer that holds the funds.
- L2: “layer 2”: a system that does its work off the L1 while depending on the L1 for what must be trusted: ordering, data availability, and settlement. A rollup is the common L2 shape: execute off-chain, then prove or commit the results back on-chain. The machine in this book is one.
- KIP: Kaspa Improvement Proposal: the process by which the network proposes, reviews, and activates protocol changes. The proposals themselves live in the KIP repository.
- Output (UTXO): a piece of KAS at a lock. Every Kaspa transaction consumes earlier outputs (as its inputs) and creates new outputs; one that no transaction has consumed yet is an unspent transaction output. “Your money” is the set of UTXOs your key can unlock. And an output lives in exactly one transaction: once spent it is gone, and no other transaction can reference it. That one-way rule is why shared on-chain state is hard here (Based rollup on Kaspa).
- SPK, P2PK, P2SH: the locking half of an output is its SPK (script public key), a small program stored inside the output. A later transaction spends that output by supplying input data that satisfies the SPK. P2PK: the SPK demands a signature from one specific public key, and the address is derived from that key. P2SH: the SPK stores only the hash of a script; the spender reveals the script, shows its hash matches, and the revealed script then runs and must succeed. P2SH is how this machine puts its own rules onto Kaspa: the program is committed from the moment the output exists, but only seen at spend time.
- Settlement: the transaction that commits one new state digest of the program’s off-chain (L2) state to Kaspa, spends the output only a valid settlement can spend, and chains to the settlement before it. The transaction vocabulary is about it.
- State digest: a short fingerprint of the program’s whole off-chain state: one 32-byte number that changes whenever the state does. Settlements commit it on L1. The transaction vocabulary builds it.
- Lane: the program’s public inbox: a labeled stream (a Kaspa subnetwork) of ordinary transactions carrying users’ signed actions. Miners mine them like any other payment; there is no gatekeeper to refuse an entry.
- Covenant: Kaspa’s mechanism (KIP-20) for scripts that bind outputs to one program instance: a covenant transaction carries the instance’s id, consensus enforces the binding, and a script can demand that its spender run under a named covenant. Settlements and exit claims are covenant transactions. The id, the blake2b hash of the covenant’s redeem script, is the 32-byte name of the instance: that one script pins the guest image ids (the exact rule-set version), the lane, and the state digest and lane tip the instance chains from, so it commits to code, lane, and settlement chain together, and the deposit address derives from it in turn.
- Guest: the program’s own code, running inside the proving machine (the zkVM), as opposed to the framework around it.
- Image id: the cryptographic hash of one guest program binary. Pinning image ids fixes the exact code and proof stack an instance runs (The transaction vocabulary and The zkVM, briefly).
- Mempool: the set of transactions announced to the network but not yet included in a block.
- Execution: the guest program doing its work: reading confirmed L1 data, checking and applying each action, crediting deposits, and moving the state from one root to the next. It runs off-chain inside the zkVM; the proof is what makes its result trustworthy (How it all chains).
- Witness: the confirmed L1 data execution reads: lane entries, deposits, block context. How it all chains builds the pipeline around it.
- Journal: the fixed-format record inside each proof: the state before, the state after, how far the lane had been read, which L1 blocks execution saw, and the deposit and exit commitments where the step carried any. vprogs writes three levels of them, transaction, batch, bundle; The zkVM, briefly lays them side by side. Used from How it all chains on.
- Proof, receipt: a few kilobytes of mathematics that convince anyone, without re-running the program, that a specific guest produced a specific journal.
- Runtime: the layer of code that checks and applies each action. Solana: the real difference is about who owns it.
- Reorg (reorganization): now and then the network briefly agrees on one block order, then switches to another; the switched-away blocks “vanish”. Shallow churn like this is normal and expected; deeply buried blocks essentially never reorganize, which is why the machine waits out a confirmation window before trusting fresh blocks (How it all chains).
- Confirmation window: the number of blocks of depth the machine waits before treating an L1 block as final; widened adaptively when the network looks reorg-prone (How it all chains).
- Safety: the guarantee that nothing invalid ever becomes settled state: no one can forge, steal, or pay twice, whatever the operator does.
- Liveness: the guarantee that someone keeps executing, proving, and settling, so actions and exits keep processing. Safety says no one can steal; liveness says the machine does not stop. The machinery owns it.
- Data availability*: the guarantee that you can fetch the full record of what was published, yourself, from the network, rather than trusting someone’s summary of it. Ordinary nodes prune old block bodies, so data from far enough back is served by archival peers, and what they serve checks against the chain’s own headers (the appendix).
- Mass: the weight of a transaction that fees are priced on: the larger of compute mass (verification work) and storage mass (the unspent-set growth the transaction leaves behind; it rises when value is split into many small outputs). The appendix has the formula and what nodes store.
- Dust: outputs too small to be worth spending. The network’s minimum-relay rules floor how small an output may be, which limits how far the deposit pile can be split.
Based rollup on Kaspa
The one-sentence version
A based rollup moves a program off the L1: its state, its rules, and its execution. It keeps enforcement on the L1: every state change worth trusting is proved and settled back to Kaspa. That off-chain half is the L2: the program itself, using Kaspa for what it must not provide itself.
What each side carries follows from that. Ordering is the L1’s: users publish straight to the program’s lane, and the chain’s own order is the order. Verification is the L1’s: consensus checks every settlement’s proof before it counts. Enforcement is the L1’s: payouts move only through scripts Kaspa itself executes. That is the whole sense of based here: the app runs directly on its L1, with nothing in between. The state itself is the one thing the L1 does not carry: it holds a single 32-byte fingerprint of it (The transaction vocabulary), so the full data behind that fingerprint, every account, balance, and game, is stored and served by an L2 provider, the operator’s node. The actions and deposits do sit fully on the chain, in the lane. No external committee, no data-availability service, and no bridge token (no new coin standing in for the locked KAS) sits in between. One qualifier: ordinary Kaspa nodes prune old block bodies, so data from far enough back is served by archival peers, and what they serve checks against the chain’s headers (the appendix).
A note for readers from the Ethereum ecosystem (others can skip this paragraph): in Ethereum discourse “based” means L1 proposers do the sequencing. Here nobody sequences at all; ordering is not a service: users publish actions straight to the lane, and the L1’s own order is the order. One Ethereum connotation does not transfer: there, based also comes with forced inclusion, an L1 path that makes the rollup process your transaction even if the sequencer refuses. This machine ships no such forced path; the liveness limits are The machinery’s. Readers who prefer the established name for this shape will find it in Single sovereign apps: a sovereign app.
The shape of the whole system fits in one diagram:
flowchart LR
U["Users (signed actions)"] -- "actions -> lane (subnetwork)" --> L1["Kaspa L1"]
U -. "state reads (index)" .-> OP["Program instance (operator)"]
OP --> PR["Provers"]
PR -- "proofs" --> OP
OP -- "settlements + proofs" --> L1
L1 -- "witnesses (lane data, deposits, blocks)" --> OP
L1 -- "payouts (exits)" --> U
Users sign actions and submit them to the program’s lane on Kaspa themselves: the lane is an L1 subnetwork that serves as the program’s public inbox and its data availability. The operator is not in the loop for submissions: it reads the lane and the chain directly and executes actions off-chain. Provers produce cryptographic proofs that the execution followed the program’s rules. Settlements, carrying those proofs and commitments to the new state, land on Kaspa. Money flows back out to users through exits, enforced by those settled proofs.
One arrow in the diagram needs a note: apps read current state from the operator’s index (the dashed line), not from Kaspa directly. The index is a convenience, not the authority; Building an app on it covers what a skeptic can do without it.
The two limits, and the extensions that change them
Kaspa’s script engine is real and richer than Bitcoin’s: it concatenates, hashes, does arithmetic, inspects its own transaction’s inputs and outputs, and, since KIP-16, verifies zk proofs. Ecosystem projects (SilverScript, Argent among them) are building higher-level abstractions over it. The claim that Kaspa has “no smart contracts because no scripting” is wrong. The limits are structural, and there are two.
First, Kaspa runs on UTXOs: every coin is an output consumed by exactly one transaction, and no other transaction can reference it afterward. Shared state has no native home, so a program must carry its pot, board, and balances forward output by output (abstractions like SilverScript make the threading easier; it is still manual work).
Second, the script language is deliberately not Turing-complete (it cannot run arbitrary programs, only check fixed shapes): flexible enough to lock and check, not to be an application.
Both limits are deliberate design choices: they keep the chain fast and simple to reason about. Both also received protocol extensions through Kaspa’s own proposal process (KIPs, the network’s improvement proposals), activated on Kaspa mainnet by the Toccata hard fork, a coordinated upgrade. KIP-20 added covenant ids, so scripts can bind outputs to one program instance’s identity. KIP-21 added lane commitments: every block header carries a commitment to the active lanes’ entries, so a lane’s history is committed in the chain’s headers and reconstructible from the chain itself (How it all chains has the mechanism). Proof verification is therefore a consensus rule, not a service: every Kaspa node that executes a settlement runs the check, and a bad-proof settlement is invalid, rejected like a bad signature. The check is verification, not re-execution: small for every node, and priced into the settlement’s own transaction like any script work. Which proof system and which program version to trust are not choices made at spend time; they are baked into the covenant’s script hash itself, which The transaction vocabulary opens up.
Where this lives today: those extensions are implemented and activated on Kaspa mainnet. The live demo settles on public testnet-10 (mined by its public miners), and that is a deployment choice, not a protocol gap: a demonstration belongs on a demonstration network, with deliberately looser security assumptions. Every vprogs settlement you can watch today is a testnet fact; what separates the demo from a mainnet deployment is operational work, not protocol activation.
With these three extensions (KIP-16 from above, plus KIP-20 and KIP-21), a based rollup removes both limits, KIP-16 doing the verification of arbitrary rules and KIP-20 with KIP-21 giving one instance’s scattered outputs a shared identity and its data a canonical order: the program’s state lives inside the proof, in whatever shape the program defines, and the rules can be arbitrary code, because the chain never runs them; it verifies a compact proof and moves money according to the result. The L1 stays simple; the applications do not have to be.
What are you actually trusting?
Every design for “a game with a pot” answers the same question: who could steal, and what stops them?
| Model | Who holds the pot | What stops them stealing | What’s left to trust |
|---|---|---|---|
| Custodial referee | The operator | Reputation, law | Everything: they can just take it |
| Multisig escrow | A set of signers | M-of-N honesty | The majority of signers, and their liveness |
| Zk-proven program | The L1 itself | Proofs the L1 verifies | The code being proved, the zkVM and verifier opcode underneath it (The zkVM, briefly), and that someone keeps the machine running |
Moving down the table removes trust in people one layer at a time. The zk-proven model’s key property is that “did the execution follow the rules?” stops being a question about anyone’s honesty: the operator can be anyone and still cannot produce a settlement for a state the program’s rules don’t allow. The proof either verifies on Kaspa or the settlement doesn’t happen.
One more point the table compresses: the deposit pile, many separate coins locked at the one deposit address, is pooled custody at a script, and its safety is exactly the safety of the pinned code, bugs included. A rule-set bug that pays the wrong recipient drains the pile through perfectly valid proofs. That is what “trusting the code” means, and it is why the pinning in The transaction vocabulary matters.
What remains is a different kind of trust: liveness and data availability (you can see the state you need to act). Liveness: a settlement only exists if someone executes actions and settles proofs, and today that someone is the operator. No permissionless escape-hatch flow is shipped yet either: if every operator of an instance stops before your balance has become a committed exit, your funds wait until someone resumes the stack. The machinery and Building an app on it cover who can resume, at what cost.
The shape and the rules
One idea runs through the rest of the book:
The rollup fixes the shape; the program picks the rules.
The framework fixes the shape of the machine: user actions are signed, state changes are proved, settlements land on Kaspa, money leaves only through enforced exits. But the rules (what a deposit requires, who may move which funds, how the state is derived, even how exits work) are choices made by each program. vprogs ships working implementations of all of them (deposit logic, lockers and signers, the permission tree, the exit mechanism); treat each as a battery: a standard part you can use as-is and, in principle, replace with a different design.
The next chapter covers the handful of transaction types everything else is built from.
The transaction vocabulary
This chapter first covers what a Kaspa transaction is, then the compression step that fits the off-chain state into 32 bytes, and then the four transaction types the machine is built from, which is where the L2, the program’s off-chain half, appears.
The Kaspa transaction itself
Every Kaspa transaction, whether it pays a friend or runs this machine, has the same shape. It consumes earlier outputs (its inputs) and creates new ones (its outputs). An output is an amount of KAS plus a lock: the SPK, a small program stored inside the output. A later transaction spends that output by supplying input data that satisfies its lock, and once spent, the output is gone; no other transaction can reference it again. That is the whole format. The balance rule: a transaction’s inputs must be worth at least its outputs, and the difference is the fee, paid to whoever mines the block that includes it. Paying 3 KAS out of a single 10 KAS output means building two outputs in the same transaction: 3 to the recipient, the rest back to your own lock, your change. Every transaction in this book, payment or machine, plays by those rules. There are no accounts and no shared stored state on this chain, only coins at locks, each spendable exactly once, and whatever anyone builds on Kaspa is expressed in these bytes. The spend-once rule is the first limit from Based rollup on Kaspa; this chapter builds directly on it.
The 3 KAS payment above, as a picture:
flowchart LR
subgraph E["an earlier transaction"]
O1["its output: 10 KAS<br/>locked at your SPK"]
end
O1 -- "input: names that output,<br/>carries the unlock data<br/>its SPK demands" --> TX["the new transaction"]
TX --> O2["output: 3 KAS<br/>locked at the friend's SPK"]
TX --> O3["output: your change,<br/>back at your SPK"]
TX -. "the fee: inputs<br/>minus outputs" .-> M["the miner of the<br/>block that includes it"]
The shape itself, annotated:
a Kaspa transaction
├── inputs: earlier outputs being spent, each named by its
│ transaction id and position, each carrying the unlock data
│ its SPK demands
├── outputs: new coins, each an amount in sompi plus the SPK
│ (the lock) they now sit at
├── version and lock time: housekeeping, ignorable in this book
└── subnetwork id and payload: a label and a data field; plain
payments leave them default, and the machine's lane tag
rides them (the lane section below)
Everything the machine publishes, and every deposit feeding it, is built from this one shape.
State compression: the whole state in 32 bytes
The program in this book keeps accounts, games and balances, and none of that fits inside one-shot outputs. So the full state lives off-chain, and what the chain holds is a fingerprint of it: the state digest, a single 32-byte root of a sparse Merkle tree (a tree of hashes: every node is the fingerprint of its children, all the way up to one root). The tree is the whole state hashed into a structure where every possible position exists. “Sparse” means the empty positions are not stored anywhere: the tree is a rule for computing what an empty spot would hash to, so where a thing sits never depends on what else is present.
Eight leaf slots out of effectively unbounded positions (tree depth 256), each addressed by position:
flowchart TD
R["root - the state digest"] --> N0["node"]
R --> N1["node"]
N0 --> M00["node"]
N0 --> M01["node"]
N1 --> M10["node"]
N1 --> M11["node"]
M00 --> L0["slot 0 - empty"]
M00 --> L1["slot 1 - an account"]
M01 --> L2["slot 2 - empty"]
M01 --> L3["slot 3 - empty"]
M10 --> L4["slot 4 - a game"]
M10 --> L5["slot 5 - empty"]
M11 --> L6["slot 6 - empty"]
M11 --> L7["slot 7 - an account"]
classDef empty fill:#eee,stroke:#999,stroke-dasharray: 4 3
class L0,L2,L3,L5,L6 empty
The empty slots are the “sparse” part done cheaply: an empty slot’s hash comes from the fixed rule, not from storage, and an entirely empty subtree collapses to one computed value, so emptiness costs nothing. That is what makes membership cheap: proving that one account was inside takes a short branch of hashes and reveals nothing else. Change one sompi (the smallest unit of KAS) anywhere and the digest is a completely different number; there is no way to move the state without moving the fingerprint. One chain-held number commits to the entire off-chain state; that is the core mechanism of this book: the chain never stores the state, it checks fingerprints of it. Digest, root, state root: this book uses the words interchangeably for the same 32 bytes. (This structure is standard equipment far beyond this book; Kelvin Fichter’s “What’s a Sparse Merkle Tree?” is the classic short introduction, with pictures in the same shape.)
The four transactions
Those two pieces are what the L1 offers: a transaction format that can carry anything, and a way to compress the whole state into a number a transaction can carry. The L2 is what you build with them: the program executes off-chain, over the full state, and publishes the digest. Everything that must be trusted rides ordinary Kaspa transactions: they carry users’ intent in, order it, commit state back, and pay value out. Four kinds of transaction make up the machine’s whole traffic (users publish three; the machine publishes settlements), and from the L1’s side, from Kaspa’s side, they all have exactly the same shape: same fields, same validation, nothing marked out in protocol. The split into four is not an L1 distinction at all; it is semantics from the L2, the program’s perspective: particular scripts and payloads whose meaning only the proofs enforce.
- a lane entry is an ordinary transaction tagged for the program’s subnetwork, carrying a signed action
- a deposit is an ordinary payment to the program’s deposit address
- an exit claim spends one of the program’s exit commitments and pays a user out
- a settlement spends the output only a valid settlement can spend, and commits the new state digest
Two are plain Kaspa usage (lane entries, deposits); two carry the machine’s own scripts (settlements, claims). The kinds overlap in practice: in tt the deposit is also a lane entry, one transaction doing both jobs. The rest of the book follows from these four. This chapter takes them in the order things flow: the lane they ride, value in, value out, the actions between, and the settlement that commits it all.
The lane (subnetwork) and its key
User actions cannot be sent privately to the operator, because then nobody could prove what was submitted, when, or in what order. Instead, each program instance owns a lane: an L1 subnetwork where its users’ actions are published as ordinary Kaspa transactions. The entries are ordered by the node’s own commitment machinery (KIP-21): no sequencer decides, the chain’s own order is the order, and Kaspa’s consensus can prove, up to any block, exactly what it contained. So the lane has a single well-defined head, the lane tip, and the L1 node itself can prove what the lane contained up to any block.
What is a lane, physically? Ordinary Kaspa transactions. A lane entry is a real transaction, paying a real Kaspa fee from the user’s own funds, mined by the network’s miners like any payment. The subnetwork is a label the node’s consensus tracks: it gossips, orders, and accounts for lane traffic alongside ordinary payments. Carrying registered lanes is a consensus rule, not an opt-in a miner could quietly refuse; the L1 side enforces no admission rules beyond that, and a program may layer its own entry rules if it wants them. Each lane has a gas limit per L1 block; entries over the limit wait for later blocks, and nothing is dropped or refused. And the node itself will hand anyone a cryptographic proof of what the lane contained up to any confirmed block. There is no lane operator to refuse an entry; entry happens through the Kaspa mempool, the network’s shared set of transactions not yet in blocks.
The lane is both the program’s entry point and its data availability: publish there and the operator must eventually see your action; prove from there and a verifier knows nothing was left out. The lane is identified by a lane key, the hash of its subnetwork id, and every proof names the lane it settles.
How the lane becomes state, in one picture: execution watches confirmed blocks, filters each one down to this lane’s entries, and wraps a whole run of them, several blocks’ worth, into a single proved transition; where that run emitted exits, the settlement carries the permission-tree commitment alongside.
flowchart LR
subgraph L1["confirmed L1 blocks (mixed traffic)"]
B1["block N<br/>payments + 3 lane entries"]
B2["block N+1<br/>payments only"]
B3["block N+2<br/>payments + 2 lane entries"]
end
L1 -- "filter by lane key" --> E["this lane's entries,<br/>in chain order"]
E --> T["one state transition"]
T --> S["settlement: state digest + lane tip,<br/>plus the permission-tree commitment<br/>when the window emitted exits"]
Deposits
A deposit is how value enters, and it is one transaction seen from two sides. From the L1 side, money moves to the program’s script: an output paying the deposit address, derived from the covenant id (the 32-byte identity of this program instance from the glossary; this chapter pins it properly at the end), and spendable only by exits already proven into a commitment. From the L2 side, the payload names the owner: the signed deposit action carried in the same transaction says which account the program must credit, and which lock authorizes that account when it is new (a lock here is the account’s spending rule in program state, not the L1 output’s lock; the closing table pins the term). In tt the deposit is the action: one lane transaction carries both.
a tt deposit, one ordinary Kaspa transaction
├── payload: the signed deposit action, "credit account X",
│ plus X's lock if the account is new
└── outputs: one output paying the program's deposit address;
only the covenant's scripts can ever spend it
The proof binds the two sides: it checks that the output exists, pays the covenant-derived script, and credits exactly the account the signature named. Nobody can steer a deposit to a different account, and the landed coins sit at a script only the covenant’s own transactions can unlock, never at an operator’s key.
The shape is fixed (an L1 output, recognized by the program, proven into the state), but the address policy is the guest’s choice (the guest is the program’s own code running inside the zkVM; The zkVM, briefly). tt’s deposit policy derives one shared deposit address from the covenant id; the framework documents a per-user address scheme as an equally valid choice. Same battery, different placement.
Exits and the permission tree
An exit is how value leaves: the program debits a user and emits an entitlement to withdraw on L1. The mechanism below is the shipped standard part, not a rule of the framework: what the framework fixes is only that a window’s exits fold into one commitment the settlement carries; the accumulator that builds it, and the claim flow behind it, are the program’s choice. The shipped choice is the permission tree, an accumulator (a Merkle structure that answers one question: is this leaf in?) whose leaves are “(L1 script, amount)” pairs: who may claim how much, by L1 lock type. A settlement that emitted exits carries a commitment to exactly those exits, the ones its own proof window produced, in a dedicated P2SH output (a settlement with no new exits carries none). Each commitment covers only its own settlement’s exits, so an entitlement lives in exactly one commitment, ever.
The mental model is one list per settlement, not one account per user: a settlement that emitted exits locks the list of exactly those exits into one commitment output, and a leaf is one line of the list, an L1 lock plus an amount. The tree itself lives off-chain, with whoever serves the program’s data (the DA and index layer of The machinery); L1 holds only its root, inside the commitment’s script, and a claimant brings the leaf and its branch when claiming.
The full journey of one withdrawal:
- you publish a signed withdraw action to the lane, naming how much and which L1 lock to pay;
- execution debits your L2 balance and emits an exit leaf, “(your L1 lock, amount)”; the window’s exits become one tree inside the bundle proof, the proof’s journal commits its root, and the settlement covering that window carries the commitment;
- once that settlement is confirmed, the claim is an ordinary L1 transaction: whoever builds it, you, the operator, or anyone offering claims as a service, spends the commitment, reveals your leaf and its branch, and the script pays the lock the leaf names.
A user claims with one ordinary Kaspa transaction that spends that commitment. The commitment is locked at a P2SH script whose bytes embed the tree’s root, and the claim’s unlock data reveals the leaf, its branch in the tree, and how much of the leaf to deduct. The script’s first job is verification: it hashes the leaf (destination script and amount), folds it up the branch, and requires the result to equal the embedded root; the payment must go to the lock the leaf names, for exactly the deduct, which may be a whole leaf or part of one. Its second job is the handover: it folds the same branch around the reduced leaf to compute the next root, and the change becomes a fresh commitment with the paid part removed, serving the next claimant. The payout is funded from the program’s deposit pile; the pile’s untouched remainder is re-locked at the deposit address’s script. So the same exit can never be paid twice: the leaf lives in one commitment, the UTXO spent is gone, and the new commitment carries only what is left. Claims on one commitment contend like any two spends of one coin: one wins, the loser rebuilds against the fresh commitment; claims on different commitments pay out in parallel. What does not ship is a sweeper for the pile itself: claims split what they sweep and dust rules floor the pieces, so keeping the pile in spendable coins is operational work.
Why not fold the deposit pile into one coin while settling? Folding is itself an L1 transaction that pays fees, and a single coin would serialize every claim behind one spendable output. Many coins let each claim sweep its own inputs and pay out in parallel, so the complexity lands on the claim side, where claims already pay fees.
The claim transaction’s full shape (which coins it pulls, how the fee rides, what each output is) and what happens when two claims race are appendix material.
Like the deposit policy, the permission tree is a ready-made part: the framework ships the accumulator, the L1 commitment format, and the claim flow. In code the choice is literal: the guest implements a small exit-accumulator interface (record exits, return one commitment), and the framework ships the permission tree as one implementation, a no-exits no-op as another, for programs that never emit. The tree is also the universal shape: the payout happens in an ordinary L1 transaction that the claimant builds and pays for, so the program itself never has to compute transaction mass or fees to get value out. A program with different needs can ship its own exit design (direct payouts, several trees); the settlement shape would not change.
User actions
Everything a user does inside the program is a signed action. tt’s action.rs defines them: transferring balance, rotating your lock (switching the key that authorizes your account, the move you want if a key leaks), depositing, withdrawing, creating a game, joining a game, placing a mark, forfeiting an expired turn. The program sees the chain’s per-block context, timestamp, DAA score, and blue score, committed by the chain inside every proof window (KIP-21 commits them for exactly this use). Deadlines such as tt’s turn timer are measured in DAA score, one of the chain’s depth counters from the glossary, not in wall-clock time, so “expired” is determined by the chain, not by the operator. An action carries its author’s authorization (more on locks and signers below) and is published to the lane. What makes an action valid (whose signature, which state it may touch, how much stake a game locks, what happens when your turn timer expires) is not L1 law. It is the program’s own logic, checked inside the proof. The L1 has no concept of a “game”; it carries the action to the program, which does.
The settlement
The settlement is the committing transaction: the only one that advances the program’s authoritative state, and it does so on Kaspa itself. A settlement attests three things at once:
- a state digest: the program’s new state root, the 32-byte fingerprint from the compression section above. The full state (every account, every game, every balance) lives off-chain; the lane and the chain carry everything needed to rebuild it (Building an app on it). What Kaspa holds is the fingerprint of all of it at one moment.
- a lane tip: how far execution had read the program’s action lane (the lane section above) when the snapshot was taken: “I have processed every published action up to here.” Concretely the tip is a hash, the lane’s running commitment, not just a bookmark (How it all chains pins the mechanism).
- a block proof point: the last L1 block whose data execution consumed: “and the L1 data I saw was real up to this block.”
Each attestation is backed by a zk proof, carried in the same transaction. The proof’s central claim is always of the form “state root X became state root Y by executing the rules correctly”: the previous settlement’s digest, the claimed new digest, and every lane entry, deposit, and L1 context item in between, processed by the program’s pinned code, with nothing skipped and nothing invented. The proof is generated off-chain by a zkVM and verified on-chain by every Kaspa node as a consensus rule (KIP-16; The zkVM, briefly covers the zkVM). A settlement whose proof does not verify is invalid, and no node includes it.
These three ride directly in the settlement transaction’s script data (the bytes its inputs carry to satisfy the covenant’s lock), and the settlement’s outputs chain to the next settlement: output 0 is a P2SH continuation that only the next valid settlement can spend. So on L1 itself there grows a single unbroken chain of settlements, each one inheriting its predecessor’s covenant id and committing the next state digest. That chain is the program’s history; you can walk it on any Kaspa explorer.
How the windows behind one settlement chain together, why no block range can be skipped, and what happens when cited blocks reorganize are mechanics worth their own page: the appendix has them.
One coin’s round trip
Alice deposits, plays, and withdraws; here is the whole trip with the vocabulary attached.
The deposit is one ordinary Kaspa transaction: Alice’s coins in, the signed deposit action in the payload, one output paying the deposit address (the Deposits section above). It confirms; a proof window covers it; execution credits Alice’s L2 account, and the next settlement commits a state digest that includes her balance.
Playing is lane entries. Alice sends signed actions, create a game, place a mark; Bob joins and answers. Each action rides an ordinary transaction to the lane; execution checks each one against the program’s rules inside the proof, and the state digest moves with every settled window. Bob’s turn timer, measured in DAA score, expires; the game resolves and the stake lands on Alice’s balance.
Leaving is two transactions, and the first is itself on L1: the withdraw action rides an ordinary lane transaction Alice signs and publishes, like every action; the window that processes it debits her balance and emits an exit leaf, and its settlement carries the commitment. The second is the claim: once that settlement confirms, it spends the commitment and pays her lock (the Exits section above). That is the full trip: one deposit transaction in, a run of lane entries, one settlement with a commitment, one claim out.
Shape versus rules
| Fixed by the rollup shape | Chosen by the program (batteries included) |
|---|---|
| Actions are signed and published to a lane | What the actions are (tt: CreateGame, PlaceMark, …) |
| State is a digest; settlements chain digests on L1 | What lives in the state (resources, balances, boards) |
| Value enters by L1 deposit, leaves by proven exit | The deposit address policy |
| Settlements prove execution against L1 blocks | The exit mechanism (permission tree is the standard part) |
| Actions require authorization | Locks, unlockers, signers: how users authorize |
That last row deserves its own sentence: locks, unlockers, and signers are guest preferences too. The framework provides common implementations: a lock names who may act (tt uses pubkey locks), a signer proves control of it, an unlocker pairs them. A program can compose or replace them. Every battery above is real, shipped code in vprogs today; “replaceable” means the shape doesn’t depend on which one you use.
One term is left, and it names the whole thing: a covenant id, the 32-byte identity of one program instance. Deposits pay into it, settlements chain within it, exits reference it. The pins chain together in one direction. The covenant’s redeem script embeds the three guest image ids (The zkVM, briefly fixes the code and proof stack), the lane key, and the state digest and lane tip the instance chains from (the pin list). The covenant id is that script’s hash, and the deposit address derives from the covenant id (a short derived script), so rules, lane, id, and address change together: swap one image id or the lane and the hash matches nothing. Anyone can recompute the hash and check the address before depositing; the tooling is a script today, not a website, so in practice you rely on someone you trust having run it, the same trust in the code that Based rollup on Kaspa’s table already counted. A binary that changes by one byte hashes to a different image id, which no longer matches its pin; that is why an upgrade means moving to a new instance (Solana: the real difference); the pin could in principle migrate to new images, but no such mechanism ships today.
In tt’s live deployment the covenant id is literally a constant. At bootstrap the operator funds an initial output locked by the covenant’s script with the genesis state root embedded; that output is the settlement chain’s first link, and every settlement descends from it.
With the vocabulary in hand: how do proofs tie all four tx types together across multiple L1 blocks?
How it all chains
This chapter puts the pieces together: every piece from the last chapter (the lane, deposits, exits, user actions, settlements) moves at once, across multiple L1 blocks, tied together by proofs.
Vocabulary check before it piles up; six terms, one pipeline:
- a witness is confirmed L1 data fed to execution;
- a tx proof proves one transaction’s execution; transactions that write disjoint resources execute and prove in parallel, across blocks too, while actions inside one transaction always run in sequence and contending transactions take the chain’s order;
- a batch is the work over one block, and its proof is a batch proof;
- runs of batches compound into aggregate proofs;
- the latest aggregate, the one a settlement carries, is the bundle proof;
- the guest program that does the compounding is the aggregator (The zkVM, briefly).
One word in that list needs more room. An aggregate is any such compaction; a bundle is one built as a settlement candidate against a specific view of the chain. A reorg can invalidate a bundle before it settles (the settlement appendix has the recovery); the one that settles is the bundle whose view survived, and its proof is the bundle proof the settlement carries.
The wrappings, innermost out: one proof per transaction, compounded into one proof per block, compounded into the one proof the settlement carries.
flowchart TD
subgraph S["bundle proof"]
subgraph B1["batch proof"]
T1["tx proof"]
T2["tx proof"]
end
subgraph B2["batch proof"]
T3["tx proof"]
end
end
The word block needs pinning too. Execution does not deal with the blockdag’s parallel web; it consumes one sequence of blocks, the order consensus has already produced, and one block there is one step of execution: one witness set, one batch. How the dag collapses into that sequence, and what exactly counts as one block, is appendix material.
sequenceDiagram
participant U as User
participant OP as Operator
participant P as Provers
participant L1 as Kaspa L1
U->>L1: submit signed action to the lane (subnetwork)
U->>L1: deposit (output paying the covenant's deposit address)
Note over L1: blocks pass, actions, deposits and settlements interleave
L1-->>OP: witnesses: confirmed lane data, deposits, chain context
OP->>OP: execute actions + credit deposits, block by block
OP->>P: batch steps (state root X -> Y, lane tip, block context)
P-->>OP: batch proofs
OP->>P: compound into one proof up to block N
P-->>OP: bundle proof
OP->>L1: settlement: (new_state, new_lane_tip, block proof point N) + proof
Note over L1: confirmation window passes
L1-->>U: exit entitlements live, user claims via permission spend
State digests succeed each other
The program’s history, the full ordered record of everything the L2 did, is a sequence of state transitions, one per executed block. The machine writes them to its journal: every executed block produces one entry of this form, previous state root, new state root, previous lane tip, new lane tip, and the L1 context it saw. The lane entries a step consumed are the run between its two lane tips, every published action up to the new one; where the step credited deposits or emitted exits, their commitments ride the same entry (the deposit commitment is the deposit address the step credited, hashed, a constant under tt’s shared address but the carrier of real information under a per-user address policy; the exit commitment is the permission tree’s). A batch proof attests one such step; an aggregate proof compounds a run of them.
L1 never sees the journal directly, and does not need to: the journal is the proof’s claim, one of the inputs zk verification checks the receipt against, and the settlement script pins its on-chain numbers to the journal digest the receipt commits (The zkVM, briefly has the role it plays). What lands on L1 is one settlement per proved window, and a window spans a range of blocks: the settlement commits the final state digest of the range, the lane tip execution had read to, the block proof point, and, where the window emitted exits, the permission-tree commitment, all backed by the one proof that covers the whole range of blocks (The transaction vocabulary lists what each settlement carries). From the L1’s point of view, then, the program’s history is a chain of 32-byte state roots, one per settlement, and every step inside a window is provably reachable from the one before.
Why multiple L1 blocks?
A match takes minutes: moves arrive, deposits confirm, other users act, and the L1 keeps producing blocks the whole time. A proof covering a single block would be stale before it landed. Instead, each proof covers a window of L1 history (all lane entries and deposits up to a named block), and the settlement says so explicitly: this state is proven against L1 block N, this lane tip is included, and if you disagree, verify the proof. User actions submitted in block 3 and block 40 end up proven together into one settled state, with nothing in between silently dropped: the node’s lane commitments (KIP-21) give the lane one canonical history, and the proof binds its tip. Every block header carries a commitment to every active lane: one root over the tips of all active lanes, where each lane’s tip is a running hash folded over every entry the lane has ever carried. The tip is therefore a commitment value, not a bookmark: replay the lane’s entries through the hash and you land on it. That is what makes the check tight. The fold follows the chain’s own order, one selected-chain block (its absorbed mergeset included) at a time, the same order execution consumes (the settlement appendix defines one block). A batch guest consumes a run of entries and journals the two tips it moved between, so its claim is hash-bound to exactly those entries, and the settlement script requires the proof’s journal to commit the tip value the cited block’s header actually carries: the script derives this lane’s committed value from the journal’s lane tip and compares it with what the KIP-21 opcode serves for the cited block. Fabricate or skip one lane entry and the recomputed tip no longer matches the header, so a proof about a fabricated or stale L1 history cannot satisfy a node that follows the real chain (the settlement appendix has the mechanics, including why no range can be skipped).
The machine only cites blocks already behind the confirmation window, so a cited block always sits deeper than the settlement resting on it. A reorg deep enough to remove the cited block would also remove the settlement, and past that depth both are as permanent as any Kaspa payment. The commitment-reading opcode does enforce one bound of its own: it serves commitments only from a recent window of blocks, roughly the last twelve hours’ worth. The header check reads one block: the block proof point the settlement names (The transaction vocabulary defines it). Where the proving window started needs no header of its own; the previous settlement already pinned it. A machine stalled longer than that must first prove a window that reaches a recent block, then settle; the chaining shape makes that possible (the settlement appendix covers the recovery).
Deposits and exits use the same proof path
Deposits are L1 outputs, so they’re witnessed the same way lane actions are, and bound twice: the proof’s journal commits the deposit address it credited, and the settlement script re-derives that address from the covenant id and requires the proof to name exactly it. Exits flow the other way through the same path: when execution debits a user and emits an exit, the entitlement lands in the permission tree, and the settlement carries the tree’s commitment. One proof cycle carries the whole ledger of who-entered and who-may-leave.
Waiting for finality, and surviving reorgs
Kaspa orders blocks fast (roughly ten a second), but “a block” is not yet “a fact”. The machine follows the chain behind a confirmation window, a configurable number of confirmations, widened adaptively when the network looks reorg-prone, and treats a block as solid only inside it. If the chain reorganizes anyway, blocks the machine was watching simply vanish from its view as rollbacks: bundles that stood on the reorganized side are dropped and rebuilt against the surviving chain, and a settlement removed by a reorg before it is confirmed is simply resubmitted (the settlement appendix has the recovery detail, including which proving work gets reused). The settlement chain, immutable once Kaspa has it, is never reinterpreted.
Money that already moved follows Kaspa’s own rule. An exit claim is an ordinary Kaspa transaction; once it is buried as deep as any ordinary Kaspa payment requires, undoing it means rewriting Kaspa, not rewriting the rollup. The machine protects state; Kaspa’s proof-of-work depth protects payments, payouts included.
Reading one settlement
Look at a single settlement tx on an explorer. It names its covenant id. It commits the new state digest, the lane tip, and the block it proves to. Its first output can only be spent by the next settlement of the same covenant id, so the history cannot fork without splitting real money on L1. Where the bundle emitted exits, a permission output commits the tree: live, claimable entitlements (a bundle with no exits settles without one). And everything inside it, every move of every game, every deposit, every balance, is a 32-byte root away, verified by a proof anyone can check. What L1 does not enforce is freshness: nothing on-chain forces a settlement to advance the tip to today’s lane head; the tip moves when the operator settles, and an operator can stall (The machinery lists the stall cases). The rest of this book covers who runs the machine, what proves it, and what building on it is like.
The machinery
How it all chains showed how proofs tie the pieces together across L1 blocks. This chapter describes who runs what. Five roles cover the whole machine.
flowchart TB
BR["Bridge: watches confirmed L1, feeds witnesses from lane and deposits"]
EX["Executor: runs the program's rules over actions and deposits"]
PR["Provers: transaction -> batch -> aggregate proofs"]
ST["Settler: builds and submits settlement txs"]
DA["DA / index: serves program state to apps"]
BR --> EX --> PR --> ST
BR --> DA
The bridge is the machine’s view of L1. It follows the Kaspa chain behind the confirmation window and turns confirmed lane entries and deposits into the inputs execution consumes. Users submit to the lane themselves; the bridge only reads. Every fact the machine holds about L1 arrives through it.
The executor applies the program’s rules, the guest code, to the witnessed actions. It is deterministic: same inputs, same state, same result. What it computes is defined by the program, not by the executor.
The provers produce proofs of the execution, in three stages, one guest program each: the transaction guest proves one transaction, the batch guest compounds a block of those proofs, and the aggregator compounds batches into the single proof a settlement carries (The zkVM, briefly details the pipeline). Batch proofs chain within a bundle, and bundles chain across settlements, so the pipeline is a chain by construction.
The settler builds the settlement transaction (state digest, lane tip, proof, continuation and permission outputs) and submits it to Kaspa.
The DA/index layer is the read side: an operator can serve the program’s current state over an API so apps can query it without replaying proofs. Building an app on it builds on it.
In tt’s deployment these roles are in-process components of the single
ttd daemon (the framework’s reusable runner engine), plus the web app
reading the DA APIs. The roles are independent of the packaging: a
bigger deployment could scale each separately.
Who are you trusting, again?
With the roles named, the trust question from Based rollup on Kaspa gets concrete. The bridge can’t invent L1 facts; the proofs check everything against the real chain. The executor can’t cheat; its output is proven. The settler can’t settle a fabricated state; Kaspa verifies the proof before accepting the tx, and the settlement chain can’t fork without splitting real money on L1. What the operator can do is stop: stop executing, stop proving, stop settling, or keep settling against an old lane tip so your action is never processed. That is the liveness trust: safety needs no operator; liveness does, until someone else takes over.
Two properties make “someone else” possible. First, nothing in the machine is operator-keyed: the settlement script requires no signature, only a valid proof (a valid proof from anyone extends the chain), the bridge only reads public chain data, and the lane is public. So anyone can, in principle, stand up this same open stack against the same lane and continue where the last operator stopped. The limits of that claim: resuming means running a proving stack, real work at real cost (Building an app on it says who would pay), so it is a capability, not a service anyone promises. Second, exits already committed to the permission tree are claimable by their holders alone; no operator sits in that loop. The claim data travels the same way everything else does: the tree is rebuilt from public data, its leaves ride the proofs’ journals, and anyone running the stack reconstructs the identical tree, so withholding a branch means withholding L1 itself.
What is not shipped today is an escape hatch: a flow that lets a user force a settlement through without first running the machine. Until one exists, a balance that never became a committed exit waits for an operator. The practical advice: exit early if you plan to leave.
The zkVM, briefly
The engine under everything is a zero-knowledge virtual machine, a zkVM. This chapter is the minimum you need; it does not cover the cryptography.
A zkVM runs an ordinary program (normal instructions, normal toolchain), but alongside the result it produces a receipt: a small artifact that proves “this exact program produced this exact output record”. Anyone can verify the receipt in milliseconds, without re-running the program and without trusting whoever ran it. The key property: verification cost is nearly independent of execution cost. A program may run for an hour; its receipt still checks in milliseconds and is only a few kilobytes.
Verification takes three things: the receipt, the journal (the claimed output record), and the image id (the hash of the program binary). The input is not one of them: a receipt pairs with the image id and the journal and nothing else, so two runs with different inputs that produce the same journal look identical from the outside. That suits this machine: the proving host (the prover’s side outside the zkVM) assembles each guest’s input privately, and everything the settlement must pin travels in the journal. Given the three, the zkVM’s verifier answers one question, did this exact program produce this exact journal, and it answers by checking the mathematics inside the receipt, never by re-running the program. Underneath, that mathematics is polynomial arithmetic; this book stays at that level. One expectation to leave at the door: zero-knowledge here is verification technology, not privacy. Journals, lane entries, and program state are public data.
flowchart LR
E["guest execution"] --> Q{"proving mode"}
Q -- "dev mode" --> S["stub receipt<br/>(local demos only)"]
Q -- "real proving" --> R["zk proof<br/>(verifiable by anyone)"]
R --> V["L1-verified settlements"]
The journal: what the proof claims
The journal deserves a second look, because everything downstream hangs from it. It is the guest’s public output, its stdout, and it is one of the inputs zk verification checks the receipt against: a receipt does not verify against “this program ran somehow”, it verifies against this exact program and exactly this journal. That turns the journal into the proof’s claim. vprogs writes three of them: a transaction’s journal records what that one transaction did, the exits it emitted included; a batch journal states one transition, “from this previous state root, lane tip and L1 context, to this new one”; the bundle journal is the settlement’s claim; the aggregator verifies the batch proofs behind it and states the window’s endpoints. L1 never stores any of them. When the settlement script runs the zk-verify opcode (below), it hashes its own on-chain numbers into the journal digest the receipt must commit, so the proof only holds for exactly the values on the chain; and because the bundle journal is exactly those named values in one fixed layout, anyone holding the settlement can rebuild the digest, SHA-256 over the 352 canonical bytes (the field list), and check the receipt without asking the operator (How it all chains follows the chain of digests; the script side is the last section of this chapter).
What vprogs uses today
vprogs is built on RISC0, a zkVM for RISC-V programs. The proving pipeline is three guest programs, each an ordinary program binary (in RISC-V ELF form) with its own image id:
- the transaction guest is the application itself: tt’s runtime and rules, compiled with the framework’s runtime processor. It executes one transaction’s actions against the resource bytes they touch and produces the per-transaction proof; state roots exist only at the batch level (the state tree appendix draws the split).
- the batch guest verifies a block’s transaction receipts and compounds them into one proof per batch.
- the aggregator compounds a run of batch proofs into the single proof a settlement carries: the bundle proof.
How does one proof cover another? The zkVM exposes verification to the guest itself, so the batch guest checks each transaction receipt while it runs, and the aggregator checks each batch receipt the same way. Each level’s proof then covers the checks it did, which is how proofs nest.
Why a transaction tier at all? Parallelism and reorgs. Transactions with disjoint resources prove at the same time, independently; and when a reorg leaves the transactions’ order unchanged, their proofs are reused exactly as they are, with only the compounding redone (the settlement appendix tells the same story for bundles).
All three image ids are pinned at bootstrap, alongside the covenant id (The transaction vocabulary), and every proof names the exact images its guest ran, so “the rules” are never an ambiguous reference. One property of the pipeline matters for everything downstream: bundles prove in sequence, each on top of the last, because each bundle’s journal continues the previous one. That is what makes the digest chain a chain.
The sequence also shapes latency. A prover that keeps up stays a fixed distance behind the chain; one that cannot keep up falls behind without bound, so settlements wait and exits wait to be committed: the same liveness exposure The machinery names.
Recovery is catching up: a stalled machine proves a larger window that reaches a recent block, then settles again. The anchor-window limit and that recovery are the settlement appendix’s subject.
And where does verification happen? On-chain, in consensus. Kaspa’s script engine ships a zk-verify opcode (KIP-16, activated with Toccata, a Kaspa network upgrade); the settlement’s script calls it with the receipt and with the journal digest rebuilt from the settlement’s own script data, and every Kaspa node executing that transaction runs the check. The verifier identity (which image ids, which proof system) is baked into the covenant’s script hash, so “which rules am I trusting?” and “which address did I pay?” are the same question. And the receipt cannot be paired with mismatched claims. The script itself hashes the settlement’s named values (state digests, lane tip, block point, commitments) into the journal digest the receipt commits. A proof for one state simply fails against script data naming another.
Two modes matter in practice:
- Dev mode: the prover emits stub receipts instead of real proofs. Instant, free, and completely unsafe: fine for a local simnet demo, worthless on a public network. A dev covenant’s script omits the proof check, so on a public network its continuation is spendable by anyone, and no one would or should hold money in it.
- Real proving: actual cryptographic proofs, GPU-produced. tt’s testnet deployments settle with real proofs.
What may come
The zkVM field is new and still changing. vprogs’ proving stack sits behind a backend interface: execution, proving, and verification each sit behind one standard interface, and RISC0 is currently the one implementation behind them. That seam is what makes “another zkVM tomorrow” a migration rather than a rewrite. The interface exists today; a second implementation does not.
One more property: since the guest is an ordinary program, the program’s rules and its runtime ride inside the same proof. On smart-contract platforms that is precisely what you cannot do, which is the next chapter’s subject.
Solana: the real difference
If you know one smart-contract platform, this chapter is for you. If you don’t, the one-line version: there, the platform’s rules are fixed by the network and your program lives inside them; here, your program’s rules are the only rules, and the network just checks the proof. vprogs rollups and Solana look strikingly similar on the surface, and the one place they differ explains everything else about the design.
The similarities are real
Both systems share the same core shape:
| Solana | a vprogs rollup | |
|---|---|---|
| State shape | accounts, addressed by keys, holding data and lamports (Solana’s smallest unit) | resources, addressed by derived ids, holding data and balances |
| User intent | signed transactions naming programs and accounts | signed user actions naming targets in program state |
| Execution | a runtime validates and applies each transaction | a runtime validates and applies each action |
| Concurrency discipline | non-conflicting txs run in parallel across programs; a losing tx fails and is resubmitted | transactions that write disjoint resources execute and prove in parallel, across blocks too; actions inside one transaction always run in sequence, and contending transactions take the chain’s order; only the stitching, the batch and bundle proofs, is sequential by construction (How it all chains defines the tiers) |
| Money | native token, rent (a minimum-balance floor) on accounts | native KAS, fees and storage mass (an extra charge for creating small outputs, since every full node stores the unspent set; the Kaspa appendix has the formula) |
A Solana developer reading tt’s guest code will feel at home: there are user accounts with balances, locks authorizing spends, a minimum-balance floor on accounts, an index of “who owns what”. The similarity is deliberate.
The difference: whose runtime is it?
On Solana, the runtime is the chain. Sealevel (Solana’s parallel transaction scheduler), the account model, rent and fee rules, what makes a transaction well-formed: all of that is protocol, identical for every program, enforced by every validator, changeable only by a network upgrade. Programs run inside a runtime they cannot change: they orchestrate accounts, but the rules are set by the network.
In a vprogs rollup, the runtime is the program. tt’s guest literally
ships a runtime.rs, and inside the proof it is the only
runtime there is. Resource derivation lives in the app too: tt decides
its resource kinds, its hash domains, its id derivations (the framework
hands you the pattern; the app picks the keyspace). Transaction validity,
lock semantics, what a “turn timer” means: all rollup program logic,
executed and proved like any other line of guest code. There is no shared runtime; every program assembles its own from the
framework’s parts.
The claim needs a boundary, because it is smaller than it sounds. A chain’s hardest problems are agreement: what order transactions happened in, where the data lives, what counts as settled, what is final. The program re-solves none of them: it buys ordering, data availability, settlement, and finality from Kaspa unchanged (Based rollup on Kaspa to How it all chains). What moves into the program is the layer Solana fixes in protocol, the system program’s duties:
- Account creation and addressing. On Solana the system program creates accounts, and PDAs (program-derived addresses) hand programs control of derived accounts. Here derivation is app code: tt gives games their own keyspace, derived from the creator’s lock and a counter, with domain separation keeping keyspaces from colliding.
- The cost of holding state. Solana’s rent is protocol: holding state costs lamports, enforced by every validator. Nothing meters rollup state per block; state costs what proving and storage cost the operator, so the honest limit is how much you can afford to keep, not how much the protocol lets you. A program can still place floors as rules: tt’s minimum-balance floor and its withdrawal minimum are config the app chose, batteries rather than metering.
- Transaction well-formedness and balance. On Solana these are protocol rules, enforced by the runtime and the system program. Here they are program logic, proved with everything else.
This is a direct consequence of the zkVM: since the guest is an ordinary program, whatever it computes about its own rules is covered by the same proof that covers the rules themselves. On a chain, the runtime must be fixed because every validator must agree on it before running your code. In a rollup, agreement comes from the receipt, so the runtime can be as application-specific as the application.
What it buys
- Resource derivation as a design surface. The keyspace pattern above is plain code: the app picks any derivation it can compute, and the proof pins the result.
- Rules that can be arbitrary. A turn timer that forfeits a round, a pot that splits on a draw: no need to fit these into a shared execution model, because there is no shared execution model.
- New rules, new identity. “The protocol” is the guest ELF; changing the rules is changing the program (covenant ids pin image ids precisely so this is explicit: a new rules version is a new identity, not a surprise). The money path is explicit: a new image id is a new covenant id, so an upgrade means moving to a new instance: users exit through the old instance’s permission tree and deposit into the new one. In-place migration does not ship, and draining the old instance still needs someone to keep its stack settling; an instance everyone is leaving is the least likely to keep one, which is The machinery’s liveness trust at its sharpest.
What it costs
- No free composability. Solana programs share one state machine, so one program can call another atomically. Each vprogs program is its own proved system; cross-program calls are future work, sketched but not shipped (Single sovereign apps lives inside this limitation).
- The runtime is your responsibility. Nobody else guarantees your
rules make sense. The framework’s batteries (locks, unlockers, signers)
are the strong default, and
runtime.rs’s job is largely assembling them rather than inventing from zero. But the choice, and the consequences, belong to the program.
The Ethereum reader’s checklist
The Solana comparison above covers accounts and runtimes; the reader from EVM rollups has a different checklist, and the answers are short:
| The question | Here |
|---|---|
| Validity or fraud proofs? | Validity: every settlement carries a zk proof every Kaspa node checks |
| Forced inclusion? | Data inclusion is permissionless (the lane has no gatekeeper); forced processing is not shipped: nothing forces the tip to advance, it moves when someone settles (The machinery) |
| Sequencer failure? | There is no sequencer; whoever settles picks how far the tip moves, and anyone with a valid proof can settle |
| Exit latency? | To entitlement: confirmation window plus proving plus L1 inclusion; to funds: plus the claim transaction’s own confirmations (How it all chains); no measured numbers published yet |
| Data availability? | The lane on Kaspa itself: public, consensus-ordered, its commitments verifiable from any node’s retained headers; entry data older than the pruning window needs an archival node (the Kaspa appendix) |
| Preconfirmations? | None as a protocol promise; anyone can run a node in execution mode, replay the lane, and read the pending state, anchored to the latest settlement |
In Solana the runtime belongs to the network; in a vprogs rollup it belongs to the program, assembled from framework parts and covered by the proof, on top of an L1 that still owns ordering, data availability, and settlement.
Building an app on it
Everything so far has been the machine. This chapter is the view from the other side: you want to build an app on a vprogs rollup. What do you get?
The pattern is one sentence: signed actions in, indexed state out.
The write side
Your app’s users hold keys. Everything a user does is a signed action, and building one is pure, local computation: take the action, encode it with the program’s wire format, sign it with the user’s key. tt compiles its guest wire library to WebAssembly so the browser can do exactly this: the same encoders the zkVM verifies, running in the page, producing signed actions with zero servers involved. The wallet never asks anyone’s permission to construct a transaction; the L1 carries it, and whether it holds is decided at proof time by the program’s own rules.
The read side: indexes
Actions go in; how does your UI read the current board, the user’s balance, the open games? Replaying proofs is too heavy for a frontend. So the operator runs a DA/index layer: a service that follows the program’s execution and serves queries over the resulting state. tt’s node exposes exactly the API a tic-tac-toe app needs:
| Endpoint | What the app reads |
|---|---|
GET /api/state | program state summary (current digest, settlement info) |
GET /api/config | program config (stakes, turn timer, withdrawal minimum) |
GET /api/games?status=… | game list, filtered and paginated |
GET /api/games/:id | one game: board, players, stake, status |
GET /api/accounts/:id | one account: balance, lock |
GET /api/exits | exit entitlements waiting to be claimed on L1 |
The web app is then a perfectly ordinary frontend: fetch state, render a board, post signed actions. There is no Anchor-style IDL or generated client yet: the WASM wire library is the client SDK, and the endpoints above are hand-written. All the exotic machinery from Based rollup on Kaspa through The machinery is behind two habits: sign locally, read the index.
Who pays for what
Users pay ordinary Kaspa fees for their own lane actions and deposits; each action rides a normal transaction funded from the user’s own UTXOs, and an exit claim is likewise an ordinary transaction, built by whoever claims (the user, a subsidized operator, a claims service), its fee carried by one of the builder’s own coins (the settlement appendix has the shape). The operator pays the settlement transactions’ fees and the proving compute. tt itself charges nothing inside the program today; an in-program fee model (debiting accounts to fund the operator) is a battery a real deployment would place, not something the shape forces or forbids.
Spam has a partial answer: anyone can publish garbage actions to the lane, but garbage pays its own L1 fees, and the program rejects it at near-zero execution cost. The rest of the answer is cost: garbage bytes still ride the batch into the proving pipeline, so volume spam burns operator proving cycles until the fee battery is placed.
There is no pre-proof filter, deliberately: whoever filters decides what counts as garbage, and the lane’s promise is that inclusion is not anyone’s decision. Skipping an entry cannot hide; it shows up as a stalled lane tip, The machinery’s stall, visible rather than silent. Running the stack is pure cost in tt, so the “someone will resume it” story depends on enthusiasm until that battery is placed.
Indexes are convenience, not authority
This section is the difference between an index and a trusted backend. The money is not in the index. The money is in the proof-chained state on Kaspa, and every exit goes through the permission tree that the settlement chain itself commits. The index can be wrong, hostile, or down; it cannot move funds itself. What it can do is induce you to sign: your wallet builds actions from what it can see, so a lying read costs you exactly what the resulting action can lose under the program’s rules. For tt that bound is tight: a fake board produces a move that fails at proof time, or at worst loses one game’s stake. For an app whose actions trade on state, a swap against reserves the index misreported, the induced action is valid, executes against the true state, and the loss is yours. The defense is two-sided. The program’s half: actions should carry their own bounds, a swap its minimum received, every action its deadline in DAA score, so an action built on bad data fails at proof time instead of executing a loss. The reader’s half: treat the index as a claim and keep a way to check it, which is the ladder below. The endpoints are a performance optimization, not the source of truth.
How far can you verify a read?
First, what an account is: a resource id plus its state bytes. The whole state is one sparse Merkle tree, the id is the key, the leaf commits the id and the hash of the bytes, and the settled state digest on L1 is the root. Every read verification walks the same chain of anchors, and how far you can walk it depends on what you run:
| What you run or trust | What you verify | Against what |
|---|---|---|
| Nothing (the operator’s index, the default app path) | Display only; the board it shows is a claim | Nothing today; the next settlement publishes the digest, but checking an account against it still needs the path that does not ship (below) |
| An L1 node, your own or an explorer’s | The digest chain: each settlement names the digest it moved to, and Kaspa consensus checked the proof binding it | L1 itself |
| Your own L2 node in execution mode (replay without proving) | The full state: it replays the public lane and takes nothing from the operator | L1, re-executed |
An indexer is a view over one of these nodes; it inherits whatever that node is worth trusting. Your own indexer against the operator’s node buys nicer queries, not independence. One more check the shape allows: the settlement’s receipt is public data on L1 and verifies in milliseconds against the pinned image ids (The zkVM, briefly); consensus runs that check on every settlement, so running it yourself matters only if you do not process the chain yourself.
In the game’s terms: the index shows your opponent’s move as a new board. Checking it means hashing that board, checking the hash is the leaf at the game’s resource id, and checking the resulting root against the settled digest. That middle step is the one the shape allows but no API serves today: tt hands out account bytes, not Merkle paths, while the proving host (the prover’s side outside the zkVM) loads exactly those paths for every account a batch touches. The state tree appendix has the walk, and where the bytes behind the leaves live. A purpose-built light client (a small program that verifies only the data it needs) doesn’t ship today either; re-execution does.
One gap no endpoint closes, only time or re-execution: state between settlements. The last settlement pins a digest; everything after it, up to the lane tip the operator claims, is unproved. Either wait for the next settlement, whose proof must explain the gap, or replay the suffix yourself.
That inversion, reads untrusted and writes self-certifying, is what keeps the app layer simple.
Single sovereign apps
Solana: the real difference named the costs; this chapter measures them. What is a vprogs program today, and what isn’t it yet?
One covenant id, one instance
Each program instance is one covenant id: one lane, one state tree, one settlement chain, one set of pinned guest images, one operator stack. Every proof in the system names its covenant id and settles into it; the identity is part of the settlement journal itself. The consequence:
A vprogs program is a sovereign app. It sets its own rules, state, and exits, and shares nothing with any other program. At the current stage there is no mechanism by which program A reads or calls program B’s state. Composability, the property that makes a smart-contract chain feel like one financial system, does not exist here yet. Each app is isolated, with its own deposits and exits.
What that means in practice
- You can build tt. A staked game with accounts, transfers, withdrawals, exits, a web frontend, live on a testnet with real proofs. Any application whose state fits inside one program (games, escrows, marketplaces where the market is the program) fits the shape.
- You cannot build “the DeFi stack” as one system. Lending here, DEX there, sharing balances atomically: that requires either one monolithic program (which works, but then composability is just internal function calls) or cross-program mechanisms the shape doesn’t ship yet.
- Trust is per-app. Each program has its own operator liveness and its own rules; using three programs means three of each. There is no global validator set behind them, by design, since the L1 never runs the programs.
Why start here?
Because a self-contained program is cheap to prove and audit. A program that owns everything it touches can be proved end-to-end with one proof chain, exited through one permission tree, audited as one artifact. The hard problems vprogs solves first (proof chaining across blocks, reorg survival, trustless exits) are exactly the problems an isolated app needs solved. Shared-state mechanisms can build on that foundation; built before it, they have nothing to build on.
Where it goes from here, multi-program systems and cross-covenant calls, is future work with design behind it: a yellow paper sketches composability, cross-program invocation between instances, and none of it is implemented yet. The current shape is a deliberate first step.
For now, the mental model to take away: vprogs today lets you stand up a small, self-contained chain for a single application, based on Kaspa. One app, one covenant id, one proof chain, no shared runtime, and no new coin: the money is Kaspa’s KAS end to end, only the rules are the app’s.
Where things stand
This chapter summarizes what exists, what does not, and what it costs.
What is real today
- The framework. vprogs (the lane/bridge machinery, the three-stage proving pipeline, the settlement construction, the permission-tree exits, the daemon engine that runs it all) exists as working code, exercised by automated end-to-end tests.
- A complete application. tt is not a toy snippet: a RISC0 guest with its own runtime and rules, a node with DA APIs, a scripted scenario driver, a browser wallet signing with the same wire library the zkVM verifies. The full loop (deposit, play, settle, exit) runs locally in minutes against an in-process simulated Kaspa network (a simnet).
- Testnet with real proofs. The same stack has run end-to-end on Kaspa testnet-10: a fresh covenant id, a full match, real GPU-produced proofs settling on the public testnet, exits claimed. The KIP-16, KIP-20, and KIP-21 extensions it depends on are live on Kaspa mainnet; the deployment sits on the public testnet as a demonstration choice (Based rollup on Kaspa gives the details). Dev-mode stub receipts are strictly for the local demo; the testnet deployments prove for real.
- Operational hardening. The machinery survives restarts, reorgs, and pruned nodes (nodes that have discarded old block data); bootstrap, resume, and catch-up start modes exist because operations demanded them.
What it is not yet
- Production. The project’s own status: early development / prototype phase; APIs and architecture may change significantly.
- Multi-program. As Single sovereign apps said: single sovereign apps, at the current stage. Composability is future work with a yellow paper behind it; nothing is implemented yet.
- Multi-zkVM. RISC0 today; the backend seam exists (The zkVM, briefly), the migrations don’t yet.
What it costs, and how long it takes
No published numbers yet, and this book won’t invent them. What is structural: every user action is an ordinary Kaspa transaction (user-paid, included at L1 speed); settlement latency is the confirmation window plus proving plus L1 inclusion; exit claims add their own confirmations. The window is a deployment choice, widened adaptively when the network looks reorg-prone. Measured end-to-end figures belong in the runbooks and will be added there when they exist.
Where to follow and join
The two repositories are the source of truth; code, runbooks, and issues live there: vprogs (the framework) and vprog-tictactoe (the example application and its deployment runbooks, including the multi-machine testnet walkthrough). There is no token and no foundation; at this stage the repositories are the project.
Closing
Return to the opening setup: two strangers, a pot of Kaspa, a game with no casino and no escrow agent. The machine behind it is now fully described: signed actions into a public lane; execution by rules the program defines and a zkVM proves; state as a digest chain anchored settlement by settlement into Kaspa; money out through exits that, once committed, no operator can withhold (until committed, The machinery’s stalls apply: robbery and delay are different failures, and only the first is solved). The rollup fixes the shape; the program picks the rules; the L1 holds the money. If the live demo is running next door, lose a game knowing why the pot cannot be stolen, and what has to keep running so you can leave.
Appendix: settlement and exit mechanics
The transaction vocabulary kept the settlement and the exit to what they attest and pay, and How it all chains kept the block model to its simplified form; this page carries the mechanics a careful reader asks about next. Nothing here changes the design; it is the same machine, closer up.
What counts as one block
How it all chains treats one selected-chain block as one block to the machine. The full rule: a block on the selected chain absorbs the parallel blocks consensus folds into it, its mergeset, and the transactions those parallel blocks carry count as part of that step, provided they were not counted in an earlier step. One chain block plus its mergeset’s uncounted transactions is one block to the machine: one witness set, one batch, one step of execution. The dag’s parallelism is flattened before execution begins, which is why the rest of the book can speak of a plain sequence of blocks. The machine never deals with the parallel blocks directly: its attention is the selected chain, and each selected block arrives with the transactions Kaspa’s ordering assigned to it. Which parallel block’s transactions ride which selected block is the chain’s own assignment, identical at every node; the machine consumes the result.
Snapshot claims, chained
A settlement records one verified snapshot: at block Y, the state digest was D. The proof inside shows how the digest got there, root by root, from block X to block Y, where X is the previous settlement’s proof point. The windows are contiguous, each proof’s journal continuing exactly where the last one ended, so no block range goes unwitnessed. The settlement transaction itself lands at least one block after Y, and sometimes later: the gap is proving time plus the confirmation window the cited blocks must pass, before the settlement is even submitted. Each settlement commits one more provable snapshot.
Can a range be skipped?
No. Each proof’s journal picks up exactly where the previous one ended, at its lane tip, and the settlement names the tip it proves to. Skip a stretch of the lane and the two tips no longer meet: the next proof would claim a tip it never reached. The bind is checked on-chain: Kaspa’s block headers carry a commitment to every active lane (KIP-21), and the settlement script requires the proof’s journal to commit the same value the chain carries for the block it names. A skipped range fails that check, so a settlement cannot leave a hole.
What if cited blocks reorganize?
The proving pipeline has tiers (The zkVM, briefly lists the three guests), and a reorg invalidates only what stood on the reorganized side. Bundles whose proving base was rolled back are thrown away and rebuilt against the surviving chain, reusing the lower-tier proofs that still chain; a settlement removed by a reorg before it is confirmed is simply resubmitted. Proof cancellation is the pipeline dropping work on bundles whose base was rolled back: the bundle that would have stood on the losing side is not generated, and one already in flight is abandoned, keeping its still-valid lower-tier proofs for reuse. The aggregate prover ships this. This is rare by construction: the machine only cites blocks already behind its confirmation window (How it all chains).
The anchor window and the long stall
The opcode that reads lane commitments (KIP-21’s, a different one from the KIP-16 opcode that verifies proofs) has a reach limit: it serves commitments only from a recent window of blocks, roughly the last twelve hours’ worth, so a settlement cannot cite a block proof point older than that. A machine stalled longer than that must first prove its way forward to a recent block, then settle. The chaining shape is what makes that recovery possible: a proof window can span many blocks, so the backlog drains in larger windows, at the price of later snapshots.
An exit claim, up close
The transaction vocabulary kept exits to the leaf and the payout; here is the claim transaction itself. It pulls in the commitment UTXO, up to eight coins from the program’s deposit pile (the claim wallet, the builder-side tool, picks which; the cap of eight is protocol, not wallet policy: the script unrolls its per-input checks, so the cap sizes the script, and the script’s hash is the on-chain commitment), and one ordinary coin of the builder’s own to carry the fee. The deduct is the builder’s choice, any amount up to the leaf’s whole value; the swept coins must cover it exactly, and the unlock data names it. The network’s relay floors apply to the claim’s outputs like any transaction’s; tt’s withdrawal minimum is the app-level guard upstream, not script law. The outputs, in the order the script pins them: output 0 is the payout, the deducted value paid to the lock the leaf names; output 1 is the fresh commitment with the paid part removed, serving the next claimant; then the pile’s untouched change, re-locked at the same deposit lock for the remaining claimants; then the unspent part of the fee coin back. The commitment is a self-updating UTXO: each claim spends it and hands the next claimant a tree with less left to claim.
flowchart LR
A["the commitment<br/>(this settlement's exits)"] --> C["one claim transaction"]
B["up to 8 coins from<br/>the deposit pile"] --> C
F["one of the claimer's own coins<br/>(carries the fee)"] --> C
C --> P["payment to the claimer"]
C --> N["new commitment,<br/>this leaf deducted"]
C --> R["pile change, re-locked"]
C --> X["fee-payer's change"]
What the script itself checks: the commitment’s redeem script embeds the tree’s root and the unclaimed-leaf count, and the unlock data supplies the leaf, its branch of sibling hashes, and the deduct. The script requires the deduct to be positive and no more than the leaf’s amount, requires output 0 to pay the leaf’s destination exactly the deduct, and requires the leaf hash, folded up the branch, to equal the embedded root. A leaf hashes as SHA-256 over a leaf tag, the destination’s script bytes, and the amount; a branch node as SHA-256 over a branch tag and its two children (definitions). The handover is also script work: the same branch folded around the reduced leaf gives the next root, and the script takes its own template bytes, as revealed in the claim’s unlock data, prefixed with the new root and count, hashes them into the next commitment’s script hash, and requires output 1 to be exactly that, with the commitment’s own value passing through unchanged. The count rides along for the endgame: it drops by one whenever a deduct takes its leaf to zero, a whole-leaf claim or the last sliver of a partial series, and reaching zero is what tells the script the tree is done. While any leaf remains, the payout is exactly the deduct; when the last leaf empties there is no output 1 left to carry the commitment’s value, so the script folds it into the final payout instead (output 0 becomes the deduct plus the commitment’s own value, and with no continuation the later outputs shift up: the pile change, when there is one, lands at index 1, the fee change after it). The deposit coins are conserved exactly, their sum must equal the payout plus the pile change, and they may never burn: the fee comes only from the claimer’s collateral coin, whose unburned remainder returns as the trailing change output, so swept value cannot ride out anywhere but the payout and the pile (the full phase list).
Two background facts close the money flow. First, the commitment’s own value: the settlement that emits exits funds the permission output from the settler’s own wallet, alongside the fee; the amount is a small constant pinned in the covenant script, there to keep the commitment output above the network’s output floors (a commitment below them could not be spent and would strand the tree), and none of it is program money. Every claim passes it through unchanged, and the final claim folds it into the payout; the fold is the mechanism closing the tree, not a bounty. Second, what the pile coins are locked with: each is a deposit that paid the program’s deposit address, a P2SH whose redeem script derives from the covenant id alone and demands that the spending transaction itself run under that same covenant (a covenant transaction: Kaspa’s covenant rules, KIP-20, let a script refuse to run outside transactions bound to its covenant id, and consensus enforces the binding; the glossary’s covenant entry has it). No key ever unlocks a deposit; only the machine’s own covenant transactions can move one, and the claim script does not take its funding on faith: it rebuilds the expected lock bytes from its own covenant id on-chain and counts only inputs that match, so coins from another program or a plain wallet output cannot sneak in as funding.
And the tree’s build site: each transaction’s proof journals the exits it emitted, and the aggregator guest, while proving the bundle, replays them from those journals in canonical order (journal order within a transaction, transaction order within a batch, batch order within the bundle, accumulator), into the one tree whose root the settlement carries.
Who builds a claim is economics, not protocol. The user can build it and pay the fee; anyone can offer claims as a service; an operator who benefits from working exits can subsidize them. The script accepts a valid claim from whoever submits one.
Exit contention and pile maintenance
Claims against the same commitment are spends of one coin, so they conflict: two claims racing in the mempool behave like any double-spend. One wins; the other’s transaction can no longer confirm (its input is gone), and the wallet rebuilds it against the new commitment once it sees the winner. A reorg plays the same card from the other side: a claim that confirmed against a commitment output the reorg removes goes with it (its input is gone) and is rebuilt against the surviving settlement’s commitment. Claims against different settlements’ commitments are independent and pay out in parallel. The machine watches landed claims to update its own books, marking exits claimed in program state, not as an additional safety check, and a malformed claim fails its own validation without blocking later claims. What does not ship is a sweeper for the pile itself: every claim splits what it sweeps, the network’s dust rules floor how small the pieces can get, and keeping the pile in spendable coins is operational work.
Appendix: Kaspa details behind the simplifications
The glossary simplifies four Kaspa mechanisms: how the blockdag’s web becomes one order, what the two depth counters count, what storage mass charges for, and how long the network keeps full block data. This page carries the full versions. Nothing here changes the design; the machine reads the same L1 either way.
From web to one order
Every Kaspa block names one selected parent, the block it builds on, plus any further parents it merges. The consensus rule (GHOSTDAG) sorts each block’s mergeset into blue blocks, the ones well connected to the selected-parent chain, and red ones, which stay in history but order outside the blue set. The split orders blocks; it drops no transactions: a red block’s transactions ride the selected-chain block that absorbs it, as the settlement appendix’s rule says. Walking selected parents from a tip back to genesis gives the selected chain, the line the rest of the book executes along. A block on that line absorbs its mergeset’s transactions into the same execution step; the settlement appendix spells out what counts as one block.
Blue score: how deep is this block
A block’s blue score is the number of blue blocks in its past: its selected parent’s blue score plus the blue blocks in its own mergeset (the network’s own wording). It is the blockdag’s version of block height, and it is the counter Kaspa counts confirmations in: depth is measured as blue-score distance from the including block toward the current tip. Blue score only moves forward; a reorg replaces a stretch of the order, it does not rewind the counter.
DAA score: how much work is in the past
A block’s DAA score (difficulty adjustment algorithm) is its selected parent’s DAA score plus the mergeset blocks that are not far behind the chain: blue blocks and recent red ones. Deeply red blocks, mined well behind the selected chain, are excluded, so a flood of badly connected blocks cannot inflate the counter. The network reads mining difficulty from a window of blocks chosen by DAA score, and the emission schedule, including the reward halvings, is defined over DAA score. Both counters ride every block header and inherit each block’s history through its parents, so both only move forward; that is what lets the program treat DAA score as a clock (The transaction vocabulary). The per-block context the program reads inside each proof window, timestamp, DAA score, blue score, is committed by the chain itself (KIP-21).
Storage mass: paying for the unspent set
A full node keeps two things forever and one thing briefly. The unspent transaction output set, one entry per output created and not yet spent: forever, it is the state every node must consult to validate. Full block data, transactions with their bodies: only for a bounded recent window on ordinary (pruned) nodes. And the old chain itself, headers with their commitments: ordinary nodes keep the parts consensus still needs, the chains the pruning proof covers and the recent window, and delete the rest; a node joining late crosses the gap with a proof-of-work-checked pruning proof instead of validating from genesis. Archival nodes keep everything, headers included.
That asymmetry is why outputs cost. A transaction’s mass, the weight its fee is priced on, is the larger of two terms (KIP-9): compute mass, covering verification work, and storage mass, covering the unspent-set growth the transaction leaves behind. Storage mass rises when a transaction splits value into many small outputs and is offset when it consolidates small inputs: dust is expensive to create and cheap to sweep. KIP-9 defines a strict and a relaxed storage formula and the network runs the relaxed one: the storage term is a constant times the positive part of the sum of reciprocals of output values minus the sum of reciprocals of input values; the constant and the integer rounding rules live in the KIP. The rule exists because a dust attack once used Kaspa’s throughput to grow every full node’s unspent set permanently; minimum-relay rules (the glossary’s dust entry) floor how small an output may be, and storage mass prices the splitting itself. Both limits meet in this book’s deposit pile: an exit claim that splits the pile can divide it only as far as relay rules and storage mass allow.
Reconstructing the program from L1
What the pruning asymmetry means for the rollup, as one ladder. A node that has followed the chain since the program deployed watched every header and body itself, so it can verify every commitment and replay every lane entry without trusting anyone; to keep that property forever it must keep the data, or know an archival peer. A node joining later starts from a pruning proof: headers it can trust by proof of work, commitments included, as far back as the retained chains reach. KIP-21 deliberately bounds the active-lane commitment set to sit inside that reach, so a fresh node can always verify the current lane commitments. Replaying lane entries older than the pruning window, say to rebuild state from deployment, needs block bodies, which by then only archival nodes serve. The machine meets this reality with three start modes: bootstrap funds a new covenant’s first output; resume continues your own instance from its persisted identity; catch-up joins an existing covenant by replaying from its deploy block (a nearer seed would miss lane history the state already absorbed, so the deploy block is required, not a convenience). And the archival data authenticates the same way everything here does: every block’s content is committed by its header, and headers verify by proof of work, so an archival peer cannot serve you a forged past, only withhold it (Where things stand).
Appendix: the state tree
Building an app on it hands out the chain of anchors in one line: resource id, leaf, root, settled digest. This page is the tree itself: its shape, where the bytes live, how proving uses it, and the walk that checks a single account against L1.
Shape
The whole program state is one sparse Merkle tree of depth 256. The key is the resource id, 32 bytes, one bit of it per level, so every possible account has one fixed position; empty branches collapse into a default hash, which keeps the tree as small as the live state while every account keeps its spot. The leaf commits the id together with the hash of the resource’s bytes (not the bytes themselves), which binds each value to its position, and a hash of empty bytes marks deletion. The tree is versioned: a state change is a set of (key, new value hash) pairs applied at a version, and the state digest the settlements carry is the root that results. Versions keep past roots reachable, so a prover can prove at the exact state a batch saw, not only at the latest tip. Sparse plus depth-256 is also what makes a proof short: proving one leaf is a path of at most 256 branch hashes, and the proving host batches the accounts one execution touches into a single multiproof over all of them.
Where the bytes live
The bytes behind the leaves are not on L1; L1 holds only each root. The executing node keeps them, and the tree, in its own database (a RocksDB store with a column family per kind: resource data, tree nodes, version pointers). The store is a cache with a proof: every input execution consumed, the actions in the lane, the deposits, the block context, is public on Kaspa, so replaying them reproduces the same tree, root for root. A rebuild seeds at the covenant’s deployment, not at a recent settlement: a digest is 32 bytes and holds no state, so rebuilding means replaying inputs, and the whole history is the witness; a seed near the tip has no inputs before it.
Proving with the tree
The guests meet the tree at two levels. The transaction guest works on whole resources: it receives the full bytes of every account its transaction touches, checks the program’s rules over them, and commits the new bytes’ hashes. Merkle paths enter one level up, at the batch guest. The proving host reads from its store a multiproof covering exactly the resources the batch touches and passes it in as private input (input the receipt never publishes; what the receipt commits is the result, the two roots); the batch guest checks the proof’s keys are strictly ordered, checks that every resource the transactions touched is accounted for in it, and recomputes both roots, before and after, in one walk over the witness. That walk is also where the before-bytes are authenticated. A receipt does not bind its input, so a host could in principle hand a transaction guest fabricated current bytes; but the multiproof commits every touched account’s before-hash under the previous root, and that root is the journal’s prev_state, asserted equal to the prior batch’s new_state and pinned on L1 by the settlement chain. Fabricated before-bytes hash to a different prev_state and the chain of assertions breaks. The unbound input is safe because nothing trusts it: everything the guests consumed is re-derived against committed roots inside the proof stack. Those two roots leave the proof as the journal’s prev_state and new_state; the aggregator asserts each batch’s prev_state equals the previous batch’s new_state, and the settlement script pins the same chaining on L1. After proving, the host checks its own store’s root equals the root the guest committed, so a broken store cannot quietly produce valid proofs.
Checking one account against L1
The verification Building an app on it sketches, as a walk:
- Read the settled digest from L1. The settlement’s own data names the digest it moved to, and its continuation output locks that digest into the next link.
- Get the account’s resource id, its bytes, and the Merkle path from its leaf to the root. tt’s node serves the bytes today; it does not serve the path.
- Hash the bytes, hash them into the leaf with the id, walk the path, and compare the root you get with the digest from step 1.
The proving host runs this same walk for every touched account on every batch, so every piece exists; the missing part is the endpoint. Nothing in the check is operator-specific: the path either hashes to the settled root or it does not. What the walk cannot cover is state newer than the last settlement; that gap is Building an app on it’s, and only the next settlement’s proof or your own re-execution closes it.