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.
Confidential single-asset token meta-protocol on Bitcoin. Pedersen commitments hide amounts; rangeproofs and kernel signatures together enforce supply conservation without trusting any participant.
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.
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.
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.
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.
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.
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:
excess = Σr_out − Σr_in and signs kernel_msg with priv = excess (BIP-340 Schnorr).E' = ΣC_out − ΣC_in from on-chain commitments. If amounts balance, E' = excess·G and the sig verifies under E'.xonly().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.
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).
vin[0].witness[1]; decode as tacit envelope.bpRangeAggBatchVerify, with random per-proof scalars αi, βi ensuring soundness preservation.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.asset_id; the aggregated rangeproof must verify; the kernel sig must verify under (ΣC_out − ΣC_in).xonly() over the kernel msg.E' = ΣC_out + burned_amount·H − ΣC_in. N=0 is allowed (full burn, no change output, no rangeproof).markAll propagates the verdict to every sibling output of a failed envelope.(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".Tacit hides amounts. It does NOT hide:
tx.vin[1].witness[1] (P2WPKH witness); recipient needs it for ECDH blinding recovery.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.
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.
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.