The Evolution of On-Chain Privacy: Mixers → Shielded Pools → TEE Execution
--
While reviewing several privacy-focused projects, I started noticing a recurring set of design patterns in how privacy is implemented. Each system approached the problem differently, but the underlying structure often followed a similar progression, with clear trade-offs at every stage. This evolution felt almost generational each new approach building on the limitations of the previous one, improving certain guarantees while introducing new assumptions. That observation motivated this article: to highlight these patterns, examine the trade-offs they introduce, and show how privacy architectures in blockchain systems are steadily evolving through iterative engineering rather than a single definitive solution.
The Classic mixer approach
Although this approach doesn’t hide the amount a sender is sending or receiving, it was the first widely used method for private transactions (and yes, some illegal use happened — we don’t talk about that).
The idea is simple: you send funds to a smart contract, but here’s the key twist — everyone deposits the exact same amount. This is intentional. If Alice deposits 1 ETH and Bob withdraws 1 ETH, there’s no way to tell if Bob is Alice, or one of the hundred other people who also deposited 1 ETH. If amounts varied, you could just match deposits to withdrawals by amount and the whole thing falls apart.
And here’s where it gets interesting — the more people depositing, the stronger the privacy. If only 3 people have deposited and you withdraw, there’s a 1-in-3 chance someone can guess it was you. But if 10,000 people have deposited? Good luck. This pool of depositors is called the anonymity set, and size is everything. A mixer with 5 users is basically useless. One with thousands is genuinely hard to trace.
The flow looks like this: you deposit funds into the contract and get a secret key in return — think of it like dropping cash into a locker and keeping the only key. Later, from a completely fresh wallet, you (or anyone you give the key to) can withdraw the funds. The key works once and is discarded after use, so you can’t drain the same deposit twice.
Technically, the contract doesn’t track balances. Instead, each deposit creates a commitment derived from a random secret:
commitment = hash(secret, nullifier)
This commitment is added to a Merkle tree containing all deposits (the anonymity set). The contract only stores commitments, not ownership. During withdrawal, the user generates a zk proof showing:
- they know the
secretfor one commitment in the tree - the commitment exists in the Merkle root
- the
nullifierhasn't been used before
The nullifier prevents double spends, while the proof hides which deposit is being withdrawn. Funds are then sent to a new address, typically via a relayer to avoid linking the withdrawer’s new address back to the original deposit transaction.
A deeper dive into this if you are interested:
https://soliditydeveloper.com/tornado.cash
ZK-based privacy with hidden amounts
Mixers removed deposit-withdraw linkage, but amounts were still public, forcing fixed denominations.
Shielded pools remove this by converting deposits into confidential balances that move privately inside a shared pool.
A user first deposits into the pool, turning public tokens into an encrypted note representing their private balance. From that point onward, transfers happen entirely inside the pool by spending notes and creating new ones. These notes can be split, merged, and forwarded across multiple hops, all without revealing sender, receiver, or value. Only when funds exit the pool does a public transfer occur.
This creates a continuous private flow:
- deposit converts public balance → confidential note
- private transfers update encrypted notes inside the pool
- withdrawals convert confidential note → public balance
- no denomination constraints
- no deposit-withdraw matching
- balances remain shielded across hops
On-chain, observers only see commitments being added and nullifiers being consumed. The internal transaction graph remains hidden, and privacy strengthens as more notes accumulate in the pool.
However, one leak remains: the transaction sender is still visible.
The address submitting the zk proof and paying fees is public, even though the logical sender inside the pool is hidden.
One possible technical structure
Funds are represented as encrypted notes:
note = {
token,
amount,
owner_pubkey,
randomness
}Each note produces a commitment:
commitment = hash(note)Commitments are appended to a Merkle tree representing all private balances.
To spend a note, a nullifier is derived:
nullifier = hash(note, spend_key)A private transfer consumes input notes and creates new output notes, while generating a zk proof that verifies:
- input notes exist in the Merkle tree
- spender owns the notes
- nullifiers are unused
- sum(inputs) = sum(outputs)
- output commitments are valid
The chain only sees:
{
nullifiers[],
new_commitments[],
merkle_root,
zk_proof
}This hides which notes were spent, who received them, and how much was transferred, while still enforcing balance correctness.
A deeper dive into this if you are interested:
Confidential Stablecoin Transfers on Plasma: A zk-SNARK-Based Shielded Pool Design
A Research paper proposing a zk-SNARK based shielded pool for the Plasma team
parallelresearch.substack.com
TEE-Based Complete Confidentiality with Trust Assumptions
Now for the final approach the one that’s actively being built at the time of writing this article — TEE-based private execution. This model hides the entire transaction, not just parts of it. Instead of publishing sender, receiver, and amount on-chain, the user encrypts the full transaction locally and submits ciphertext. The blockchain only sees opaque data, while execution happens inside a Trusted Execution Environment (TEE). Balances, transfers, and even contract logic live in encrypted state, so observers can verify that something happened without learning what actually happened.
The flow is simple from a user perspective. You construct a transfer, encrypt it with the network key, and send it through a relayer. The relayer submits the encrypted payload, and the contract processes it inside the enclave. The state updates remain encrypted, and only the user can decrypt their own balance afterward. There’s no deposit/withdraw model, no anonymity set, and no fixed denominations. Privacy exists even with a single user, and as more encrypted activity accumulates, the system naturally becomes harder to analyze.
Under the hood, this combines TEE execution with homomorphic encryption. The TEE guarantees that approved code executed correctly, while homomorphic encryption allows computation directly on ciphertext. Instead of decrypting balances, the system updates encrypted values and writes back encrypted results. Validators only verify the enclave attestation and accept the new encrypted state root. The chain never sees plaintext — just valid transitions between encrypted states.
A minimal view of what happens internally
User encrypts the transfer:
constencTx=encrypt({
from,
to,
amount
})
submit(encTx)The contract stores encrypted balances:
balances[enc(user)] = enc(balance)Inside the TEE, the transfer runs as encrypted math:
enc_sender_new = FHE.sub(enc_sender_balance, enc(amount))
enc_receiver_new = FHE.add(enc_receiver_balance, enc(amount))Validity checks also run on ciphertext:
valid = FHE.gte(enc_sender_balance, enc(amount))
balances[sender] = FHE.select(valid, enc_sender_new, enc_sender_balance)
balances[receiver] = FHE.select(valid, enc_receiver_new, enc_receiver_balance)Homomorphic encryption makes this possible:
FHE.add(enc(a), enc(b)) = enc(a + b)
FHE.sub(enc(a), enc(b)) = enc(a - b)So balances update without ever being decrypted. The enclave returns the updated encrypted state, the chain stores it, and the user decrypts their new balance locally.
A deeper dive into the homomorphic proof validation:
Homomorphisms by Example | RareSkills
Homomorphisms by Example A homomorphism between two groups exists if a structure preserving map between the two groups…
rareskills.io
Interesting paper about TEE