Confidential tokens
indexer-validated on bitcoin
amounts hidden · downstream conservation checked · supply optionally attested
1
Connect a wallet
Use your existing Xverse / UniSat / Leather (one click), import a privkey, or — on signet — let the dApp generate one and grab faucet sats.
2
Etch a confidential asset
Define ticker, supply, decimals. Supply is committed via Pedersen — only you know the amount. The chain sees a 33-byte commitment.
3
Transfer privately
Send to a recipient's pubkey. On-chain: only commitments. The recipient learns their amount via an ECDH-derived blinding embedded in a share-link.
tacit.wallet loading…
Address
view on explorer ↗
Public key
share with senders so they can transfer assets to you
Balance
sats
Block height
Manage wallet
Tacit needs an in-page privkey to do its confidential math (HMAC-derived blindings, kernel sigs, taproot script-path Schnorr) — primitives that external wallets don't expose. Whichever path placed the key here (auto, import, or locally bound to your Xverse / UniSat / Leather address), this is the key that controls your tacit assets. The external-wallet path stores a fresh tacit key locally per external address — clearing browser storage or switching devices loses it, so export and back it up before holding value.
tacit.discover · recently etched browse all ↗
tacit.etch · define a new asset
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
A Pedersen commitment C = supply·H + r·G plus a 64-bit aggregated bulletproof go on-chain inside a Taproot script-path envelope. Only you know (supply, r) — observers see only the 33-byte commitment and a ~700-byte proof that the base-unit supply is in [0, 2⁶⁴). The supply blinding and an 8-byte encrypted-supply field are both self-derived from your wallet privkey + commit-input anchor, so a freshly-installed wallet with only your key can recover the etched supply from chain alone — no localStorage backup needed. With 64-bit range, max supply per asset_id is ~1.84 × 10¹⁹ base units, which covers any realistic decimals setting. Etch is a 2-tx flow (commit + reveal); broadcast takes a few seconds. Image URI (≤256 bytes) is stored alongside the supply commitment and propagates to recipients through the canonical CETCH ancestor — they don't trust the share-link sender for it.
tacit.transfer · confidential send
Single-asset confidential transfer. Each transfer is a 2-tx flow: a commit tx creates a Taproot output committed to the envelope, then a reveal tx spends it via script-path, exposing the commitments + rangeproofs in the witness. Observers see commitments and proofs, never amounts. The recipient receives a share-link with the cleartext amount; their blinding factor is recomputable via ECDH.
Ask the recipient to share their pubkey from their Wallet tab. The address is derived from this pubkey.
tacit.holdings · confidential
tacit.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.
tacit.market · open listings
Bilateral OTC offers across all assets. Settlement is off-chain in v1: pay the maker's address in sats, then the maker broadcasts a CXFER to your pubkey. Counterparty trust required (or use a 2-of-2 escrow). Range-disclosed listings prove a lower bound on the maker's balance without publishing their full opening — useful for makers who want to keep their treasury confidential.
loading…
tacit.discover · all etched assets
Browse confidential tokens etched on Bitcoin — tickers public, supplies optionally proven, amounts always hidden. Tap any asset's ticker to copy its asset_id, or click the etch tx to verify on-chain.
tacit.protocol

Confidential single-asset token meta-protocol on Bitcoin. Pedersen commitments hide amounts; rangeproofs and kernel signatures together enforce supply conservation without trusting any participant.

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).

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 CETCH root and reach the same verdict as any other indexer running the same rules. No federation, no off-band proof exchange, no out-of-band coordination.

This is distinct from proof-distribution protocols (RGB, Taproot Assets) where the recipient must receive validity proofs out-of-band; an indexer with chain-only access cannot reconstruct token state. It's also distinct from fully-validated chains (Liquid, Mimblewimble) where consensus enforces the rules natively. Tacit's contribution is bringing CT-style amount-hiding into the indexer-validated meta-protocol family — the only thing that travels out-of-band is the recipient's own opening (cleartext amount + blinding) via share-link, which lets them see their balance but is not required for any indexer to validate the UTXO.

Cryptography

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, meaning each committed value is proven to lie in [0, 2⁶⁴). A single proof can cover m ∈ {1, 2, 4, 8} commitments simultaneously via the inner-product argument, with 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 to prove and ~150 ms to 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 1.84 × 10¹⁹ base units. With 8 decimals that's 184 quintillion display units per UTXO — well past anything practical. Wallets still spread holdings across multiple UTXOs (1–8 per CXFER) for change tracking, but the per-UTXO cap is no longer a real constraint for any practical decimals/supply combination.

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 ‖ 1_LE). 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.

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 hides amounts. It does NOT hide:

  • Address graph. Sender and recipient Bitcoin addresses are visible on chain like any tx.
  • Asset_id. Which asset is being transferred is public (32-byte asset_id in the envelope).
  • Sender pubkey. Visible in tx.vin[1].witness[1] (P2WPKH witness); recipient needs it for ECDH blinding recovery.
  • Tx graph. Inputs and outputs are linkable like any UTXO chain.

This is strictly weaker than Mimblewimble (which hides tx graph via cut-through) and weaker than Liquid CT with surjection proofs (which hides asset_id). It's the same scope as Liquid CT without surjection proofs: amount-hiding only.

Validation cost

Recursive validation is O(chain depth) on a cold cache. With aggregated bulletproofs and batched verification, the practical cost is dominated by ECDSA/Schnorr verification 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 would want a persistent validation cache (across sessions) or a shared indexer service. The "single-file dApp" claim holds for cryptographic correctness; the practical UX for deep chains benefits from server-side help.

Trust model

Issuer at etch. 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. This is the 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 the supply remains publicly auditable across mint events. Fixed-supply assets (mint_authority all-zero) cannot be expanded — for those, etch-time attestation gives you provably and permanently public supply.

After issuance. No participant can inflate (kernel sig blocks unbalanced CXFER), 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). The 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.