JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Private smart contracts: when zero-knowledge meets confidentiality

Zcash can hide a payment, but its only verb is "send." The next leap is hiding an entire program's state — a token whose balances are secret yet provably correct. Meet notes, nullifiers, and client-side proving, and the composability wall they slam into.

From a private coin to a private program

In the last guide, Zcash's shielded pool hid the sender, the receiver, and the amount of a payment. That is remarkable — but notice how narrow it is. A shielded transfer knows exactly one verb: move value from one secret holder to another. The rules are frozen into the protocol. You cannot write a sealed-bid auction where the bids stay secret until reveal, a token whose balances are hidden yet whose every transfer is provably valid, or a private vote that proves you were eligible without showing who you are. Each of those is a program, not a payment.

The frontier of blockchain privacy is the private smart contract: a contract whose state and computation are confidential, yet which still produces a zero-knowledge proof that its rules were followed exactly. The leading general-purpose attempt is Aztec, a privacy-first Layer 2 on Ethereum, but the same ideas appear in Aleo, Miden, and Penumbra. This guide opens the machine. Three parts carry all the weight: notes (how you store hidden state), nullifiers (how you spend it without revealing what you spent), and client-side proving (how you run a smart contract on your own laptop and hand the chain only a proof).

Notes: hidden state shaped like UTXOs

The first consequence of hiding state is structural, and it surprises people. A public ERC-20 transfers value with `balances[from] -= v; balances[to] += v`. But to subtract, you first have to read `balances[from]` — and reading a hidden balance is precisely the thing we are trying to forbid. So private state cannot be account-shaped. It must be UTXO-shaped, like Bitcoin: you never edit a balance in place; you destroy old units and create new ones. This is why the account model is great for public contracts and useless for private ones, and why UTXO-style privacy keeps reappearing.

So private state lives as notes. A note is a small private record — for a token it might be `(value, owner, randomness)`; for a richer contract it can hold any fields the developer wants. But a note cannot sit on a public chain in the clear, or there would be no privacy. So the chain stores only a commitment to each note: a hash that is binding (you cannot later claim the note held different values) and hiding (it reveals nothing about those values), thanks to the fresh randomness. This is a note commitment, built from a commitment scheme like a Pedersen or Poseidon hash. Every commitment ever made is appended as a leaf to one giant append-only Merkle tree — the note tree.

One subtlety that decides real-world usability: if only the commitment is public, how does the recipient learn the note's secret contents so they can spend it later? The sender encrypts the note's preimage to the recipient's public key and posts that ciphertext as an on-chain log. The recipient then trial-decrypts every new log to discover which notes are theirs. This is the note-discovery problem, and it is an honest cost: naively, your wallet must scan and attempt to decrypt every log on the chain. Tagging and oblivious-message-retrieval schemes shrink that cost, but it never fully disappears.

Nullifiers: spend without revealing which note

Now the hard question. The note tree only ever grows — commitments are added and never visibly removed. So how does the chain stop you from spending the same note twice, when it cannot even see which note you are spending? If removal were visible, it would point straight at your commitment and destroy the privacy you just built. The answer is the single most elegant trick in this whole field: the nullifier.

When you spend a note, your proof publishes exactly one new value — the nullifier — derived deterministically from the note and your secret key, e.g. `nf = hash(commitment, spend_sk)`. It has four properties that together solve the puzzle: it is deterministic and unique (one note maps to exactly one nullifier); it is unlinkable (given `nf`, no observer can tell which commitment it kills, because the secret key is mixed in); it is owner-bound (only the holder of `spend_sk` can compute it); and it is public. The chain keeps a growing nullifier set. A spend is valid only if its nullifier has never appeared before; then the nullifier is added. A repeat is a double-spend, and it is rejected — all without ever learning which note died.

This is not a new idea bolted on for contracts — it is exactly how the Zcash shielded pool and the Tornado Cash mixer you met in the previous guides prevent double-spends privately. A zero-knowledge proof does all the real work: inside a circuit, your private inputs (the witness) prove every rule below holds, while only the public values escape. Here is the canonical spend circuit:

// Spending one private note (Zcash / Aztec style), proved in zero knowledge.
//
// PUBLIC inputs  (posted on-chain; reveal nothing on their own):
//   root        a recent note-tree Merkle root
//   nf          the nullifier of the note being spent
//   cm_out[]    commitments of the new output notes
//
// PRIVATE witness (stays on YOUR device; never published):
//   value, owner_pk, r     the note being spent
//   spend_sk               your secret key  (owner_pk = derive(spend_sk))
//   path                   Merkle path proving the note is in the tree
//   out_values[], r_out[]  amounts + randomness of the new notes

circuit spend:
    // 1. the input note is well-formed
    cm = hash(value, owner_pk, r)

    // 2. you actually own it
    assert owner_pk == derive(spend_sk)

    // 3. the note exists in the tree -- WITHOUT revealing which leaf
    assert merkle_verify(root, leaf = cm, path)

    // 4. the nullifier is the unique, unlinkable tag of THIS note
    assert nf == hash(cm, spend_sk)

    // 5. value is conserved: input == sum(outputs) + fee
    assert value == sum(out_values) + fee
    for i in outputs:
        assert cm_out[i] == hash(out_values[i], recipient_pk[i], r_out[i])
Public inputs leak only a root, a nullifier, and new commitments — none of which exposes amounts, parties, or which note was spent.
  1. Worked value check: you spend one note worth 10, sending 3 to Bob and taking 7 back as change, fee 0. Constraint 5 holds: 10 == 3 + 7 + 0. Two output commitments cm_out are appended; observers see two fresh leaves and learn nothing.
  2. The chain verifies the proof against (root, nf, cm_out), confirming all five constraints without seeing the witness.
  3. The chain checks nf is not already in the nullifier set. If it is, reject as a double-spend.
  4. Otherwise the chain adds nf to the nullifier set and appends each cm_out as a new leaf in the note tree. The input note is now permanently dead — and nobody ever learned which one it was.

Private execution: proving you followed the rules

Where does that proof come from? Not the chain — it never sees your secrets. In Aztec, a private function runs in a Private eXecution Environment (PXE) on your own device. It reads your local notes, executes the contract's private logic, and emits the public outputs (new commitments, nullifiers, encrypted logs) together with a zero-knowledge proof that the function body ran correctly. Only the proof and those public outputs are broadcast; the inputs, the values, and even which branch of the code you took stay on your machine. Because the proof is a non-interactive one, the chain checks it with no back-and-forth.

A real contract has two halves. Private functions run client-side and touch notes, as above. Public functions run transparently on the sequencer over public state, exactly like a normal EVM contract — and a private call can enqueue a public call across an explicit boundary. This is a deliberate split: a zkEVM proves correct execution of public EVM bytecode, but here the goal is more — to also hide the inputs. You keep the parts that must be private (your balance, your bid) on your device, and push the parts that must be shared (a pool's reserves, a public tally) onto the transparent layer.

A single transaction may call several private functions, each emitting its own proof. To keep the on-chain verifier cheap, Aztec folds them with recursion: a kernel circuit verifies the previous kernel's proof plus the new function's proof, and outputs one combined proof. This is recursive proof composition, and it means the chain checks one succinct SNARK for the entire private call stack, no matter how deep. Recursion is what makes a programmable private contract — not just a fixed shielded payment — actually affordable to verify.

The composability wall

Now the honest, hard part — the reason private DeFi is not just a feature flag. Public DeFi is Lego because any contract can synchronously call another and read its return value: a router asks a pool for a price, the pool answers, all in one atomic transaction. Hidden state breaks this. A private function runs on your device and can only see your notes; it cannot read another user's secret balance or a contract's secret state, because no one else will hand you their witness. Two private contracts simply cannot compose the way two public ones do.

The deepest example is shared mutable state, and an AMM makes it vivid. To price a swap, the pool must read its reserves; to match orders, a book must see the orders. You cannot have a fully private shared order book, because matching is fundamentally an act of looking. So real designs split the world: private user state (your balances, held as notes) plus public shared state (the pool itself, its reserves, the price in its liquidity pool). You can hide who you are and how much you hold, but the existence of your interaction with the public pool — and the pool's state — remains visible. Confidentiality survives at the edges; the shared core stays in the open.

Two more frictions follow. Concurrency: notes parallelize beautifully — each user owns disjoint notes, so private spends don't contend. But anything serialized through a shared public slot can conflict: if two users build proofs against the same public state and both land in one block, one proof was computed against a now-stale state and is invalidated, forcing an expensive re-prove. Front-running and discovery: the transparent boundary where a private call touches public state is visible and can be front-run, and recipients still pay the note-discovery scanning cost from earlier. None of this is fatal — Aztec, Aleo, and others ship real private contracts — but it is exactly why "just make DeFi private" is one of the hardest problems in the field.

Other roads to private state: FHE, MPC, and TEEs

The ZK-notes route is not the only design, and naming the alternatives sharpens what ZK actually buys you. The zero-knowledge / UTXO approach gives strong integrity (a proof that the rules held) and self-custody privacy (only you hold your own witness) — but at the price of no shared private state and a note-discovery cost. Three rival families trade those constraints away, each by adding a different trust assumption.

  1. FHE (fully homomorphic encryption) — Zama's fhEVM, Fhenix: contracts compute directly on encrypted data, so you CAN have shared encrypted state, like an encrypted balance map every contract touches. The cost: it is orders of magnitude slower, and decrypting a result needs a threshold-decryption committee that could, if enough members collude, decrypt your state.
  2. MPC (secure multiparty computation): state is secret-shared across a committee that jointly computes over it without any single party ever seeing the cleartext. You get shared private state, but you must trust that a threshold of the committee stays honest, and the protocol is communication-heavy.
  3. TEEs (trusted execution environments / hardware enclaves, e.g. Intel SGX): code runs inside a sealed chip region that even the machine's owner cannot peer into — fast and general, but you trust the chip vendor and that the enclave hasn't been broken, and several SGX side-channel breaks are on record.

Read them as a single trade-off, not a ranking. ZK gives you integrity for free and isolates trust to yourself, but cannot natively share private state. FHE and MPC give you shared private state, but reintroduce a committee you must trust not to collude. TEEs give you speed and generality, but move trust into silicon. There is no design that is private, shared, trustless, and fast all at once — privacy has its own trilemma, and every real system picks a corner.