Protocol
Why the chain is built this way. Several obvious-looking simplifications here are load-bearing, the failure modes show what happened when they were not respected.
Shielded state transition
Orchard's note-commitment tree is append-only and position-dependent, so on a BlockDAG the append sequence must be a pure function of consensus order:
coinbase mint first, then accepted actions in consensus applied order
Any other order produces a different root from identical data. This is the whole argument, ordering is not an implementation detail here, it is the data.
Each block's coinbase commits the parent's shielded state root:
state_root = tree anchor + nullifier accumulator + turnstile totals + burn root
Because that commitment is inside the coinbase, it is PoW-anchored: a state divergence surfaces as a block rejection rather than silent drift. There is no circularity, a block commits its parent, never itself.
Supply integrity
- Per bundle (cryptographic)
- the binding signature verifies under
bvk = Σ cv_net − commit(value_balance, 0), which is valid only if the bundle's true net value equals its declared publicvalue_balance. Only the coinbase may mint - Per block (public integers)
pool = cumulative_coinbase − cumulative_fees − burns, which may never go negative (PoolUnderflow)
Both are auditable from a plain node: GetShieldedSupply, or /info/shielded over the REST API. Today turnstileOut is zero, nothing has ever left the pool.
Anchors and liveness
A spend proves a Merkle path to a past root. It is accepted only if that root is known, its producing block is a selected-chain ancestor, and it sits inside the maturity window:
- Multi-producer anchors. Several blocks can produce the same root, so all producers are retained and any matured canonical one suffices. Activated at DAA 757,000.
- Drop, do not disqualify. A transaction whose anchor is stale or whose nullifier is already claimed is dropped from the state transition, not fatal to the block. Liveness survives a builder that mines a stale transaction.
- Applied fees only. The coinbase must be built from fees that were actually applied, otherwise a dropped spend's fee creates unbacked supply. This was a real bug; see failure modes.
Consequence for wallets: a note is not spendable the moment it arrives, and the mempool guard is relay policy, best-effort and one-directional. A builder can still mine a transaction that consensus then drops, harmless, funds intact, nullifiers not recorded, fee not collected.
Trusting peer history
cmx from genesis must reproduce exactly the frontier a node already holds, a value it never learned from the serving peer. A peer that fabricates, omits, reorders or truncates cannot match it.This is why a fresh node can take shielded history from any peer without trusting it, and why a pruned node can serve complete wallet balances from its first sync onward. The verify pass sets the history_complete flag; an offline import does not set it.
Merged-mining economics
Both chains use identical PoW, so hashrate = 2 × difficulty × BPS. Kaspa mints 10 blocks/s to ZKas's 1, which produces one governing relationship:
ZKas per-block work > Kaspa per-block work ⟺ Hz / Hk > 10%
Measured: ZKas ≈ 50 PH/s at ~1.0 blocks/s; Kaspa ≈ 357 PH/s ⇒ ratio ≈ 14%, so a ZKas block currently costs ~1.4× a Kaspa block. That is not an anomaly, it is what a 10:1 block-rate ratio does at a 14% hashrate share.
Kaspa-visibility of ZKas blocks
coverage = min(1, 10 × Hz / Hk)
A ZKas block lands in Kaspa's DAG carrying its tag only if it also cleared Kaspa's target. At 14% coverage is 100%; if Kaspa doubles while ZKas stays flat it falls to ~70%. This is a data-availability property for anything that anchors to Kaspa, and it degrades silently.
What adding hashrate actually does
| Scenario | Effect on KAS income | Effect on ZKAS income |
|---|---|---|
| An existing Kaspa pool enables merge-mining | 0.0%, no hashrate joined Kaspa | every existing ZKas miner is diluted by the new share |
| New hashrate joins both chains | diluted in proportion | diluted in proportion |
| A large merge-miner leaves | unchanged | remaining miners gain, after a ~44 min retarget lag |
- Total ZKAS/day never changes. Emission is fixed per block and the DAA restores 1 BPS, so new hashrate dilutes, it never inflates.
- There is no shutdown price. Marginal cost of the ZKas leg is zero, so difficulty can rise arbitrarily without making any merge-miner unprofitable. The only exit trigger is ZKAS/day falling below a fixed ops cost.
- Exits hurt more than entries. Difficulty retargets over ~44 minutes, so an abrupt exit strands the chain at too-high difficulty and stretches block times until it corrects.
Kaspa interface (Toccata)
Relevant if you want Kaspa-side logic to verify ZKas state. Live on Kaspa mainnet since DAA 474,165,565.
OpZkPrecompile0xa6- Groth16/BN254 at 140,000 grams, or risc0 succinct STARK at 250,000. Block compute limit is 500,000 ⇒ ~3 Groth16 verifies per Kaspa block, so proofs must batch. This ceiling does not bind the merged-mining tag reader — see below
OpChainblockSeqCommit0xd4- returns a chain block's sequencing commitment; selected chain only, within ~12 hours
- KIP-20 / KIP-17
- covenant lineage ids; transaction introspection
- KIP-21
- commits every mergeset block's raw coinbase payload plus hash and blue_work under
SeqCommit(B), this is how a covenant reaches a merged-mining tag. Leaf =H(block_hash ‖ blue_work ‖ H(coinbase_payload)) OpBlake3WithKey0xda- keyed hashing
- Not available
- no opcode reads a coinbase directly; no Halo2 verification, so Orchard proofs must be wrapped into Groth16 or risc0; risc0 pinned to 3.0.4
OpChainblockSeqCommit, then byte-offset parsing — pure hashing, no OpZkPrecompile. Only statements about ZKas internal state spend the budget. Two facts sit in those same bytes: the miner’s payout script is at a fixed offset immediately before the tag, so every tag is attributable and blue_work-weighted; and the magic must appear exactly once, so the Kaspa→ZKas map is 1:1 and non-repudiable. The tag still proves publication, never validity.This asymmetry is why rich shared-state DeFi has to live beside the chain rather than on it, see what is not possible.
Settlement on Kaspa
Rich shared-state applications cannot live on ZKas, no VM, one asset. They can live beside it, on a Kaspa-side rollup with a wrapped representation, where privacy applies at entry and exit rather than continuously. That path is research-stage but it is not hypothetical:
OpZkPrecompile verifies Groth16/BN254 at 140,000 grams against a 500,000 block compute limit, about three verifies per Kaspa block, so proofs must batch. There is no Halo2 verification, so Orchard proofs have to be wrapped into Groth16 or risc0.Privacy model
| Hidden | Not hidden |
|---|---|
| Amounts, addresses, memo contents | That a transaction occurred, when, and its action count |
| Sender identity, no "from" field exists | Coinbase note recipients and values, which are publicly recomputable |
| Wallet balances and relationships | Which miner produced each block, via the merged-mining tag |
| Which note a spend consumed, full-tree membership | Network-level metadata, unless you use Tor |
Anonymity is statistical, not per-transaction. Membership is maximal, a spend proves inclusion in the entire tree, and privacy is mandatory, so nobody is flagged for using it. But the cryptography hides contents, not patterns: with low concurrent volume, timing correlation can still link activity, and coinbase leaves add tree size without adding confusion because anyone can recompute them. Quantified on live mainnet: 6,184,135 notes, ~91% coinbase mints, only ~5.5% ever spent — so a non-mint spend hides in roughly 547,000 user notes, an order of magnitude below the headline figure, and the gap widens every block because the set grows from mining rather than usage. Quote the real number, not the tree size.
Not post-quantum. Orchard/Halo2 is not PQ, and encrypted notes are subject to harvest-now-decrypt-later.
zkas-rusty at zkas-v1.0.9. If this page contradicts the code, the code is right. Report an error.