Protocol Architecture
Tacit is a meta-protocol: Bitcoin consensus does not enforce its rules. Bitcoin nodes see ordinary Taproot transactions whose witness data happens to encode an asset-protocol envelope. An indexer — anyone running the reference client or a faithful re-implementation of SPEC.md — reads those envelopes, walks ancestry back to issuance, verifies cryptographic proofs, and reaches a verdict on which UTXOs hold which tokens. Two indexers running the same code against the same chain converge to the same answer. No consensus change is required and no off-chain proof exchange happens between sender and recipient.
The Overview compares Tacit against the other Bitcoin token protocols.
Envelope wire format
Every Tacit envelope rides in tx.vin[0].witness[1] — the script-path leaf data of a Taproot script-path spend. The witness layout is fixed:
| INDEX | CONTENTS | SIZE |
|---|---|---|
witness[0] |
BIP-340 Schnorr signature over the envelope script | 64 bytes |
witness[1] |
The envelope script itself (carries the opcode and payload) | variable (~800 B for a CXFER) |
witness[2] |
Taproot control block — proves the script was committed | 33 bytes (or 33 + 32·k for deeper Merkleized Alternative Script Trees (MAST); Tacit uses k=0) |
The envelope script itself is structured as:
<32-byte signing pubkey> OP_CHECKSIG
OP_FALSE OP_IF
PUSH "TACIT" (5 bytes)
PUSH 0x01 (envelope version)
PUSH <payload> (split across PUSHDATA chunks ≤ 520 B each)
OP_ENDIF
The OP_FALSE OP_IF … OP_ENDIF wrapper makes the entire envelope unexecuted during script evaluation — the same trick Ordinals uses to embed inscriptions into Taproot witnesses without affecting consensus validity. Bitcoin nodes verify the script-path spend is well-formed and then ignore the dead branch entirely.
The Taproot output's internal pubkey is the BIP-341 Nothing-Up-My-Sleeve (NUMS) point (the full canonical value, 0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0) — a hash-to-curve point with no known discrete log. That means the only way to spend a Tacit output is via the script-path; the key-path is provably unsignable. Verified end-to-end on the sample T_AXFER tx b76c90de...150f49 (mempool), where witness[2] begins 0xc050929b74… — the NUMS pubkey exactly as the spec promises.
payload[0] is the opcode byte — 0x21 for CETCH, 0x23 for CXFER, and so on. The full opcode catalog is on the Operations page.
The commit-reveal pattern
A Tacit operation is two Bitcoin transactions, not one. This is the same Commit-Reveal Pattern Ordinals uses, for the same reason: revealing arbitrary data in Taproot witness only works if you have an output to spend script-path against.
sequenceDiagram
autonumber
actor U as User
participant B as Bitcoin L1
participant I as Indexer (dApp)
U->>B: 1. Commit tx — create P2TR output<br/>committed to envelope script's leaf hash
B-->>U: confirms (1+ block)
U->>B: 2. Reveal tx — spend the P2TR via script-path<br/>envelope script in witness[1]
B-->>I: tx visible on chain
I->>I: decode witness[1], verify proofs,<br/>walk ancestry, update wallet state
Figure 1: Two-transaction commit-reveal flow. The reveal tx is what carries the Tacit envelope; the commit tx exists only to create a Taproot output the reveal can spend script-path.
The commit tx is cheap — just a small P2TR output. The reveal tx is where the witness bytes (and the fees) go.
For operations that consume existing Tacit UTXOs (CXFER, T_MINT, T_BURN, T_AXFER, T_DEPOSIT, T_DROP), the reveal tx's vin[1..] carries the asset inputs being spent. For T_AXFER specifically, vin[1+asset_input_count..] carries auxiliary BTC inputs — that is what makes it atomic: the buyer's BTC payment and the seller's token delivery close in the same Bitcoin tx, with neither side able to grief the other.
Asset identity
Asset identity is derived deterministically from the etch transaction:
where reveal_vout = 0 for both CETCH (0x21) and T_PETCH (0x27). The asset_id keys off the first-output position regardless of whether vout[0] actually carries a Tacit UTXO — T_PETCH, for example, creates no supply UTXO at all.
This has three consequences worth highlighting:
- Tickers are not unique. Two CETCH envelopes can both declare
ticker = "USDC"— they will have distinct 32-byteasset_idvalues. Wallets must display the asset_id alongside the ticker for disambiguation, the same way they show contract addresses for ERC-20 tokens. Ticker collision is structural to the design, not a bug. - Etch envelopes do not require a signature. Anyone can broadcast a CETCH-shaped envelope. This is intentional: there is no "the asset" to forge, because the asset_id is bound to the carrying transaction. A forger has nothing to gain by re-using bytes from someone else's etch — they would only re-etch the same logical asset under a fresh, unrelated
asset_id. - Mint references bind via
etch_txid+asset_id. T_MINT and T_PMINT envelopes carry both fields. The validator checksasset_id == SHA256(etch_txid_BE || 0_LE)to prevent cross-mode references (you cannot mint a CETCH asset via T_PMINT and vice versa).
The validator algorithm
Wallets do not store balances; they reconstruct them from the chain. Every validation begins the same way: validateOutpoint(txid, vout) → fetch tx → decode envelope at vin[0].witness[1] → switch on opcode. The table below is the dispatch table; failure on any listed check rejects the envelope. Recursive paths (CXFER, T_AXFER, T_BURN) walk back to the originating CETCH or T_MINT; mixer and drop ops short-circuit because their parents are state-bearing envelopes, not UTXO-bearing.
| OPCODE | VERIFICATION STEPS | ANCESTRY WALK |
|---|---|---|
| CETCH | Range proof on supply commitment; vout == 0 |
Leaf — terminates here |
| T_MINT | Fetch CETCH ancestor; verify issuer sig under mint_authority; range proof |
Leaf — terminates here |
| T_PMINT | Lookup T_PETCH metadata; amount == mint_limit; height window; cap not exceeded; Pedersen opening |
Non-recursive lookup |
| CXFER / T_AXFER / T_BURN | Recursively validate each asset input; asset_id consistency; aggregated range proof; Kernel Signature under E' |
Recursive — walks back to a CETCH or T_MINT leaf |
| T_WITHDRAW | Pool registered; claimed merkle_root in last 32; nullifier unspent; Groth16 proof verifies; external Pedersen check |
Short-circuits — parent is state-bearing, not UTXO-bearing |
| T_DCLAIM | Fetch T_DROP parent; per_claim, height window, cap; eligibility witness if merkle_root gate set |
Short-circuits — parent is state-bearing, not UTXO-bearing |
A few details worth flagging:
- The Taproot script-path framing is canonical, not enforced. The validator inspects bytes at
tx.vin[0].witness[1]and tries to decode them as a Tacit envelope. It does not assert that the input is actually a script-path spend with the NUMS internal key. Soundness for each opcode is supplied by a different mechanism: kernel sigs + range proofs for CXFER/T_AXFER/T_BURN, an issuer signature bound to acommit_anchorfor T_MINT, asset-id self-binding for CETCH/T_PETCH (the asset_id derivation is the protection), Groth16 proof verification for T_WITHDRAW, eligibility witness for gated T_DCLAIM. - Recursion is memoized. A
(txid, vout) → boolcache means each ancestor is decoded once even if many descendants share it. Range proofs can be batched into one multi-exponentiation across many UTXOs. - Recursion terminates at CETCH or T_MINT. Both produce a fresh supply UTXO and are the leaves of the ancestry tree. T_PETCH terminates differently — it produces no UTXO, so the validator returns
falseif asked to validate one of its outputs as a Tacit asset; T_PMINT envelopes reference T_PETCH metadata via a non-recursive lookup.
Sample envelope (verified end-to-end)
Tx b76c90de...150f49 (tacitscan · mempool) confirmed at block 949,716 (mempool). Tacitscan decodes it as a T_AXFER (atomic Over-The-Counter (OTC) settlement) of asset TAC#f0bbe868.
The raw transaction structure as fetched from mempool.space/api/tx/<txid>:
| FIELD | VALUE |
|---|---|
| Size | 1,724 bytes |
| Weight | 2,648 wu (≈ 662 vBytes after SegWit discount) |
| Fee | 1,458 sat (≈ 2.2 sat/vB) |
| Inputs | 5 (1 Taproot envelope-bearing input + 4 BTC P2WPKH aux inputs) |
| Outputs | 3 (Tacit recipient at 546 sat dust + BTC payment at 210,000 sat + change at 2,160 sat) |
The envelope-bearing input (vin[0]) has the exact three-item witness the spec requires:
| WITNESS SLOT | LENGTH | LEADING BYTES |
|---|---|---|
witness[0] (Schnorr sig) |
64 B | 957df5b0...09bb1... |
witness[1] (envelope script) |
881 B | 20dd10a2db8e0271ac… (PUSH-32 + 32-byte signing pubkey) |
witness[2] (control block) |
33 B | c050929b74...e803a (full canonical NUMS pubkey after the parity tag) |
The control block's leading byte after the parity tag (c0) is the NUMS internal pubkey — full canonical value 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, confirming script-path is the only spend path on this Taproot output, exactly as SPEC §5 requires. The envelope script begins with 20 (the PUSH-32 opcode) followed by the 32-byte signing pubkey, which would be followed by OP_CHECKSIG and the inert OP_FALSE OP_IF branch carrying the T_AXFER payload.
Tacitscan further decodes the envelope as containing a 64-byte kernel signature, a 33-byte Pedersen commitment, an 8-byte Elliptic-Curve Diffie–Hellman (ECDH)-encrypted amount field, and a 688-byte aggregated Bulletproof range proof — the canonical CXFER/T_AXFER payload shape. The Mechanisms page explains what each of those bytes does cryptographically.
Cost per operation
Because Tacit envelopes ride in Taproot witness data, the SegWit discount applies (witness bytes count ¼ toward the virtual-size fee calculation). A representative CXFER with two recipients (m=2 bulletproof aggregation, the most common case):
| COMPONENT | BYTES |
|---|---|
| Schnorr signature (witness[0]) | 64 |
| Signing pubkey + OP_CHECKSIG + envelope header | ~12 |
| asset_id | 32 |
| kernel_sig | 64 |
| 2 × (commitment + amount_ct) | 82 |
| Aggregated bulletproof (m=2) | 754 |
| Control block (witness[2]) | 33 |
| Total witness | ~1,041 |
| Non-witness (inputs + outputs + headers) | ~250 |
| Total raw size | ~1,291 B |
| vsize after SegWit discount | ~510 vB |
At a 10 sat/vB mainnet fee, that is ~5,100 sat per transfer. The README quotes ~25–30k sats for higher fee environments and more conservative aggregation. The point of comparison: a Runes runestone is in the hundreds-of-bytes range. Tacit is roughly an order of magnitude more expensive, and that order of magnitude is entirely paid to hide the amount.
Forward compatibility
The validator implements a soft-fork rule for unknown opcodes: an indexer that encounters an envelope opcode it does not recognize MUST treat the envelope as a no-op at the asset and pool-state level. The Bitcoin transaction remains structurally valid (Bitcoin consensus does not care about envelope content); only the protocol-layer effect is skipped. The indexer SHOULD log the unknown opcode but MUST NOT halt indexing or revert state.
This is what lets future opcodes ship without breaking deployed indexers. A V1-aware indexer continues to operate correctly when V2 opcodes appear on chain — it ignores them. A V2-aware indexer additionally interprets the V2 opcodes and tracks the V2 state. Both converge on the same V1 state for V1 envelopes.
The complementary constraint: opcodes already defined in the spec MUST NOT be redefined or reused with different semantics. Future ceremonies add new opcodes at new code points; they do not overload existing ones.
Indexer trust model
The validator is the trust target. The reference dApp ships an indexer; anyone can re-implement it from SPEC.md. The trust assumptions, ranked by criticality:
| TRUSTED FOR | WHAT IT ENFORCES | MITIGATION |
|---|---|---|
| Bitcoin L1 | Transaction ordering, witness integrity, no double-spends | None — this is the substrate |
| Indexer code (the dApp HTML/JS or a re-implementation) | Correct enforcement of every rule in SPEC.md | Re-host, audit, pin by IPFS CID; two browsers running the same code reach the same verdict |
@noble/secp256k1 and @noble/hashes |
secp256k1 ops + hash primitives | Vendored under dapp/vendor/tacit-deps.min.js, pinned by the same IPFS CID as the rest of the dApp; runtime Known Answer Test (KAT) in runStartupKAT() is an independent defense |
| In-browser tacit privkey | Signs every Tacit op (taproot script-path, kernel sig, mint sig); Hash-based Message Authentication Code (HMAC) key for all blinding/keystream derivations | The wallet for Tacit assets. localStorage; namespaced by network. Must be exported and backed up |
| Asset issuer | Honest announcement of initial supply (Pedersen hides it cryptographically) | Resolved at client layer via attestation flow: dApp publishes (supply, blinding) to IPFS on by default; anyone verifies that the commitment opens to the announced supply |
| Mint authority | Minting decisions on mintable assets | Holder of the mint_authority private key from the originating CETCH envelope |
What is not trusted:
- ☑ Any external server (worker, IPFS gateway, mempool API) for protocol-level validity (see Overview for the worker's role)
- ☑ Off-chain proof distribution (RGB-style). Wallets recover full balance from privkey + chain alone for every on-chain envelope.
- ☑ Watchtowers, federation members, or any third party.
Privkey-only recovery has one edge case — atomic-intent recipient UTXOs — explained in Mechanisms.
The chain-data layer has its own defensive pattern: the dApp queries both mempool.space and blockstream.info in parallel via a tip-divergence watchdog. A single Esplora endpoint outage or tampering surfaces as an in-app banner before it affects validation.
Properties
The following properties are downstream of the architecture, not features bolted on:
| PROPERTY | SOURCE |
|---|---|
| Confidentiality on Bitcoin proper | Pedersen + Bulletproof at the asset layer, every operation |
| No federation, no sidechain | Indexer-validated metaprotocol pattern (same as Runes) |
| Privkey-only recovery | Deterministic ECDH-keyed blinding/keystream derivations; on-chain everything except atomic-intent edge case |
| Open re-implementation | MIT license, normative SPEC.md, content-addressed dApp |
| Forward-compatible upgrades | Unknown-opcode soft-fork rule |
| Auditable per-UTXO openings | tacit-disclosure-v1 SPEC §5.6 (off-chain primitive) |
The Mechanisms page covers the cryptography that makes the asset-layer confidentiality work. The Mixer page covers the additional Groth16 layer that breaks the address-graph link between deposit and withdrawal.
References
- Tacit protocol specification — SPEC.md (z0r0z/tacit, MIT)
- Tacit reference dApp — tacit.finance (source)
- Bitcoin Improvement Proposal 340 (Schnorr signatures) — BIP-340
- Bitcoin Improvement Proposal 341 (Taproot) — BIP-341
- Bitcoin Improvement Proposal 342 (Validation of Taproot Scripts) — BIP-342
- Ordinals theory and inscriptions (commit-reveal precedent) — docs.ordinals.com
- DNZN — z0r0z entity profile
- DNZN — Project Overview, Mechanisms, Mixer, Operations, Risks