Privacy-focused chains do not solve the same problem. Some focus on shielded payments. Some focus on private program execution. Some focus on recursive proofs and cheap verification. This tutorial compares Midnight, Aztec, Aleo, Mina, and Zcash from a developer point of view: what you write, where state lives, what the chain verifies, and where engineering friction appears.
This guide is for developers and Web3 enthusiasts who understand basic smart contract concepts and want a practical comparison of privacy-preserving stacks.
Prerequisites
You should know account and UTXO state models, zero-knowledge proof basics, and the lifecycle of deploying a smart contract or DApp.
What this comparison checks
Use three questions to compare these systems:
- Language design: How do you express public values, private values, assertions, and proof-generating logic?
- State model: Does state live in accounts, notes, records, local private state, public mappings, or commitment trees?
- Privacy model: What does the chain hide, what does it reveal, and how does a developer control disclosure?
Midnight sits between programmable smart contracts and shielded asset movement. It uses Compact for smart contracts, ZK circuits for correctness, Kachina for data-protecting smart contract execution, Zswap for shielded multi-asset swaps, and a dual ledger approach that combines UTXO-style ledger tokens with account-style contract tokens.
A current Midnight baseline
Start with the smallest useful Compact example. The current Midnight Hello World tutorial uses Compact language version >= 0.23 and shows the main privacy boundary in one line: a private circuit parameter becomes public only through disclose().
pragma language_version >= 0.23;
export ledger message: Opaque<"string">;
export circuit storeMessage(newMessage: Opaque<"string">): [] {
message = disclose(newMessage);
}
The ledger declaration creates on-chain state. Circuit parameters are private by default. disclose(newMessage) makes the value available for public storage. That explicit boundary is the key Compact habit: do not let private inputs become public by accident.
A typical setup flow follows the official example repository and compiler workflow:
git clone https://github.com/midnightntwrk/example-hello-world.git
cd example-hello-world
yarn install
cd contracts
compact compile hello-world.compact managed/hello-world.compact
For DApp interaction, you also run a proof server. The proof server generates ZK proofs for circuit calls before the transaction reaches the network.
docker run -p 6300:6300 midnightntwrk/proof-server:8.0.3 midnight-proof-server -v
That workflow shows Midnight's shape. You write Compact. The compiler validates the smart contract, emits ZK artifacts, and generates TypeScript-facing APIs. Midnight.js connects the compiled smart contract to providers for public data, private state, ZK artifacts, proof generation, wallet interaction, transaction submission, and logging.
Compact source
-> compiler validation
-> JavaScript circuit implementation
-> TypeScript declarations
-> ZK intermediate representation and keys
-> Midnight.js providers
-> proof server
-> submitted transaction
Midnight: Compact, Kachina, Zswap, and the dual ledger
Midnight's developer model starts with Compact. Compact is not Solidity with a privacy library attached. It is a smart contract language that compiles to ZK circuits used to prove correctness of interactions with the ledger. That matters because you write application logic with proof generation in mind from the start.
Kachina explains the contract privacy model. Public state lives on-chain. Private state stays local to the user or application. A ZK proof connects the two by proving that a private state transition justifies a public state update. Validators do not need to learn the private witness. They verify the proof and the public part of the transaction.
That model feels different from an EVM account update. In a Solidity contract, the chain sees the call data unless you add a separate privacy mechanism. In Midnight, the default design pushes sensitive inputs into local execution and proof generation. You still need to decide what becomes public. The compiler does not remove the need for data classification. It makes the boundary explicit.
Zswap handles the asset side. It is a data-protecting transaction scheme for multi-asset atomic swaps. It draws from Zerocash and Zcash Sapling ideas, uses ZK proofs and commitments, and supports non-interactive merging of transactions. For developers, this means Midnight is not only about hiding smart contract inputs. It also gives a native path for shielded asset exchange and DeFi-style flows where pre-trade visibility can leak value.
Midnight also uses a dual ledger approach. Ledger tokens use a UTXO model. Contract tokens use an account-style model inside Compact smart contracts. That gives you two design paths. Use ledger tokens when you need shielded, UTXO-style asset movement. Use contract tokens when your application needs smart contract controlled state. The trade-off is that you must choose the right token model early. Migrating an application from one state assumption to another can change wallet UX, indexing, and proof flow.
The main strength is coherence. Compact, Midnight.js, the proof server, Kachina, and Zswap target the same application style: privacy-preserving DApps with explicit disclosure. The main friction is operational. You manage proof generation, private state, public data queries, network compatibility, and toolchain versions.
Aztec: Noir, Aztec.nr, PXE, and notes
Aztec is a privacy-first Ethereum Layer 2 that combines private and public execution. Developers write smart contracts in Noir through Aztec.nr. Private functions execute client-side and produce proofs. Public functions execute in the Aztec Virtual Machine. This split gives Aztec a strong programming model for applications that need both confidential user actions and public settlement.
Aztec's state model centers on notes. A note is an encrypted piece of private state. The protocol stores note hashes and uses nullifiers to prevent double spending. Public state sits in a public data tree. Private state behaves like a UTXO model: you create notes, consume notes, and reveal nullifiers when notes are spent. The user discovers and decrypts relevant notes through local tooling.
The PXE, or private execution environment, is central to the developer experience. It stores secrets, synchronizes relevant notes, prepares private function execution, and generates proofs before transaction submission. That gives developers a clear privacy boundary: private inputs stay in the PXE. It also creates a local-state dependency. If note discovery or PXE synchronization fails, the application can feel broken even when the chain is healthy.
Noir itself is broader than Aztec. It compiles to an intermediate representation that can target proving backends. Aztec.nr adds the blockchain-specific layer: macros, storage abstractions, contract patterns, and execution conventions. If you already think in ZK circuits, Noir is attractive because it exposes a general ZK programming language. If you come from account-based smart contracts, the note lifecycle is the larger adjustment.
A minimal Aztec.nr private counter shows the model. The private function updates note-backed state for an owner, while the utility function reads local private state through the PXE.
use aztec::macros::aztec;
#[aztec]
pub contract Counter {
use aztec::{
macros::{functions::{external, initializer}, storage::storage},
messages::message_delivery::MessageDelivery,
protocol::address::AztecAddress,
state_vars::Owned,
};
use balance_set::BalanceSet;
#[storage]
struct Storage<Context> {
counters: Owned<BalanceSet<Context>, Context>,
}
#[initializer]
#[external("private")]
fn initialize(owner: AztecAddress, start: u64) {
self.storage.counters.at(owner).add(start as u128).deliver(
MessageDelivery.ONCHAIN_CONSTRAINED,
);
}
#[external("private")]
fn increment(owner: AztecAddress) {
self.storage.counters.at(owner).add(1).deliver(MessageDelivery.ONCHAIN_CONSTRAINED);
}
#[external("utility")]
unconstrained fn get_counter(owner: AztecAddress) -> pub u128 {
self.storage.counters.at(owner).balance_of()
}
}
Aztec is strong when an application needs Ethereum-adjacent settlement, programmable private actions, and a clean split between private and public functions. The cost is complexity around notes, PXE state, asynchronous execution, and the mental shift from updating contract storage to consuming and creating private commitments.
Aleo: Leo, records, transitions, and mappings
Aleo uses Leo as its developer-facing language. Leo looks like a domain-specific language for ZK applications rather than a general smart contract language. It gives developers records for private state and mappings for public state.
A record is Aleo's private state container. It can hold arbitrary application data. Records are encrypted on-chain, owned by an address, and consumed when used. A transition can consume old records and create new records. This mirrors the UTXO lifecycle: state changes by spending old objects and producing new objects, not by mutating a global account balance in place.
Public state uses mappings. Mappings are visible on-chain and suit counters, registries, configuration, and other shared values. Leo also supports finalizers for public state updates after private execution. The result is a hybrid model. Private data flows through records. Public coordination flows through mappings.
In Leo, the split is visible in the syntax: records carry private state, mappings carry public state, and an async finalizer updates the mapping.
program private_points.aleo {
mapping public_points: address => u64;
record Points {
owner: address,
amount: u64,
}
transition mint_private(receiver: address, amount: u64) -> Points {
return Points {
owner: receiver,
amount: amount,
};
}
async transition publish(points: Points) -> Future {
return finalize_publish(points.owner, points.amount);
}
async function finalize_publish(owner: address, amount: u64) {
let current: u64 = Mapping::get_or_use(public_points, owner, 0u64);
Mapping::set(public_points, owner, current + amount);
}
}
Aleo fits applications whose state maps to private record ownership. The hard parts are shared mutable state, coordination, indexing, and recovery because each user depends on finding, decrypting, and spending the right records.
Compared with Midnight, Aleo puts more emphasis on records as the unit of private program state. Midnight puts more emphasis on the relationship between local private state, public ledger state, and smart contract calls.
Mina: o1js, zkApps, and recursive proofs
Mina approaches privacy and scalability from a different angle. Its signature feature is recursive proof composition. The chain itself uses recursive proofs to keep verification small. Developers build zkApps with o1js, a TypeScript library for writing ZK circuits and smart contracts.
The developer experience is familiar if you already use TypeScript. You define a smart contract class, add methods, use o1js field types, and generate proofs locally. The network verifies the proof and applies account updates. On-chain state is intentionally small. A zkApp account has a limited number of on-chain fields, so applications often keep larger state off-chain and commit to it with hashes or Merkle roots.
Privacy in Mina is about what the proof hides. Method parameters and computation can remain private, while the public account update reveals the verified state change. Mina is less natural for rich native shielded asset flows or large private state directly represented on-chain.
Mina's best developer story is proof composition. You can build a proof of a computation, then use recursion to fold proofs together or verify one proof inside another. For identity, games, compliance checks, and verifiable compute, this can be more important than a full shielded transaction system. The trade-off is that you design around small public state and off-chain data availability.
An o1js contract looks like TypeScript, but the method body builds constraints. Here, the public state stores only a commitment. The secret argument stays private inside the proof.
import { Field, Poseidon, SmartContract, State, method, state } from 'o1js';
export class HashCounter extends SmartContract {
@state(Field) commitment = State<Field>();
init() {
super.init();
this.commitment.set(Poseidon.hash([Field(0)]));
}
@method async incrementSecret(secret: Field) {
const current = this.commitment.get();
this.commitment.requireEquals(current);
Poseidon.hash([secret]).assertEquals(current);
this.commitment.set(Poseidon.hash([secret.add(1)]));
}
}
Compared with Midnight, Mina gives you a TypeScript proving environment and a strong recursive proof story. Midnight gives you a domain-specific smart contract language, a proof server workflow, and native shielded asset architecture through Zswap.
Zcash Sapling: shielded payments, not general DApps
Zcash Sapling is the reference point for shielded payment privacy. Sapling improved shielded transaction efficiency, introduced Sapling shielded addresses, and supports viewing keys that let a holder disclose transaction details without giving spend authority.
The Sapling state model uses note commitments and nullifiers. A shielded output creates a note commitment. A spend reveals a nullifier so the network can reject double spends without learning which note was spent. The protocol can hide sender, receiver, amount, and memo data for shielded transfers, while fees and some transaction metadata remain visible.
For developers, Zcash is not a general-purpose privacy-preserving smart contract platform. It is a payment protocol and wallet ecosystem. You build wallets, exchanges, payment flows, custody tools, compliance flows, and viewing-key workflows. You do not write arbitrary DApps that update application state through smart contract circuits.
The developer surface is therefore RPC and wallet integration. A Sapling-oriented flow creates an account, derives a shielded receiver, sends with a privacy policy, and checks the async operation.
zcash-cli z_getnewaccount
zcash-cli z_getaddressforaccount ACCOUNT_NUMBER '["sapling"]'
zcash-cli z_sendmany "FROM_SHIELDED_OR_UNIFIED_ADDRESS" \
'[{"address":"RECIPIENT_SHIELDED_OR_UNIFIED_ADDRESS","amount":0.01,"memo":"48656c6c6f"}]' \
10 null FullPrivacy
zcash-cli z_getoperationstatus '["OPERATION_ID"]'
That makes Zcash a clean comparison point. It shows mature shielded payment privacy, but not programmable private application logic. If the product is a privacy-preserving application with custom logic, you need one of the programmable stacks.
Developer experience comparison
| Stack | Language design | State model | Privacy model | Best fit | Main friction |
|---|---|---|---|---|---|
| Midnight | Compact smart contracts compile to ZK circuits and TypeScript-facing artifacts. | Public ledger state, local private state, UTXO-style ledger tokens, and account-style contract tokens. | ZK proofs connect local private state to public updates. Zswap supports shielded multi-asset swaps. | Privacy-preserving DApps that need smart contract logic and shielded assets. | Toolchain compatibility, proof server setup, private state handling, and token model choice. |
| Aztec | Noir plus Aztec.nr for private and public smart contract functions. | Public data tree plus encrypted notes and nullifiers. | Private functions run client-side through PXE. Public functions run in the AVM. | Ethereum-adjacent applications with private actions and public settlement. | Note discovery, PXE sync, asynchronous private/public flows, and non-EVM design. |
| Aleo | Leo programs with transitions, records, mappings, and finalizers. | Private records and public mappings. | Records are encrypted and spent like private UTXOs. Public mappings support shared state. | Private-by-default applications where user-owned records are natural. | Record lifecycle management, shared state coordination, and indexing. |
| Mina | o1js TypeScript smart contracts and ZK programs. | Small on-chain account state, off-chain data, and committed roots. | Proofs hide private inputs and computation while public account updates settle on-chain. | Recursive proof applications and succinct verification of off-chain compute. | Limited on-chain state, proof design, and off-chain data management. |
| Zcash Sapling | Protocol-level shielded payment logic, not a smart contract language. | Note commitments, nullifiers, shielded addresses, and transparent addresses. | Shielded transfers hide sender, receiver, amount, and memo data. Viewing keys support disclosure. | Payments, wallets, custody, and shielded transfer workflows. | No general smart contract layer for application logic. |
How to choose
Choose Midnight when your DApp needs privacy-preserving smart contract logic and shielded asset movement in the same architecture. Compact gives you explicit disclosure control. Zswap gives you shielded multi-asset exchange. The dual ledger gives you a choice between UTXO-style ledger tokens and account-style smart contract tokens.
Choose Aztec when your application benefits from Ethereum Layer 2 settlement and you can accept the note-based private state model. Aztec is a strong fit for applications that need both private user actions and public composability.
Choose Aleo when your product state maps cleanly to private records. It works well when users own private objects, spend them, and receive new ones as the application evolves.
Choose Mina when the application centers on proving off-chain computation and composing proofs. Mina is less about shielded asset flows and more about succinct verification.
Choose Zcash when the requirement is shielded payment privacy rather than programmable DApp logic.
Troubleshooting common mistakes
Version mismatch: Compact examples depend on language and toolchain versions. Match the pragma language_version directive to the installed Compact toolchain and the current Midnight compatibility matrix.
Proof server unavailable: Midnight DApp calls that require proofs need a reachable proof server or proof provider. Check the host, port, Docker status, and ZK artifact path before debugging wallet code.
Accidental disclosure: Treat disclose() as a security review point. Every disclosed value becomes part of the public logic. If a value should stay private, keep it in the witness path or local private state.
Wrong token model: Ledger tokens and contract tokens do not give the same developer experience. Use the UTXO-style path for shielded asset movement and the account-style path for smart contract managed tokens.
Lost local state: Aztec notes, Aleo records, and Midnight private state all depend on local discovery or storage. Plan backup, recovery, and indexing before users hold value.
Overusing ZK: A proof hides inputs and computation, but it does not hide everything. Timing, fees, public state changes, nullifiers, commitments, and application-level messages can still leak information.
References
- Midnight Compact documentation
- Midnight Hello World tutorial
- Midnight Kachina documentation
- Midnight Zswap documentation
- Midnight ledgers documentation
- Midnight.js documentation
- Aztec documentation
- Aztec counter contract tutorial
- Aztec state model
- Aztec PXE documentation
- Aztec.nr documentation
- Noir documentation
- Aleo Solidity-to-Leo migration guide
- Aleo public and private state documentation
- Aleo records documentation
- Mina smart contract documentation
- Mina recursion documentation
- Zcash addresses and value pools
- Zcash new account RPC
- Zcash account address RPC
- Zcash shielded send RPC
- Zcash Sapling network upgrade
Top comments (0)