Skip to main content

Information Model

At its core, zkCoins is an information system: a small set of pieces of data, each created at a specific point, held by a specific party, and either kept private or published. This page is the complete catalog. For every piece of information it answers four questions:

  1. What is it?
  2. How does it come into existence? (its genesis / derivation)
  3. Who holds it?
  4. How — and whether — may it be shared?

It complements two neighbouring pages: the Privacy Model (what an on-chain observer can see) and the Trust Model (what your node operator can see). Read this one first — it is the map.

Notation: H(...) denotes a domain-separated hash. In-circuit hashes use Poseidon over Goldilocks; the Schnorr signature over the on-chain commitment uses SHA-256 per BIP-340. The exact functions live in Proof System and Key Management; this page focuses on what is hashed and why, not the primitive.

Protocol vs. the zkcoins.app service

This page describes the zkCoins protocol. The protocol's only notion of identity is the 32-byte address = SHA-256(initial public key).

Human-readable handles such as [email protected], username registration, and LNURL / Lightning-Address resolution are features of zkcoins.app — one specific, centralized service provider built on top of the protocol. They are not part of the protocol: it neither stores nor needs an @ handle, and a different operator may offer different naming, or none at all. Wherever an @ handle appears below, treat it as a service-layer convenience, not a protocol fact.

The four sensitivity classes

Every piece of information falls into exactly one class. The class answers "who may hold it and who may see it" in one move:

ClassMeaningLives where
🔴 SecretNever leaves the wallet. Disclosure = total loss of funds.The user's device only
🟠 PrivatePlaintext bookkeeping. Disclosure = loss of privacy (never theft).Wallet + the node that hosts the account
🟡 ShareableHanded out on purpose.User + the payment counterparty
🟢 PublicWritten to Bitcoin, world-readable — but only opaque commitments (no amounts, no identities).Bitcoin L1

The whole privacy story is the gap between Private (off-chain plaintext) and Public (on-chain commitments), and the whole trust story is the question whose node holds the Private data.

The information catalog

InformationClassHow it comes into existence (genesis)Held byShared withMay it be shared?
Seed🔴 Secret256-bit entropy (BIP-39 mnemonic, or Passkey PRF → HKDF) generated in the walletUser onlynobodyNever
Master private key (Xpriv)🔴 SecretBIP-32 derivation from the seedUser onlynobodyNever
Per-transaction private key🔴 SecretBIP-32 child derivation; a fresh key per send (forward secrecy)User onlynobodyNever
Public key (33 bytes)🟢 Publicsecp256k1 from the current private key; rotates each send (unlinkable to outsiders)User → embedded in the on-chain commitmentBitcoinYes (it is published on-chain)
Address🟡 ShareableSHA-256(initial public key) — fixed once at account creation; the protocol's only identity (see Key Management)Userthe payer (inside an invoice)Yes — but it is stable, so reusing it links payments
Invoice {amount, recipient, asset_id}🟡 ShareableCreated by the recipient when requesting a payment (recipient is the 32-byte address)Recipientthe chosen payerYes, with the payer
AccountState {owner, balance, public_key}🟠 PrivateCreated when the account is created; mutates on every sendUser + the hosting nodeonly your nodeNo — the balance is private
account_state_hash🟢 PublicH(AccountState)User → commitmentBitcoinYes — a hash; hides the balance
Coin {identifier, recipient, amount, asset_id}🟠 PrivateBuilt during a send from a CoinTemplateSender → recipientthe recipient (via the coin proof)Recipient only (today more is visible than ideal — see Privacy Model)
Coin identifier🟡 ShareableH(account_state_hash ‖ asset_id ‖ coin_index)inside the coin proofrecipient + the coins treeYes
AssetId🟡 ShareableHc("AssetId", genesis_tag ‖ creator_pubkey ‖ name_hash ‖ decimals ‖ issuance_version) at asset creationCreator → every user of that assetpublicYes (everyone using the token needs it)
Account / coins / history trees (Sparse Merkle Tree, Merkle Mountain Range)🟢 Public (roots) / 🟠 Private (leaves)The node builds them incrementally from accounts and coinsThe noderoots go on-chain; plaintext leaves stay privateRoots yes; plaintext leaves no
Validity proof (Plonky2, recursive)🟢 PublicThe node's prover produces one per state transitionThe nodethe recipient (inside the coin proof)Yes — zero-knowledge, it reveals nothing beyond validity
ProofData (public inputs) {account_state_hash, output_coins_root, commitment_history_root, coin_history_root, asset_id}🟢 PublicThe public outputs of the proofpublicbound on-chainYes — only hashes and roots
CoinProof = coin + proof + inclusion_proof🟠 PrivateThe sender bundles it for deliverySender → recipientthe recipientRecipient only (it contains the plaintext coin)
Commitment {public_key, signature, message}🟢 Publicmessage = account_state_hash ‖ output_coins_root; the owner Schnorr/BIP-340-signs it (the signature hashes the message with SHA-256 internally)first the owner, then BitcoinBitcoinYes — this is the only object that goes on-chain
Inscription🟢 PublicThe full commitment (public key + signature + message, ~177 bytes) is inscribed in the Bitcoin Taproot reveal transaction at broadcastBitcoin (permanently)the whole worldYes — the public, permanent anchor of the transaction

How information comes into existence

This is the heart of the model. Everything is born in the wallet, top-down from the seed, and only the opaque commitment (no amounts, no identities) is ever written to Bitcoin:

Seed (entropy)
└─BIP32─▶ Master Xpriv
└─child(i)─▶ Private key_i ──secp256k1──▶ Public key_i
├─ H ─▶ Address (identity, fixed once)
└─ rotates each send

Address + AccountState{balance} ─── H ───▶ account_state_hash

Send: CoinTemplate{recipient, amount, asset_id}
└─ + coin_index + account_state_hash ─ H ─▶ Coin.identifier
└──▶ output_coins_root (tree)

message = account_state_hash ‖ output_coins_root
message ── Schnorr/BIP-340(Private key_i) ──▶ Commitment{public_key, signature, message}
└── inscribed in full ──▶ BITCOIN L1 ◀── the only on-chain step

Node prover: (previous state, coins, trees) ── Plonky2 ──▶ Validity proof (+ ProofData)
Delivery to recipient: Coin + proof + inclusion_proof = CoinProof

Reading it as layers:

  • Key layer (Secret → one Public key). The seed deterministically produces every key. Only the public key of each transaction ever leaves the device, and it does so inside the on-chain commitment.
  • Identity layer (Shareable). The address is a one-time hash of the first public key; it is the stable handle a payer needs. An invoice wraps it together with an amount and asset.
  • State & coins layer (Private). Balances and coins are plaintext bookkeeping. They live off-chain. They are computed by the owner and held by whichever node hosts the account.
  • Asset layer (Shareable). An asset is defined the moment someone hashes their creator key together with a name and decimals. The resulting AssetId is public — every holder of the token references it.
  • Tree layer (Public roots / Private leaves). The node folds accounts and coins into Merkle structures. The roots are committed on-chain; the leaves (plaintext) are not.
  • Proof layer (Public). A recursive zero-knowledge proof attests the whole state transition is valid without revealing any Private data. Its public inputs (ProofData) carry only hashes.
  • On-chain layer (Public). A single signed Commitment — a rotating public key, a Schnorr signature, and the message account_state_hash ‖ output_coins_root (two hashes) — is inscribed in full on Bitcoin. Nothing else.

Two invariants

These are the load-bearing truths of the whole design:

  1. On-chain there are no amounts, recipients, or balances. Bitcoin carries only the opaque Commitment (a rotating public key, a signature, and two state hashes). The rotating key is unlinkable to outsiders, and no bookkeeping can be reconstructed from the chain. See Privacy Model.
  2. Privacy is decided by whose node holds the Private data. Your own node ⇒ no leak. Someone else's node ⇒ that operator sees your Private (plaintext) data — a privacy trade-off — but can never steal, forge, or double-spend, because that is enforced by the Public layer plus cryptography. This is exactly the Trust Model: run your own node and you are trustless and private at once.

Naming is a service-layer feature, not protocol

To the protocol, identity is the 32-byte address (SHA-256(initial public key)) and nothing else. The friendly [email protected] form is provided by the zkcoins.app service, in two variants:

  • Default handle — the first 8 hex characters of the address, derived on the fly and resolved on request. It is not stored anywhere; it exists for every account automatically. Example: address e660e4ea…[email protected].
  • Custom username — an optional override (e.g. [email protected]) that the user claims with a Schnorr signature over the identity key. Only custom claims are persisted (in the service's own table); a service with no claims still serves every default handle.

Because naming lives entirely in the service, it sits outside the four protocol classes above. A self-hosted node may implement naming differently, or not at all — the protocol is unaffected. See Addressing for the service's endpoints.

A worked example (live)

A single real account from a Mutinynet deployment, shown across the classes. Hashes are full and unabridged; the values are a point-in-time snapshot.

🔴 Secret — never present on the node at all. The seed and private keys stay in the wallet; the node only ever sees the public key below.

🟠 Private (off-chain, in the node's database) — the account's full state:

address (identity) : e660e4ea3ce6c92c1e27ecb6cad611236d96a412cbc51d28eeba45fa86903825
state blob size : 181,967 bytes (balance + rotating key + coin queue & history)

This blob lives only on the node. It is never published.

🟢 Public (on-chain, on Mutinynet) — the account's entire Bitcoin footprint is the inscribed commitment (~177 bytes = 33 + 64 + 64 fields, plus bincode framing):

commit txid : a5b267555b2284f19d77670b6e55a2d74117ec91f5a84cc8b335509c15f3f53c

public_key (33 B) : 0363c9346024c5dcd393bb7577fd3f8d045c68ad297d7b90a454d8e2ec388ed8c7
signature (64 B) : b50b629ea32dadf453561c763e3567869b1e326777459ca7d8f7fe71462b2506
9addc28c08b80ddb66a11830aa95e57a7b5ec24414486102c54695fc4ea1a4e2
message (64 B) = account_state_hash ‖ output_coins_root
account_state_hash : a791849cffb631de85d299074d150f1410c1c0e6edf66ffa42246bde64334fbc
output_coins_root : be900b25dad0c79b981741d7f37e3a3b635cf51cc7c4af2d1b428baa565134dc

What is not here: no balance, no amount, no recipient — only a public key, a Schnorr signature, and two hashes. And the address is exactly the SHA-256 of that public key:

SHA-256(0363c934…388ed8c7) = e660e4ea3ce6c92c1e27ecb6cad611236d96a412cbc51d28eeba45fa86903825

So this account is 181,967 bytes of private plaintext off-chain, but only ~177 bytes of an opaque commitment on-chain. The account_state_hash (a791849c…) is the public fingerprint of that private blob: an observer sees the hash and learns nothing; the owner can open it to the full balance.

🟢 Public (global tree roots) — every account folds into shared roots; the latest values:

smt_root      : aee03c2d44273005cc1fb5d999a564231784044a3a3e5a39d2f7a173448140b6
prev_mmr_root : da929cb09a4f034b4e30a2d74afc0e2db862d523c321af8d29e1358bca9bb3fd

Service layer (zkcoins.app — not protocol) — the friendly handle resolves live:

GET /api/username/resolve/e660e4ea
→ {"username":"e660e4ea","address":"0xe660e4ea3ce6c92c1e27ecb6cad611236d96a412cbc51d28eeba45fa86903825"}

The protocol neither knows nor needs this handle; it only ever uses the 32-byte address.