Confidential tokens
verifiable on bitcoin
private amounts · no federation · privkey-only recovery
1
Connect a wallet
Use Xverse / UniSat / Leather (one click), import a privkey, or — on signet — let the dApp generate one and faucet some sats.
2
Etch a confidential asset
Pick a ticker, supply, and decimals. The supply is hidden on-chain — only you (and anyone you choose to tell) know the amount.
3
Transfer privately
Privacy is layered — pick what fits the use case. Amounts hidden on every transfer (default). Shielded address (tcs1…) hides the recipient on chain via per-tx unique markers (opt-in, paste in place of pubkey). Mixer pool (T_DEPOSIT / T_WITHDRAW) breaks the on-chain link between deposit and withdrawal for full anonymity-set unlinkability (live in production). Merchants can keep public transfers + shielded balances; users wanting full privacy can layer the lot.
tip loading…
Public key
Address
explorer ↗
Shielded i
Balance
sats
Manage wallet
Tacit needs its own signing key in this browser — confidential transactions use primitives existing Bitcoin wallets don't expose. This is the key that controls your tacit assets; export and back it up before holding value, since clearing browser storage loses it.
Advanced: chain indexer
The dapp rotates across mempool.space + blockstream.info by default; both are public Esplora endpoints with per-IP rate limits. To avoid 429s on heavy use, point at your own Esplora (Start9, Umbrel, Citadel, or a self-hosted node) — your URL takes priority and the public endpoints become fallback only.
Custom Esplora URL (for the active network)
Off-chain in the metadata blob; ticker stays the on-chain identity. Renderers display "Name (TICKER)" when both are set.
Why is the supply hidden? · what gets pinned
The supply is committed to the chain via a 33-byte cryptographic commitment plus a ~700-byte zero-knowledge proof that the value is a valid 64-bit integer — only you know the actual number; observers see only the proof. The blinding factor that hides the supply is derived from your wallet's privkey, so a fresh device with just your key recovers the etched supply from chain data alone — no localStorage backup needed. Max supply per asset is ~1.84 × 10¹⁹ base units, which covers any realistic decimals setting. Etch is a 2-tx flow (commit + reveal) and broadcasts in seconds. Optional image / metadata is bundled into the envelope and propagates to recipients alongside the supply commitment.
etch · public mint
Deploy a token with a fixed cap and per-mint amount. You receive zero tokens at deploy — anyone can mint until the cap fills.
Advanced: mint window (optional)
Restrict T_PMINTs to a Bitcoin block-height window. Leave both blank (or 0) for open-ended minting until the cap fills — that's the default. Both fields are permanent and locked at deploy; can't be changed after broadcast.
Validation: start must be ≥ deploy block + 1, end must be > start, both must fit in u32. If start = 0 the dapp uses deploy block + 1; if end = 0 minting stays open until the cap fills. SPEC §5.8.
⚠ Cap and per-mint amount are permanent; per-mint must divide the cap evenly. Minting opens at the next confirmed block after this deploy lands (or at the start height above, if set).
How is this different from a confidential etch?
Mints publicly reveal (amount, blinding), so the cap is auditable from chain alone — supply is fully observable, unlike CETCH where supply is committed but hidden. The deploy is a 2-tx flow (commit + reveal). Once the cap fills, no more T_PMINTs are accepted. SPEC §5.8 / §5.9.
send
Confidential transfer of one asset. Observers always see valid math but never the amount. Optionally hide the recipient too by pasting their shielded address (tcs1…) instead of their pubkey — on-chain recipient marker becomes a per-tx unique address. Recipients auto-discover their balance from chain data on their next scan; share-link is optional for pubkey sends and recommended for shielded sends.
Paste the recipient's pubkey (from their Wallet tab) for a standard confidential send, or their shielded address for an opt-in privacy upgrade that hides the recipient on-chain. Shielded addresses only apply to tacit token transfers — plain BTC sends use a regular bc1q…/tb1q… address.
Choose a specific lot to spend (advanced)
When set, the amount field is locked to the chosen lot's exact value (whole lot is spent, change returns to you). Useful for cancelling a stale listing's lot without touching your full balance.
Review your transfer details below before broadcasting — once sent, it can't be undone.
drops · run an airdrop
Send a tacit token to holders of any Ethereum ERC-20. Drop in an Etherscan holder CSV, generate a treasury, launch — recipients tip ~3,500 sats each to claim, you don't pay per-batch fees.
1 Pick the token you're airdropping

The tacit asset to send. Source amounts get floor-truncated to this asset's decimals.

2 Add ETH holder snapshots

Etherscan token-holder CSV exports (HolderAddress,Balance,…) work as-is. Add as many as you want — same-address balances are summed across all sources.

3 Exclude addresses · optional

Drop the zero/dead address, CEX hot wallets, and your own team wallets so they don't get a share. One address per line.

Show exclusion list
4 Make a fresh treasury wallet

This hot key signs every payout. Generate a dedicated one so a leak doesn't expose your main wallet — back up the privkey now, you only see it once.

5 Launch your drop

One click builds the merkle snapshot, pins it to IPFS, and saves a drop card below. After launch: fund the treasury, switch to it, publish to discovery, enable auto-fulfil.

run steps individually
What happens after I launch?
  1. Your drop card appears below — while main wallet is still active, click Fund: TAC → to send payout supply to the treasury pubkey, and Fund: sats → to send bootstrap sats to its bech32 address.
  2. Switch active wallet to the treasury (button on the generated key panel above).
  3. From the treasury, click Publish to discovery on the drop card. ⚠ Publishing before switching routes recipient tips to the wrong pubkey.
  4. Click Fulfil claims → ⚙ Auto-fulfil → Enable. Walk away — the dapp pulls the queue and broadcasts batches automatically.

Your drops

Saved in this browser. Export JSON on each row to back up the local fulfilled[] ledger; import below to restore on another device.
No drops saved yet.
Advanced · on-chain claim pool (T_DROP)
Alternative to the recipient-tipped flow above: lock the payout supply into a public on-chain pool. Recipients self-claim and pay their own ~$5-10 Bitcoin tx fee. Issuer pays once (the pool-creation tx) instead of one Bitcoin tx per batch of 7. The cap is enforced on chain. Optional eligibility gate (paste a snapshot's merkle root) restricts claims to specific ETH addresses; leaving it empty opens the pool first-come-first-served.
Trade-off: each T_DCLAIM is publicly attributable (claimer address + leaf_index on chain). For a CETCH-rooted asset, T_DROP does not change asset_id or migrate holders — it's a one-tx pool that spends some of your existing supply.
Cap MUST be evenly divisible by per-claim. max_claims = cap ÷ per_claim. For merkle-gated drops, size cap to the count of eligible leaves × per-claim. For open FCFS drops, set a non-zero expiry so unclaimed remainder is recoverable by you via Reclaim once expiry passes (SPEC §5.12.1).
Merkle-gated: only addresses in the snapshot can claim. Open FCFS (empty merkle root): anyone can claim, first-come-first-serve. Open FCFS requires an expiry block so unclaimed tokens can be reclaimed by you afterward.

Active on-chain pools

Pools created via T_DROP on this network. Updated when the worker indexer scans (every ~5 min) or via Refresh.
No on-chain pools loaded · click Refresh.
Trust model · self-host the worker
The shared worker is a convenience layer with no trust authority over your drop. Snapshots live on IPFS (content-addressed, verifiable from chain + CID alone). Drop records are local to your browser (back them up via Export JSON; restore via Import). The worker's claim queue is dumb storage — it can withhold tuples but cannot forge them, and recipients can always hand-deliver tuples instead of submitting through the queue. Setting WORKER_BASE = '' disables it; the dApp falls back to manual flows. To run your own worker for a campaign, see worker/README.md (deploys to a free Cloudflare Workers account in ~5 minutes).
claim · airdrop recipient
Connect your Ethereum wallet to see airdrops you're eligible for, then pick one to start a guided claim. No gas, no Ethereum transaction.
Compatible wallets
Most Ethereum wallets work: MetaMask, Rainbow, Rabby, Coinbase Wallet, Trust, Frame. Smart-contract wallets (Ambire, Safe, Argent) work too — those need to be on the chain that holds the contract; regular wallets work regardless of current chain.

Available drops

Live drops on .
no drops loaded yet · click Refresh
Other ways to claim
Have a private drop or a direct share link from the issuer? Use the options below.
Self-claim on-chain (you pay your own Bitcoin tx fee)
For drops where the issuer locked tokens into a public claim pool on Bitcoin. Anyone in the snapshot can self-claim first-come-first-served until the cap runs out. For drops with an eligibility list, you'll also need to sign with your Ethereum wallet (use the discovery list above to do that first). Cost: roughly $5-10 in Bitcoin tx fees per claim, paid by you instead of the issuer.
no on-chain drops loaded yet · click Refresh
Have a merkle root + CID? Manual entry
…or paste / upload the snapshot JSON directly (fallback if IPFS gateways are unreachable)
Useful when corporate proxies block IPFS gateways or for offline review. The dapp still recomputes the merkle root locally and refuses any snapshot whose rows don't match the declared root.
holdings
activity · local log
Local-only history of broadcasts you've signed on this device (etch / transfer / mint / burn / received). Cleared if you wipe browser storage; the chain is the source of truth.
market
how it works non-custodial · Bitcoin-native · zero protocol fee

Trade. Type an amount, click Swap. Every fill is a single Bitcoin transaction — your asset and the counterparty's sats change hands in the same tx or neither moves. No bridges, no custodians, no settlement layer.

Fees. Zero protocol fee. You pay only Bitcoin miner fees, shown in the preview before you sign.

No MEV. Orders are pre-signed by the maker against a specific outpoint, so the only valid fill is the one that spends it. No sequencer to reorder you, no privileged relayer, no protocol surface to bribe. Bitcoin's PoW is the only orderer.

Limit + rest. If your swap doesn't fully fill at your limit price — including when nothing matches — the unfilled remainder rests as a passive order for 24h. One click either fills, posts a tradeable order, or both.

Private amounts. Trade sizes are hidden on-chain via Pedersen commitments + bulletproof rangeproofs. Asset IDs and tickers are public; the size you move is not.

Custody. Your signing key never leaves this browser. Tacit can't move your funds — only your wallet can.

public mints · fair launch assets · confidential supply
public mints · fair launch
Anyone can mint a fixed tranche until the cap fills. Supply is publicly auditable from chain alone.
assets
Confidential tokens on Bitcoin. Tickers and metadata are public; balances stay hidden by default. Some issuers publish an IPFS attestation that reveals total supply — filter by "Supply proven" below. Click an asset_id chip to copy.
mixer · shielded pools
Private send for any tacit asset. Deposit a fixed amount into a shared pool, wait, then withdraw to a fresh address — a zero-knowledge proof breaks the on-chain link between the two halves. The zero-knowledge prover and verifier run live in your browser.
pools
Initialized pools are listed below. Each row shows total deposits, total withdrawals, current anonymity-set size, and the verifying-key CID. Empty list = no pools have been initialized on this network yet.
No pools indexed yet.
deposit
Send the pool's exact denomination from your wallet into the pool. Your browser generates a private deposit record — back it up before broadcasting; without it, the deposit can never be withdrawn.
Backup all deposit records. Stored in localStorage per network — wiping the browser's storage = losing the secret pair = funds permanently un-withdrawable. Download a JSON of every deposit you've made on this network, then re-import on a fresh device.
wrap sats → cBTC.zk slot
Lock sats into a self-custody slot, get a cBTC.zk note in the matching mixer pool. Trustless 1:1 wrap — no federation, no co-signer. Lost notes lock the backing sats permanently (like losing a Bitcoin key); back up your slot records.
Slot records on this browser. Each row is one wrapped slot: the secret material needed to redeem the backing sats. Stored in localStorage per network — back up before clearing browser data.
No slots wrapped yet on this network.
send slot to recipient
Rotate a live slot to a recipient. New secrets are encrypted to their viewing pubkey; their dapp auto-detects incoming slots on next load. Leave recipient empty to refresh your own slot's keys (useful if the secrets may have been exposed).
Your incoming viewing pubkey (give this to senders so they can send slots to you; derived deterministically from your wallet privkey via HKDF):
redeem slot → sats
Burn a live cBTC.zk slot to recover the backing sats. Generates a Groth16 proof (5–15s on desktop, longer on mobile) that you own the leaf, then spends the slot UTXO under r_leaf. Trustless single-tx unwrap.
deposit slot → cBTC.tac (v1 lien model)
Bond LP-shares against a live cBTC.zk slot to mint cBTC.tac — a tacit-asset wrapper for BTC. cBTC.tac is transferable amount-privately and tradeable on the marketplace.
withdraw cBTC.tac → sats (v1 lien release)
Close an active cBTC.tac position. Spends the underlying slot UTXO atomically with the cBTC.tac burn; sats land at the chosen payout address. Same-wallet redemption only in v1 (the depositor's r_btc is required to sign the slot input).
Your cBTC.tac positions on this browser. Each row is one open or closed position: slot leaf, denomination, TAC bond, current state, deposit txid. Back up before clearing browser data — losing a position record means losing the mint blinding needed to identify your cBTC.tac UTXO.
No cBTC.tac positions yet on this network.
withdraw
Withdraw a deposit to a fresh address. Pick a saved deposit, paste a deposit record JSON, or import a share-link. The privacy set size — how many other deposits yours hides among — is shown beneath each pool.
ceremony · contribute to the trusted setup
The tacit mixer's privacy was bootstrapped by a multi-party trusted setup ceremony. Anyone could contribute — and as long as one participant across the chain was honest, the mixer is sound. The chain is now finalized; the canonical verifying key is pinned and used by every mixer envelope. The AMM has its own per-circuit ceremony, contributable from the chip on every tab.
Loading…
initialize a new pool
Anyone can open a pool for any asset and any deposit size. The first confirmed pool for a given asset + amount wins. The verifying key and ceremony transcript are pre-pinned and locked to the canonical Phase 2 setup — pick an asset, type a denomination, broadcast.
Enter the deposit size in whole tokens. Pick an asset first to see the resolved on-chain u64.
Both CIDs are locked to the canonical Phase 2 trusted setup — 2,227 community contributions, Bitcoin-block beacon, finalized chain. Every pool for this circuit shares the same verifying key. Audit bundle on IPFS ↗
AMM trusted setup ceremony in progress. Swap + Add Liquidity + Remove Liquidity unlock once the canonical verifying key is published. Pools and your LP positions are visible now; the buttons below activate when the dapp verifies the ceremony attestation.
pool · AMM
Constant-product AMM (x·y=k) over confidential tacit assets. Pool reserves are public so any indexer reconstructs state from chain alone; LP shares are amount-hidden via Pedersen + bulletproof. Two swap paths share these pools: T_SWAP_VAR for per-trade fills against the spot curve (cleartext amount, no Groth16, ships independent of the ceremony) and T_SWAP_BATCH for batched uniform-price settlement (hidden per-trader amounts via Groth16, MEV-resistant, ceremony-gated). Pool-side fee is a single bps knob (default 30 = 0.3%); LP shares mint by the Uniswap V2 formula. Every swap settles in one Bitcoin commit-reveal pair, no sequencer.
swap
Trade against a pool's curve. The router compares the direct pool against multi-hop paths (up to 4 pools) and picks whichever yields more — a single-hop swap settles via T_SWAP_VAR, a multi-hop route settles via T_SWAP_ROUTE atomically across every pool it touches. Either way it's one Bitcoin commit-reveal pair with a slippage floor on the final delivery — if the route can't clear your floor before the reveal lands, the tx fails on chain and your input UTXO stays where it was.
pools
All AMM pools indexed by the worker on this network. Click a row to copy its pool_id. Empty = no pools confirmed yet on this network.
Loading pools…
add liquidity
Deposit a proportional pair of assets into a pool. The dapp computes your share of the LP supply using the Uniswap V2 mint formula (min(ΔA·S/Rₐ, ΔB·S/R_b)) and binds the share commitment to your wallet via an XCurve sigma proof.
your LP positions
LP-share UTXOs in your wallet across pools on this network. Burn shares with Remove Liquidity to recover your proportional reserves.
No LP positions found in this wallet.
remove liquidity
Burn one of your LP-share UTXOs and receive a proportional withdrawal of both reserve assets. Rounding remainders donate to remaining LPs (Uniswap V2 convention).
AMM trusted setup ceremony in progress. Farm bonding + harvest unlock once the canonical verifying key is published — farm metadata and your bonds are visible now, but stake/harvest/unbond actions activate when the ceremony attestation verifies.
farms

Stake LP shares against a farm to earn streamed reward emissions. Each farm runs over a fixed Bitcoin block range (start_height → end_height), with rewards distributed pro-rata by bonded share. Harvest accrued rewards anytime without unbonding; unbond returns your LP shares plus any final reward. Launchers can reclaim unspent treasury 7 days post-end.

your bonds
actions (CLI for now)
Bond / Harvest / Unbond / Refund actions are wired in dapp/amm-farm-actions.js (buildAndBroadcastLpBond, etc.) but not yet bound to buttons here. Until the next dapp build, use the encoders from a script context or the tests/amm-farm-e2e-signet.mjs harness. The data view above polls /farms and /farm/<id>/bonds?bonder=<your_pubkey> on the worker for live state.
protocol

An indexer-validated meta-protocol on Bitcoin that scales the Runes/Ordinals pattern past plain tokens. Privacy is layered so each user picks the level that fits their use case: a merchant might keep public transfers + shielded balances for auditability, while a privacy-conscious user can layer shielded amounts + shielded addresses + mixer-pool round-trips for full unlinkability at every endpoint. Tacit applies the same indexer-consensus trust model across the privacy stack and the wider surface: shielded amounts (Pedersen + bulletproofs + kernel signatures, default on every transfer) and an opt-in shielded address primitive (BIP-341-style blinded-pubkey commits for per-tx unique recipient markers); a shielded mixer pool (Poseidon Merkle tree + Groth16 + nullifiers) for anonymous spend that breaks the on-chain link between deposit and withdrawal; a native AMM (uniform-clearing-price block-batched market with confidential per-trader amounts, plus a per-trade variable-amount path that needs no Groth16); trustless wrapped BTC (cBTC.zk locks real BTC at a key derived from a mixer leaf's secret) and fungible wrapped BTC (cBTC.tac layers an LP-share lien with TAC over-collateralization); atomic single-tx OTC settlement; permissionless fair-launch mints; on-chain claim pools. Every surface settles in regular Bitcoin transactions.

Validation model

Like every Bitcoin token meta-protocol (Runes, BRC-20, Ordinals, Taproot Assets, RGB, Counterparty), tacit's token rules are not enforced by Bitcoin nodes. Bitcoin validates the underlying tx structure and Taproot signatures; an external indexer validates the token-level rules (commitments, rangeproofs, kernel signatures, asset_id consistency, AMM pool reserves, pool nullifier sets, slot key derivations, lien states).

Architecturally tacit is an indexer-validated meta-protocol — same family as Runes, BRC-20, and Ordinals. All data needed to validate any UTXO lives on Bitcoin's chain. An indexer with chain-only access can recursively walk every UTXO back to its origin (CETCH, T_PETCH, mixer withdraw, AMM pool, cBTC.zk slot mint) and reach the same verdict as any other indexer running the same rules. No consensus change, no federation, no out-of-band proof exchange.

This is distinct from proof-distribution protocols (RGB, Taproot Assets) where the recipient must receive validity proofs out-of-band, and from fully-validated chains (Liquid, Mimblewimble) where consensus enforces the rules natively. Tacit's contribution beyond plain tokens is leveraging the same consensus-of-indexers substrate into a richer surface: confidential value, AMM market-making, and a native collateral substrate that backs trustless wrapped BTC. The market-validated value of indexer-defined assets (the same kind of value a Rune carries) becomes the bond mechanism for cBTC.tac's fungibility leg — same trust model, structurally aligned. See the CIRCUITS.md overview for how the two Groth16 circuit families (anonymous-spend + amount-confidentiality) compose across these surfaces.

Operations

v1 envelope version is 0x01. Every op is a 2-tx commit/reveal pair carrying the envelope inside tx.vin[0].witness[1] of the reveal. SPEC.md §1.1 is the canonical opcode table — full assignments, drafted/reserved status, source amendment.

Core asset lifecycle

0x21CETCHissue new asset; hidden initial supply, optionally mintable 0x23CXFERconfidential transfer; split into 1, 2, 4, or 8 outputs 0x24T_MINTissuer-signed additional supply on a mintable CETCH 0x25T_BURNany holder destroys supply; public burned_amount 0x27T_PETCHpermissionless-mint deployment: cap + per-mint amount + height window; deployer gets zero tokens 0x28T_PMINTanyone mints exactly mint_limit; reveals (amount, blinding) so the cap is auditable

Marketplace (atomic OTC + orderbook)

0x26T_AXFERCXFER variant with aux BTC inputs — single-tx atomic OTC + preauth sales + batched multi-fill takes (§5.7.8.1) 0x37T_AXFER_VARvariable-amount atomic settlement — one signed offer up to X, any taker fills any chunk in [min, X] (§5.7.6.1 / §5.7.9)

Mixer (anonymous spend)

0x29T_DEPOSITlock a fixed-denomination UTXO into a shielded pool (Poseidon leaf) 0x2AT_WITHDRAWGroth16-gated mint from a pool; recipient unlinkable to any specific deposit 0x2BT_DROPlock existing supply into a public claim pool with optional Merkle eligibility gate 0x2CT_DCLAIMpermissionless claim from a drop; reveals (per_claim, blinding) for audit

Native AMM (AMM.md)

0x2DT_LP_ADDadd liquidity to a confidential AMM pool; variant=1 sentinel doubles as POOL_INIT 0x2ET_LP_REMOVEburn LP shares for proportional withdrawal of pool reserves 0x2FT_SWAP_BATCHbatched uniform-price settlement; N intents at one P_clear via Groth16 over hidden per-trader amounts 0x30T_INTENT_ATTESTscope-generic preconfirmation channel attestation (~30 s soft-confirm UX) 0x31T_PROTOCOL_FEE_CLAIMmint accrued protocol fee shares to the founder-pinned recipient (or insurance-pool sentinel) 0x32T_SWAP_VARper-trade variable-amount swap; cleartext amounts against curve, no Groth16, no ceremony coupling 0x33T_SWAP_ROUTEatomic multi-hop AMM routing (2..4 pools in one Bitcoin tx); reuses T_SWAP_VAR cryptography

LP-staking farms (SPEC-AMM-FARM-AMENDMENT)

0x34T_FARM_INITlauncher-funded LP-staking reward farm; virtual treasury 0x35T_LP_BONDbond lp_asset_id shares against a farm; per-bond worker-indexed record 0x36T_LP_UNBONDsettle bond: mint fresh LP shares + reward UTXO 0x3BT_LP_HARVESTclaim accrued reward without unbonding (MasterChef harvest() equivalent) 0x3ET_FARM_REFUNDlauncher reclaims unspent treasury after end_height + ~7 days

Wrapper convention + trustless wrapped BTC

0x38T_WRAPPER_ATTESTwrapper attestation — issuer binds a tacit asset to an external collateral claim (§5.19) 0x43T_SLOT_MINTcBTC.zk: lock BTC at K_btc = r_leaf · G_secp256k1, derived from a mixer leaf's secret 0x44T_SLOT_BURNcBTC.zk: redeem BTC by proving anonymous-unique-spend via the mixer's withdraw circuit 0x45T_SLOT_ROTATEcBTC.zk: atomic key rotation / transfer of a slot 0x46T_SLOT_SPLITcBTC.zk: atomic 1→N slot split, ΣD_new = D_old 0x47T_SLOT_MERGEcBTC.zk: atomic N→1 slot merge

Fungible wrapped BTC via collateralized lien

0x49T_CBTC_TAC_DEPOSITcBTC.tac: lien on canonical (cBTC.zk, TAC) LP-share → mint fungible cBTC.tac 0x4AT_CBTC_TAC_WITHDRAWcooperative unwind: burn cBTC.tac → release LP-share lien + spend slot 0x4BT_CBTC_TAC_FORCE_CLOSEpermissionless transfer to claim pool when collateral ratio falls below threshold 0x57T_CBTC_TAC_DEPOSIT_ATOMICatomic LP_ADD + cBTC.tac mint in one envelope 0x58T_CBTC_TAC_WITHDRAW_ATOMICatomic cBTC.tac burn + LP_REMOVE + slot redeem

Wire-format examples below cover the core four (CETCH / CXFER / T_MINT / T_BURN). The remaining opcodes follow the same envelope shape with their own payload layouts and validator rules — see SPEC.md §5.7–§5.19 for the byte-exact specifications.

Marketplace primitives live in §5.7 and route through T_AXFER / T_AXFER_VAR settlement — no separate opcodes needed: atomic intents (§5.7.6 — seller publishes one signed offer, any taker claims it within 5 min and settles atomically on Bitcoin); preauth sales (§5.7.8 — seller pre-signs everything, then goes offline; any buyer settles whenever); batched preauth-take (§5.7.8.1 — buyer sweeps N preauths in ONE reveal tx, ~70% Bitcoin-fee reduction vs N separate settlements; pure flow-level optimization leveraging position-independent SIGHASH_SINGLE_ACP); variable-amount intents (§5.7.6.1 — partial-fillable seller offers); and bid intents (§5.7.7 — partial-fillable buyer offers, off-chain coordination, on-chain settlement via T_AXFER/T_AXFER_VAR). All marketplace flows share the same kernel-sig + rangeproof crypto as base CXFER; the difference is the coordination layer, not the on-chain primitive.

Cryptography

The full ZK + commitment toolkit composes across all surfaces. See CIRCUITS.md for the canonical walkthrough.

On-chain commitments — Pedersen on secp256k1

Commitments: C = a·H + r·G, additively homomorphic. H is a NUMS generator (no known discrete log w.r.t. G) derived deterministically by hash-to-curve from the seed "tacit-generator-H-v1". The protocol carries C on chain; (a, r) stay private.

Rangeproofs — aggregated Bulletproofs

Bünz et al. 2017, at n=64 bits — each committed value proven to lie in [0, 2⁶⁴). A single proof covers m ∈ {1, 2, 4, 8} commitments simultaneously via the inner-product argument, witness size O(log(n·m)). On secp256k1: 688 B at m=1, 754 B at m=2, 820 B at m=4, 886 B at m=8. ~250 ms prove, ~150 ms verify in-browser per proof; the indexer batches multiple proofs into a single multi-scalar multiplication for sub-linear amortized cost. 64-bit range bounds any single Pedersen commitment to ~184 billion display units at 8 decimals.

ZK circuits — two families, one stack

All Groth16 circuits run over BN254 with Phase 1 inherited from the Polygon Hermez ceremony. Two families do all the in-circuit work:

  • Anonymous unique-spend — the mixer's withdraw.circom (Poseidon-Merkle leaf membership + nullifier reveal, ~3K constraints, pot14 Phase 1). Reused without modification for cBTC.zk slot spending (T_SLOT_BURN / ROTATE / SPLIT / MERGE) — the slot's spending key K_btc = r_leaf · G_secp256k1 is derived from the leaf's own Poseidon secret, so one secret proves mixer-set membership and signs the backing BTC UTXO.
  • Amount confidentiality — the AMM's three circuits (amm_lp_add 5K, amm_lp_remove 10K, amm_swap_batch 171K @ N≤16 traders; pot18 Phase 1). BabyJubJub Pedersen openings + range proofs + in-circuit AMM arithmetic (clearing-price derivation from private aggregates in the batch case).

Chain commitments live on secp256k1; in-circuit work lives on BabyJubJub (the embedded curve over BN254 Fr). A 169-byte Camenisch–Stadler sigma proof binds the two, out-of-circuit, with no trusted setup. This avoids ~1M constraints per opening that an in-circuit secp256k1 gadget would cost.

Shielded-address primitive — blinded-pubkey commits

Opt-in BIP-341-style construction: commit = recipient_pubkey + blinding · G where blinding = HMAC(wallet.priv, domain || op-specific anchor). The commit serves as both a Schnorr verification key (if the op signs under the recipient) AND a P2TR output key (so payouts go to P2TR(x_only(commit))). Hides the recipient pubkey itself — on-chain markers sit at per-transaction unique addresses with no apparent link to the recipient's published identity. Same crypto as BIP-340 / BIP-341 / BIP-352; no new ceremony. Composes orthogonally with shielded amounts; both schemes remain valid indefinitely, wallets pick at tx-build time based on the recipient's signaled capability.

Wire format (Taproot envelope)

Rangeproofs and aggregated payloads don't fit in 80-byte OP_RETURN, so payload moves into a Taproot script-path leaf. Each operation is 2 transactions: a commit tx that creates a P2TR output committed to the envelope's leaf hash (internal pubkey = BIP-341 NUMS, so script-path is the only spend), and a reveal tx that spends the P2TR via script-path, exposing the envelope script in the witness. Indexers scan tx.vin[0].witness[1] for envelopes.

Envelope leaf script

32B<signing pubkey x-only>wallet pubkey allowed to sign reveal acOP_CHECKSIGverifies witness signature 00 63OP_FALSE OP_IFenter unreachable data block 05"TACIT"protocol magic 01versionenvelope version (0x01) ..payload chunks (≤520B each)opcode + body + rangeproof 68OP_ENDIFend

CETCH payload (opcode 0x21) — initial issuance

21T_CETCHconfidential etch Lticker length1 byte (1–16) ..tickerUTF-8 ddecimals1 byte (0–8) 33Bcommitment Ca·H + r·G 8Bamount_ctu64 LE supply XOR keystream — etcher-only, recoverable from chain 2Brp_lenu16 LE; rangeproof byte count (688 for m=1) ..rangeproofaggregated bulletproof, m=1, n=64 32Bmint_authorityx-only pubkey or all-zero (non-mintable) 2Bimage_lenu16 LE (0–256) ..image_uriopaque UTF-8; image OR metadata-blob CID; renderers MUST validate scheme

The wire format treats image_uri as opaque UTF-8 bytes. The decoder accepts any valid UTF-8 ≤256 bytes — it does not enforce a URI scheme. This leaves room for future schemes (ar://, ipns://, etc.) without a wire-format change. Renderers MUST validate before display: tacit's UI accepts only ipfs://CID and bare CIDs (Qm…/bafy…), and rejects http:, https:, javascript:, data:, and other schemes. Direct https:// images are rejected on purpose — every wallet that views the asset would fetch from the issuer-controlled host, which is an IP-correlation beacon. Forcing IPFS routes traffic through the configured gateway, one fixed origin that the CSP locks down at img-src.

Asset_id = sha256(reveal_txid_BE ‖ 0_LE) (32 bytes). Supply commitment lives at vout 0 of the reveal tx. If mint_authority is all-zero, the asset is fixed-supply (BTC-style); otherwise the named pubkey can issue more via T_MINT.

CXFER payload (opcode 0x23) — confidential transfer

23T_CXFERconfidential transfer 32Basset_idsingle asset 64Bkernel_sigBIP-340 Schnorr; proves balance closes Noutput count1, 2, 4, or 8 (must be a power of 2 for aggregation) 33BC_icommitment for vout i 8Bamount_ct_iu64 LE plaintext XOR keystream — recoverable from chain 2Brp_lenu16 LE; aggregated rangeproof byte count ..rangeproofsingle bulletproof covering all N commitments

MINT payload (opcode 0x24) — issue more supply (mintable assets only)

24T_MINTsigned by mint_authority 32Basset_idmust equal sha256(etch_txid ‖ 0) 32Betch_txidparent CETCH; validator confirms it's mintable 33Bcommitmenta·H + r·G for the new supply 8Bamount_ctissuer-self keystream; only the authority can decrypt 2Brp_lenu16 LE ..rangeproofm=1 bulletproof on the new commitment 64Bissuer_sigBIP-340 over sha256("tacit-mint-v1" ‖ asset_id ‖ commit_anchor(36B) ‖ commitment ‖ amount_ct), under mint_authority

commit_anchor = commit_tx.vin[0].txid_BE ‖ commit_tx.vin[0].vout_LE (the same anchor the issuer uses for the mint blinding/keystream). Binding it into mint_msg ties the issuer's signature to a specific commit/reveal pair so a witnessed mint envelope cannot be replayed into a different commit/reveal pair at an attacker's address.

BURN payload (opcode 0x25) — destroy supply, public amount

25T_BURNany holder; emits a public burned_amount 32Basset_idsingle asset 8Bburned_amountu64 LE plaintext — public, auditable 64Bkernel_sigBIP-340; proves Σ C_in = burned·H + Σ C_out Noutput count0, 1, 2, 4, or 8 (N=0 = full burn, no change) 33BC_ichange commitment for vout i 8Bamount_ct_iself keystream for the burn-tx's change output 2Brp_lenu16 LE (0 if N=0) ..rangeproofaggregated bulletproof on N change commitments
How attacks are blocked

Negative amounts (mod N). A "−1000" is just N − 1000 as a scalar; bulletproofs reject any value outside [0, 2⁶⁴). The proof's inner-product argument cannot be satisfied for a value that doesn't fit in 64 bits, so the verifier rejects.

Unbalanced amounts (CXFER). Even with rangeproofs, a sender holding 100 USDV could try to construct outputs (30 to recipient, 200 to themselves) — both with valid rangeproofs — and mint 130 from nothing. Kernel signatures block this:

  • Sender computes excess = Σr_out − Σr_in and signs kernel_msg with priv = excess (BIP-340 Schnorr).
  • Verifier reconstructs E' = ΣC_out − ΣC_in from on-chain commitments. If amounts balance, E' = excess·G and the sig verifies under E'.xonly().
  • If amounts don't balance, E' = δ·H + excess·G with δ ≠ 0. Producing a valid sig would require knowing the discrete log of H w.r.t. G, which is hard since H is NUMS.
  • kernel_msg = sha256("tacit-kernel-v1" ‖ asset_id ‖ N_in ‖ inputs ‖ N_out ‖ outputs ‖ burned_amount_LE), binding the sig to all relevant fields and preventing replay across txs.

Unbalanced amounts (BURN). Same kernel construction with burned_amount made explicit: the verifier checks E' = ΣC_out + burned·H − ΣC_in = excess·G. The public burned_amount field is bound into kernel_msg so claiming a smaller burn than was actually destroyed shifts the message and breaks the sig.

Mint forgery. Only the holder of mint_authority's private key can issue valid T_MINT envelopes for an asset. The validator fetches the parent CETCH, confirms the asset is mintable (mint_authority ≠ zero), and verifies the issuer Schnorr sig under that x-only key. The signed message binds the commit_anchor (commit-tx's first input outpoint) so an observer cannot rewrap the on-chain envelope payload into their own commit/reveal pair. The mint itself is rangeproof-bounded to [0, 2⁶⁴) per envelope; the authority can mint any number of times.

Cross-asset confusion. A sender could declare a CXFER as USDV but spend GOLDC inputs. The indexer fetches each input's parent envelope, reads its declared asset_id (across all four opcodes — CETCH, MINT, CXFER, BURN — via a unified parent-resolver), and rejects the CXFER if any input's asset_id differs from the current claim.

Blinding delivery + amount recovery

Recipient blinding: r_recip = HMAC-SHA256(ECDH(sender_priv, recipient_pub), "tacit-blind-v1" ‖ anchor ‖ vout_LE), where anchor = first_asset_input_txid_BE ‖ first_asset_input_vout_LE. The anchor's per-tx entropy prevents cross-tx commitment correlation: without it, two transfers between the same parties at the same vout would produce identical blindings, and an observer could compute (C₁ − C₂) = (a₁ − a₂)·H to learn the difference of amounts.

Sender's change blinding: r_change = HMAC-SHA256(sender_priv, "tacit-change-v1" ‖ anchor ‖ vout_LE), where vout_LE is the change output's index in the reveal tx (typically 1 for a 2-output CXFER, but parameterized so multi-output CXFERs with N=4 or N=8 can place change at higher vouts). Deterministic from the wallet privkey so it's recoverable from chain alone.

Etcher's supply blinding: r_supply = HMAC-SHA256(etcher_priv, "tacit-etch-v1" ‖ etch_anchor), where etch_anchor = first input outpoint of the commit tx. The anchor predates the envelope (a pre-existing UTXO), breaking the cycle that would otherwise arise from anchoring on the reveal txid. Scanners read it via reveal_tx.vin[0] → fetch commit tx → commit_tx.vin[0].

Each commitment also carries an encrypted amount (8 bytes, u64 LE XOR'd with an HMAC-keystream). For CXFER outputs, the keystream uses ECDH-derived keying for recipients (so recipient can decrypt with their priv + sender pub) and self-derived keying for change. For CETCH supply, the keystream is self-derived from etcher_priv + etch_anchor — only the etcher can decrypt; observers see opaque bytes. After decryption, the wallet verifies C == amount·H + r·G; tampering with the ciphertext makes verification fail.

For self-derived roles (CETCH supply, MINT amount, CXFER change), blinding and keystream are derived under distinct HMAC domain tags (tacit-etch-v1 vs tacit-etch-amount-v1; tacit-mint-blind-v1 vs tacit-mint-amount-v1) so the 8-byte keystream output cannot leak any structure of the 32-byte blinding scalar.

This means share-links are optional notifications, not required for recovery. A freshly-installed wallet with only its privkey can auto-discover its full balance from chain alone — incoming CXFER transfers (via ECDH), its own CXFER change (via self-derived), and its own CETCH supply (via etch-anchor self-derived).

Indexer rules (recursive validation)
  1. For each wallet UTXO, fetch parent tx; read vin[0].witness[1]; decode as tacit envelope.
  2. Recursively validate the outpoint: walk the ancestry back to its CETCH root, validating each step. Memoize results to keep cost O(N) over a chain of length N. Depth-bounded at 200 hops.
  3. Aggregated rangeproof verification. A single bulletproof per envelope covers all m commitments (m ∈ {1,2,4,8}). The walker can additionally batch proofs across the entire ancestry into one multi-scalar multiplication via bpRangeAggBatchVerify, with random per-proof scalars αi, βi ensuring soundness preservation.
  4. For CETCH leaves (T_CETCH): only vout 0 is the supply commitment; no kernel; the rangeproof bounds the initial supply.
  5. For MINT envelopes (T_MINT): vout 0 holds the new supply commitment. Validator confirms asset_id = sha256(etch_txid ‖ 0), recursively validates the CETCH ancestor, requires mint_authority ≠ 0, and verifies the issuer's BIP-340 sig under the authority's x-only pubkey. mint_msg binds commit_anchor (the commit-tx's first input outpoint), preventing replay of the envelope into a different commit/reveal pair.
  6. For CXFER nodes (T_CXFER): every input outpoint must itself recursively validate; every input's parent (CETCH/MINT/CXFER/BURN) must declare the same asset_id; the aggregated rangeproof must verify; the kernel sig must verify under (ΣC_out − ΣC_in).xonly() over the kernel msg.
  7. For BURN nodes (T_BURN): same as CXFER but the verifier reconstructs E' = ΣC_out + burned_amount·H − ΣC_in. N=0 is allowed (full burn, no change output, no rangeproof).
  8. Any failure anywhere in the ancestry → the UTXO is flagged as inflation attempt and excluded from balance. markAll propagates the verdict to every sibling output of a failed envelope.
  9. Resolve (amount, blinding) for the commitment: try local opening first, then trial-decrypt the on-chain amount_ct (ECDH against sender pubkey for incoming, self-derived for own change/etch/mint). Verify C == a·H + r·G. If known or recovered → balance += a; if neither path works → "ghost".
Privacy scope

Tacit surfaces three orthogonal privacy capabilities:

  • Shielded amount (default) — hidden in every CETCH, T_MINT, CXFER, T_AXFER, and BURN-change commitment (32-byte Pedersen commit + 8-byte HMAC-encrypted u64). In T_SWAP_BATCH, per-trader amounts stay hidden during settlement via BabyJubJub Pedersen openings inside Groth16.
  • Shielded address (opt-in, blinded-pubkey commit primitive) — BIP-341-style construction commit = recipient_pubkey + blinding·G with blinding = HMAC(wallet.priv, domain || op-specific anchor). The commit doubles as Schnorr key and P2TR output key, so on-chain markers sit at per-transaction unique addresses with no apparent link to the recipient's published identity. Same crypto as BIP-340/341/352 silent payments, no new ceremony. Composes orthogonally with shielded amounts.
  • Anonymous spend via mixer pool (opt-in) — deposit a fixed-denomination UTXO into the Poseidon-Merkle pool (T_DEPOSIT), later withdraw under a Groth16 proof of unspent leaf membership (T_WITHDRAW). The withdraw recipient is unlinkable to any specific deposit; the anonymity set is the count of currently-unspent leaves at withdraw time. The same circuit underpins cBTC.zk slot semantics — every slot spend is anonymous-unique-spend by construction.

End-to-end: amount-hiding is on by default; address-graph unlinkability is opt-in two ways (shielded-address per transfer, or mixer-pool round-trip for full anonymity-set unlinkability). Stricter posture than Liquid CT, approaching Mimblewimble or shielded Zcash for assets that route through the pool.

What remains public:

  • Asset_id. Which asset is being transferred is public (32-byte asset_id in every non-pool envelope). Surjection proofs are on the roadmap.
  • Sender pubkey on non-pool transfers — visible in tx.vin[1].witness[1]; recipient needs it for ECDH blinding recovery.
  • Pool participation — observers see that an address deposited or withdrew, just not which deposit corresponds to which withdrawal.
  • AMM pool reserves and per-batch net deltas — public by design (cost of trustless reconstruction). Per-trader amounts within a batch are hidden.
Validation cost

Recursive validation is O(chain depth) on a cold cache, depth-bounded at 200 hops. With aggregated bulletproofs and batched verification, the practical cost is dominated by BIP-340 Schnorr verification (kernel sigs + mint sigs) across the ancestry, not rangeproofs (~150 ms verify per BP, batched into one multi-exp across the full walk). Memoization keeps subsequent scans O(new UTXOs) within the same session. For a fresh wallet on mobile, deep ancestries (≥50 hops) will still be slow; production deployments benefit from a persistent validation cache or a shared indexer service. Cryptographic correctness holds without either; the gain is purely UX.

Trust model

Issuer at CETCH. The initial supply commitment is hidden, so no kernel-sig constraint applies — the issuer chooses any value in [0, 2⁶⁴). The dApp publishes the (supply, blinding) opening by default — embedded in the asset's IPFS metadata blob (content-addressed) and pinned to the discovery worker as a cache. Anyone verifies C == supply·H + r·G against the on-chain commitment, no issuer trust required. Issuers explicitly opt out of attestation only if they have a reason to keep the total confidential; the dApp surfaces that as a deliberate uncheck.

Mint authority for mintable assets. If mint_authority ≠ 0 in the CETCH envelope, that x-only pubkey can issue additional supply via T_MINT envelopes — bounded only by 2⁶⁴ per envelope, unlimited in count. Standard signature-gated mint pattern: holders trust the mint-authority key not to be abused, and compromise of that private key means uncapped inflation. The dApp auto-attests every mint by default so supply remains publicly auditable. Fixed-supply CETCHes (mint_authority all-zero) cannot be expanded — for those, etch-time attestation gives provably and permanently public supply.

T_PETCH (permissionless mints). No issuer trust at all. The deploy declares cap_amount, mint_limit, and an optional height window; deployer receives zero tokens. Each T_PMINT reveals (amount, blinding) on chain, so cumulative supply against the cap is auditable from chain alone — no attestation channel needed.

Mixer + cBTC.zk (Groth16 + Phase 2 ceremony). Both share the same withdraw.circom primitive. Soundness rests on ≥ 1 honest contributor in the Phase 2 ceremony; privacy (zero-knowledge) is unconditional and does not depend on the ceremony. The canonical v1 mixer ceremony (Phase 2, beacon-finalized 2026-05-11, 2,227 contributors) is hardcoded in the dApp as the trust anchor; cBTC.zk slots reuse the same vk. cBTC.zk's BTC anchor is cryptographic — the slot's spending key is derived from a mixer leaf's secret — so no federation and no co-signer.

AMM circuits (Phase 2 ceremony in flight). The three AMM circuits run independent Phase 2 chains under one shared pot18 Phase 1, with one Bitcoin-block beacon at finalization. Same ≥ 1-honest-per-circuit assumption as the mixer. Pool reserves are virtual public quantities the indexer tracks — no UTXO holds any pool's funds, so no party can rug. The constant-product invariant is plain arithmetic on public reserves; Groth16 binds hidden per-trader amounts to public batch deltas.

cBTC.tac (collateralized lien). Two trust legs: the BTC anchor at L1 (cryptographic — cBTC.zk's slot lock works as long as Bitcoin + Groth16 + secp256k1 hardness hold) and the TAC collateral substrate (economic — TAC stays valuable enough relative to BTC that bonded slots remain over-margined). The collateral leg reuses the same indexer-consensus trust model that already makes Runes/Ordinals tradeable at scale: TAC's market-validated value becomes the bond that makes wrapped BTC trustless without federation. Same shape as MakerDAO's ETH-collateralized stablecoins; not the same shape as wBTC's BitGo + auditors.

After issuance. No participant can inflate (kernel sig blocks unbalanced CXFER / T_AXFER / T_BURN; T_PMINT caps are chain-auditable; AMM circuits bind hidden amounts to public deltas), burn covertly (BURN's burned_amount is public and bound into the kernel msg), or substitute assets (asset_id consistency checked across every input's parent envelope). Recursive client-side validation guarantees this independent of any indexer's honesty.

Pedersen commits via @noble/secp256k1 projective ops. NUMS H from deterministic hash-to-curve. BIP-340 Schnorr + BIP-341 Taproot inlined. Aggregated bulletproofs (Bünz et al. 2017) + Mimblewimble-style kernel sigs in pure JS, with Pippenger MSM for batched verification. Groth16 prover + verifier via snarkjs (vendored); Poseidon-Merkle (mixer / cBTC.zk anonymous-spend) and BabyJubJub Pedersen (AMM amount-confidentiality) inside the circuits; Camenisch–Stadler sigma cross-curve binding between secp256k1 and BabyJubJub at 169 bytes, no trusted setup.