The Shielded Mixer
Tacit's shielded mixer is a Tornado-style zero-knowledge pool grafted onto a Bitcoin meta-protocol. A holder deposits a fixed-denomination Tacit-asset UTXO into a per-(asset_id, denomination) pool; later, anyone holding the mixer note (secret, ν) can withdraw to a fresh address with a Groth16 proof of unspent-leaf membership that breaks the on-chain link to the deposit. The on-chain footprint is two new envelope opcodes — T_DEPOSIT (0x29) and T_WITHDRAW (0x2A) — riding ordinary Bitcoin Taproot commit + reveal transactions. Bitcoin nodes do not interpret the envelopes; indexers (the dApp client; the reference Cloudflare worker) reconstruct pool state from chain alone and enforce the protocol's rules client-side.
The cryptographic primitives are not new. The Tornado Cash circuit design (Pedersen leaves + Merkle tree + Groth16 + nullifier set) is from 2019. Pedersen Commitments on Bitcoin envelopes appeared in Confidential Transactions proposals and in the Mimblewimble lineage as far back as 2015–2016. The indexer-validated meta-protocol pattern is Ordinals / Runes / BRC-20. The novelty Tacit claims is the composition: these three specific things working together on Bitcoin L1, with no live production peer.
Scope
| TACIT MIXER | |
|---|---|
| What it shields | Linkage between deposit and withdraw (Groth16 unlinkability) AND ambient amounts of surrounding transfers (Pedersen) |
| What pools key on | (asset_id, denomination) — one circuit serves every pool size |
| Where pool state lives | Reconstructed from chain by every conforming indexer |
| Where proofs verify | In the browser via snarkjs (vendored); the worker re-verifies as cross-check |
| Where notes are generated | In the user's browser, locally; (secret, ν) are 32 Cryptographically Secure Pseudo-Random Number Generator (CSPRNG) bytes drawn locally and never leave the device |
| What this is not | Not a native-BTC mixer (native sats have no asset_id); not novel cryptography; not an L2 |
Native BTC mixing requires first wrapping BTC into a Tacit asset (single-issuer wBTC, federated mint, or sidechain peg), and that wrapping has its own trust model independent of the mixer. The cBTC and cBTC-ZK wrapper-convention amendments address this — out of scope for the present analysis.
How the deposit-withdraw flow works
The pool's accounting is a Merkle tree of Poseidon leaf commitments. A deposit appends a new leaf; a withdraw spends one (without revealing which one).
sequenceDiagram
autonumber
actor A as Alice (depositor)
participant B as Bitcoin L1
participant I as Indexers
actor R as Recipient
A->>A: locally generate (secret, v) - each 32 random bytes
A->>A: compute leaf as Poseidon3(secret, v, denomination)
A->>B: T_DEPOSIT reveal tx<br/>consumes denomination-sized asset UTXO<br/>envelope carries the leaf
B-->>I: tx confirmed
I->>I: at depth >= 3, append leaf to pool tree<br/>record new root in 32-deep ring buffer
A-->>R: out-of-band, share (secret, v)<br/>self-mix is A = R
R->>R: compute rLeaf as Poseidon2(secret, v)
R->>R: compute nullifierHash as Poseidon1(v)
R->>R: compute recipientCommit as denomination times H plus rLeaf times G
R->>R: build Merkle proof against a recent pool root
R->>R: snarkjs Groth16 prove with witness over pool.vk
R->>R: compute bindHash as SHA256(public input tuple)
R->>B: T_WITHDRAW reveal tx<br/>fresh tacit UTXO at vout 0 paying R<br/>envelope carries proof, nullifierHash, recipientCommit, rLeaf, bindHash
B-->>I: tx confirmed
I->>I: REJECT unless all six checks hold<br/>pool registered, root in recent-32, nullifier unspent,<br/>bindHash recomputes, Groth16 verifies, Pedersen opening holds
I->>I: insert nullifier into spent set, credit recipient UTXO as spendable
Figure 1: Deposit-withdraw flow. The deposit reveals nothing about Alice's mixer secret; the withdraw reveals only that some unspent leaf opens to the supplied nullifier-and-commitment pair, without revealing which leaf.
The same operation can run with Alice as both depositor and recipient (self-mix to a fresh BTC address) or with Alice depositing and Bob withdrawing (pay-to-someone-else). Pay-to-someone-else has no chain-graph link between the parties; self-mix requires either a fresh BTC wallet for the withdraw or a relayer to break the BTC fee-source chain-graph link.
The withdrawal circuit
The Groth16 proving system needs a circuit. Tacit's withdraw.circom is small — roughly 5,000-7,000 constraints, dominated by the 20-level Merkle path. Prove time on modern hardware is sub-second; verify is microseconds.
Public inputs (in the BN254 scalar field Fr):
| INPUT | MEANING |
|---|---|
merkle_root |
A recent root of the pool's tree |
nullifier_hash |
Poseidon₁(ν) — the spend-once tag |
denomination |
The pool's denomination (public; defines which pool) |
r_leaf |
The on-chain Pedersen blinding scalar (constrained inside the circuit) |
bind_hash |
SHA-256 over the public input tuple — squared into the constraint system |
Private inputs (the witness, never revealed):
| INPUT | MEANING |
|---|---|
secret, nullifier_preimage |
The two 32-byte secrets defining the leaf |
path_elements[20], path_indices[20] |
Sibling hashes + bits, proving leaf inclusion |
Constraints:
leaf = Poseidon₃(secret, ν, denomination)- Walking
path_elements/path_indicesfromleafreproducesmerkle_root nullifier_hash == Poseidon₁(ν)r_leaf == Poseidon₂(secret, ν)bind_squared == bind_hash · bind_hash(no-op arithmetic constraint that bindsbind_hashinto the proof's polynomial system)
Constraint 5 is what defeats proof-substitution attacks. Without it, a relayer or mempool observer could copy a passing proof and re-broadcast it against a substituted public input — same proof, different recipient. Squaring bind_hash inside the constraint system binds it cryptographically. bind_hash covers (asset_id, denomination, nullifier_hash, recipient_commitment, r_leaf).
Constraint 4 is the inflation defense. Without it, a malicious withdrawer could pick r_leaf freely and forge a Pedersen opening for any denomination they wanted. By constraining r_leaf to be a deterministic function of the depositor's secret pair (secret, ν), and then having the validator check externally that pedersenCommit(denomination, r_leaf) == recipient_commitment on secp256k1, the soundness gap closes. Equivalently, this is "the in-circuit Pedersen check, externalized" — at roughly 100× lower constraint cost than doing secp256k1 multi-scalar multiplication inside a BN254 circuit.
Per-pool, not per-protocol
Each (asset_id, denomination) pair gets its own pool with its own Groth16 verifying key (vk). The vk is fixed at pool creation time (the POOL_INIT envelope is a T_DEPOSIT with denomination = 0 sentinel) and pinned to IPFS by content hash. There is no protocol-wide ceremony covering all pools.
This has two consequences:
- ◇ Pool initializers bear the responsibility of running a Multi-Party Computation (MPC) ceremony with credible contributor diversity before broadcasting POOL_INIT. The ceremony transcripts are pinned to IPFS and committed to in the on-chain envelope; anyone can verify the contributor count and re-run beacon-pinning offline.
- ☑ Soundness of one pool does not depend on any other. If a pool's ceremony is compromised, only that pool's withdrawal soundness is at risk. Privacy (the zero-knowledge property) is unconditional — Groth16 has perfect zero-knowledge regardless of the trusted setup.
The reference implementation ships with one canonical mixer pool whose ceremony was finalized 2026-05-11.
The canonical ceremony
| PHASE | WHAT IT IS | TACIT'S CHOICE |
|---|---|---|
| Phase 1 (Powers of Tau) | Universal, reusable up to a constraint-count ceiling | Polygon Hermez pot14 (≈ 71 contributors, Bitcoin-block-hash beacon, 2020–2022). dapp/circuits/build.sh downloads and dual-hash-checks (SHA256 + BLAKE2b) per the snarkjs README — refuses to proceed on mismatch. |
| Phase 2 (per-circuit) | Circuit-specific, irreversible once finalized | Coordinator endpoints (init, contribute, finalize) shipped; contribute endpoint was publicly reachable during the contribution window; client-side verifyFromInit walked the contribution chain + content-checked IPFS-fetched r1cs/ptau before accepting any contribution. |
| Beacon finalization | Public randomness ensuring no contributor can have predicted the final transcript | Bitcoin block 948,824 (mempool) hash, 10 MiMC iterations. Coordinator cross-checked the block hash against mempool.space and blockstream.info before applying; refused to finalize if confirmation depth < 12 blocks. |
| Canonical bundle | The artifact users actually load | 2,229-record attestation chain (genesis + 2,227 contributions + beacon), pinned to IPFS at bafybeidq2ahzte4sfiqjsmhqta62ufenpppzpch5ppry55tzxzlvltxy2u. Contains withdraw_final.zkey, verification_key.json, withdraw_pre_beacon.zkey, withdraw.r1cs, pot14_final.ptau, and the full 21,931-record audit log. CID is hardcoded as CANONICAL_CEREMONY_CID in the dApp — operator typo is impossible. |
The Phase 2 ceremony finalized at 2,227 community contributions — roughly 2× Tornado Cash's Phase 2 (1,114 contributors) — with a public-randomness final round that closes the late-Sybil collusion window. Pool soundness holds as long as ≥ 1 of those 2,227 was honest. Privacy holds unconditionally regardless.
The 5,000–7,000-constraint withdrawal circuit fits comfortably in pot14 (≈16k constraints, the ptau file actually shipped in the canonical bundle), so smaller follow-up ceremonies are practical for additional circuits.
What this composition closes
Tornado-on-Ethereum shields linkage. Observers can still see a depositor's ETH balance drop by exactly denom when a deposit lands, and a recipient's address gain exactly denom when a withdraw lands — re-deriving participant edges the SNARK was supposed to hide. Tacit shields both axes: linkage via Groth16, ambient amount via Pedersen.
Three side channels Tacit closes that a transparent-asset mixer cannot:
| CHANNEL | TRANSPARENT MIXER (Tornado-on-ETH) | TACIT MIXER |
|---|---|---|
| Pre-deposit balance subtraction | Observer sees X → X − denom exactly when deposit lands; depositor identity preserved |
Depositor's surrounding amounts are Pedersen-hidden via CXFER — there is nothing to subtract from |
| Post-withdraw balance addition | Recipient's address gains exactly denom in cleartext; ties to subsequent spends |
Recipient's first CXFER after withdraw re-blinds the amount; chain analysis loses the trail from that point |
| Peel-off precision | Real wallets do not hold exact multiples of denom. The visible remainder ties the depositor to the deposit |
Tacit's auto-split uses a CXFER, so the carve is itself amount-hidden |
The first CXFER after withdraw is the load-bearing operation here: until the recipient spends the withdrawn UTXO into a confidential transfer, its denom value is publicly known. The dApp explicitly nudges users toward this pattern in the post-withdraw UI.
Anonymity-set considerations
Cryptographic unlinkability is unconditional under Groth16's zero-knowledge property and the hiding/binding assumptions of the underlying commitment + hash construction. Practical unlinkability depends on three things outside the protocol's control:
| FACTOR | WHO CONTROLS IT | DAPP NUDGE |
|---|---|---|
| Anonymity-set size (count of currently-unspent leaves at withdraw time) | Pool volume — emergent | Live count surfaced on withdraw screen; dApp bands are < 5 (warn — easy to single out), < 30 (casual privacy only), < 100 (healthy / well-mixed), 100+ (strong) |
| Bitcoin-level fee linkage | The user, by funding the withdraw tx from a fresh BTC wallet (for self-mix) or letting recipient pay (for pay-to-other) | Self-mix vs pay-to-other detection in withdraw confirm |
| Network-level correlation | The user (Tor + timing discipline) | Out of dApp scope |
A pool that sees 40 deposits a year is structurally sound — proofs verify, conservation holds — but practically not private. The dApp warns about this directly. Users should heed it.
Three deliberate divergences from Tornado Cash
Tacit's withdraw.circom is adapted from Tornado Cash's original circuit, with three intentional changes:
| DIVERGENCE | TORNADO | TACIT | RATIONALE |
|---|---|---|---|
| Leaf encoding | Pedersen(secret, ν) over BabyJubJub |
Poseidon₃(secret, ν, denomination) over BN254 Fr |
Denomination as a public input means one circuit serves all pool sizes — no per-denomination redeployment |
| Nullifier hash | Pedersen(ν) over BabyJubJub |
Poseidon₁(ν) |
Reuses the same Poseidon already used for leaf; smaller circuit |
r_leaf derivation |
Implicit; in-circuit Pedersen check | r_leaf = Poseidon₂(secret, ν) as a public input; external Pedersen check on secp256k1 |
~100× lower constraint cost than in-circuit secp256k1 Multi-Scalar Multiplication (MSM); closes inflation attack via the constraint + the validator's separate Pedersen verify |
The fourth divergence is bind_hash itself — covered above. Together, these are why the constraint count lands at 5–7k, materially smaller than Tornado Cash's original withdrawal circuit.
Status
The mixer is production. Wire format, worker indexing, browser-side Groth16 prover + verifier (snarkjs vendored), deposit + withdraw broadcast flows, and the Phase 2 ceremony pipeline are all shipped. From the repo's published status:
- ☑ Wire format + envelope opcodes (T_DEPOSIT, T_WITHDRAW)
- ☑ Worker indexing + KV state (per-pool init, leaves, nullifiers)
- ☑ Browser-side Groth16 prover + verifier (snarkjs vendored)
- ☑ Deposit + withdraw broadcast flows
- ☑ Indexer rejection-path determinism (worker
bind_hashrecompute matches dApp; SPEC §11 normative) - ☑ Reorg safety (
MIXER_DEPOSIT_CONFIRMATION_DEPTH = 3gate) - ☑ Recent-roots ring buffer (
POOL_RECENT_ROOTS_WINDOW = 32) - ☑ Anonymity-set warning UI in withdraw confirm
- ☑ Privacy-hygiene UX nudges (self-mix vs pay-to-other detection)
- ☑ Deposit auto-split for non-exact denominations
- ☑ Deposit-record export / import
- ☑ vk content-hash check against IPFS CID
- ☑ Phase 1 ptau swapped to verified Hermez ceremony
- ☑ Phase 2 ceremony coordinator (init / contribute / finalize) + auth
- ☑ Client-side
verifyFromInitbefore contribute - ☑ Phase 2 ceremony finalized + beacon applied (2026-05-11, block 948,824 (mempool))
- ☑ 108 mixer-specific tests across 8
mixer-*test files - △ Deterministic
(secret, ν)derivation from privkey — paused; current behavior matches Tornado / Privacy Pools (secrets must be backed up out-of-band)
The one paused item is a usability concern, not a soundness one. Until deterministic derivation lands, users must export (secret, ν) separately and back them up — same UX failure mode as Tornado Cash and Privacy Pools.
Caveats
| CAVEAT | MITIGATION |
|---|---|
| Ceremony trust anchor — soundness rests on ≥ 1 of 2,227 contributors being honest and destroying their Toxic Waste | Contributor diversity is openly observable in the bundle's attestations.json; any party who finds a sole-honest participant in the chain can re-derive the security claim |
| Anonymity-set strength scales with per-pool volume | dApp surfaces live count and warns below thresholds; users should heed the warnings, not the protocol |
| Mixer mixes Tacit assets, not native BTC | Wrapping into a Tacit asset is a separate trust assumption (single-issuer wBTC, federated mint, sidechain peg) |
| Operational privacy is user-discipline-dependent for self-mix | Pay-to-other has no chain-graph link between depositor and recipient; self-mix requires a fresh BTC wallet for the withdraw OR a relayer |
| Mixer notes are minted in your browser. Whatever bytes mint the note are in the trust path | Load from a pinned IPFS CID you have verified (or self-host); avoid forks and typo-squats |
Trade-offs versus adjacent designs
| DESIGN | RELATIONSHIP TO TACIT MIXER |
|---|---|
| Tornado Cash (Ethereum) | Same circuit family, three deliberate divergences (above). Shields one axis (linkage). |
| Citrea / BitVM / BitVMX / Alpen / Strudel | Bitcoin rollups / optimistic SNARK-verification proposals. A Tornado-style mixer can be (and likely will be) built on these, but it inherits the rollup's trust model — challenge-game operator set, fraud proofs, BitVM's 1-of-n assumption. Tacit's mixer reads from L1 envelope data and computes the answer deterministically; no operator set. |
| Ark / Spark / Lava | Shared off-chain UTXO pools with periodic L1 settlements. Provide some privacy properties via UTXO virtualization, but require an Ark Service Provider (ASP) federation. Different mechanism (off-chain virtual UTXOs with a coordinator), not a zero-knowledge anonymity set. |
| Taproot Assets (Lightning Labs) | Currently transparent — same shape as Runes for amount disclosure. Research on adding confidentiality exists but is not shipped. If/when it ships a confidential amount layer + a mixer on top, that would be a direct peer to Tacit's mixer. |
| CoinJoin variants (Wasabi / Whirlpool / JoinMarket) | Cooperative-spend on Bitcoin L1. No anonymity set growing over time, no zero-knowledge. Different category. |
| Cashu / Fedimint mints | Off-L1 with mint operators. Different trust model (mint federation, custodial). |
Tacit composes three specific things — indexer-validated meta-protocol, confidential amounts via Pedersen, Tornado-style shielded pool — on Bitcoin L1. We have not identified a live production peer that combines all three.
References
- Tacit mixer specification — MIXER.md (z0r0z/tacit, MIT)
- Jens Groth — On the Size of Pairing-based Non-interactive Arguments (2016) — eprint.iacr.org/2016/260
- Grassi, Khovratovich, Rechberger, Roy, Schofnegger — Poseidon: A New Hash Function for Zero-Knowledge Proof Systems (2019) — eprint.iacr.org/2019/458
- Tornado Cash core circuit and contracts — github.com/tornadocash/tornado-core
- Buterin, Illum, Nadler, Schwarz-Schilling, Sole — Blockchain Privacy and Regulatory Compliance: Towards a Practical Equilibrium (Privacy Pools, 2023) — papers.ssrn.com/sol3/papers.cfm?abstract_id=4563364
snarkjs(browser-side Groth16 prover/verifier) — github.com/iden3/snarkjscircom(circuit DSL used forwithdraw.circom) — github.com/iden3/circom- Polygon Hermez Powers of Tau ceremony (
pot14) — github.com/iden3/snarkjs#7-prepare-phase-2 - Tacit canonical Phase 2 ceremony bundle — IPFS CID
bafybeidq2ahzte4sfiqjsmhqta62ufenpppzpch5ppry55tzxzlvltxy2u - DNZN — Protocol, Mechanisms, Operations, Risks