Confidential tokens
verifiable on bitcoin
private amounts · no federation · privkey-only recovery
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
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
Send to a recipient's pubkey. The amount stays hidden from observers; the recipient auto-discovers their balance from chain data on their next scan — no share-link or messaging needed.
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 its own signing key in this browser — your existing Bitcoin wallet doesn't expose the primitives confidential transactions need. Whichever path put the key here (auto-generated, imported, 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 per external address; clearing browser storage or switching devices loses it, so export and back it up before holding value.
discover · recently etched browse all ↗
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
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 (fair launch)
Deploy a token with a fixed lifetime cap and a fixed per-mint amount. You receive zero tokens at deploy — to hold any, broadcast a mint like everyone else. Anyone can mint until the cap fills. Mints publicly reveal (amount, blinding) so the cap is auditable from chain alone — supply is fully observable for these assets (different from confidential CETCH supply). SPEC §5.8 / §5.9.
Cap is permanent. Once the cap is reached, no more T_PMINTs are accepted.
Per-mint amount is permanent and must divide the cap evenly (no remainder).
Minting opens at the next confirmed block after this T_PETCH lands; deployer same-block mints are rejected at validation.
send · confidential token or plain bitcoin
Confidential transfer of one asset. Each send takes two Bitcoin transactions (a commit + a reveal); observers see cryptographic proofs of valid math, never the amount. The recipient auto-discovers their balance from chain data on their next scan — share-links are an optional shortcut, not required.
Ask the recipient to share their pubkey from their Wallet tab. The address is derived from this pubkey.
Choose specific UTXO to spend (advanced)
When set, the amount field is locked to the chosen UTXO's exact value (you spend the whole UTXO, change goes back to you). Useful for cancelling a stale listing's UTXO without sending your full balance.
Review your transfer details below before broadcasting — once sent, it can't be undone.
drops · airdrop tooling
Issuer-side tooling for batched airdrops. Upload a snapshot CSV (eth_address,amount), build a merkle commitment, pin the snapshot to IPFS, and fulfil incoming claims in batches of up to 7 recipients per Bitcoin tx. The merkle root is the public commitment recipients verify against in the Claim tab.
Hot-wallet hygiene. Fulfilment CXFERs are signed by your active wallet. To bound blast radius, generate a fresh treasury identity below, fund it from your main wallet via Send, then use Wallet → Import key in a separate browser profile to switch to the treasury before fulfilling.
Trust model: where the worker fits, and how to self-host
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).

1 · Treasury (optional)

Generate a fresh tacit identity dedicated to this drop. The privkey is shown once; copy it now and store it somewhere safe.

2 · Target asset

The tacit asset to airdrop. Each source CSV's amounts are floor-truncated to this asset's decimals before merging.

3 · Source CSVs

Add one or more snapshot CSVs. Each may have its own source decimals (typical ERC-20 is 18, USDC/USDT are 6). Same-address balances across sources are summed; the sum is then floor-truncated to the target asset's decimals. Etherscan token-holder exports work as-is ("HolderAddress","Balance",…).

4 · Blacklist (optional)

Addresses listed here are excluded even if present in any source CSV. One per line, with or without 0x prefix. Comments (#, //) ignored.

5 · Build snapshot

6 · Publish

7 · Saved drops

Drop records (including the local fulfilled[] ledger) live in this browser's localStorage. Use Export JSON on each row to back up; clearing browser data without a backup means losing the "this leaf was paid" record (the chain is unaffected — fulfilled CXFERs stay valid). To restore from a backup, use Import below.
No drops saved yet.
claim · airdrop recipient
Recipient flow for airdrops you're eligible for. Connect an Ethereum wallet to find drops your address is included in, or paste a merkle root + IPFS CID below if the issuer shared a private link. You'll sign a claim binding your tacit pubkey, then submit it to the issuer (or to the worker queue).

1 · Discover available drops

Active issuer-published drops on . Connect an Ethereum wallet to filter to drops your address is eligible for.
no 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 blob whose rows don't match the root above.

2 · Connect Ethereum wallet

Connecting also auto-filters the discovery list above to drops eligible for your address.
Works with most Ethereum wallets — MetaMask, Rainbow, Rabby, Coinbase Wallet, Trust, Frame. Smart-contract wallets (Ambire, Safe, Argent) work too. The connection is only for signing the claim — no Ethereum transaction is broadcast, no gas is spent. The wallet's current chain doesn't matter for regular wallets; smart-contract wallets need to be on the chain that holds the contract.

3 · Receiving tacit identity

The airdrop will be sent to your active dapp tacit pubkey. To use a different tacit identity, switch via Wallet → Import key (or Regenerate) before signing the claim.
⚠ Back up your tacit privkey before signing. The privkey controlling this pubkey lives only in this browser's localStorage. If you clear it before the issuer fulfills, the airdrop is unrecoverable. Use Wallet → Export key now if you haven't already.

4 · Sign claim

Your wallet will display the full canonical claim message. Read every field before signing.
Preview the message that will be signed

      

5 · Send to issuer

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.
market · open listings
how it works trustless atomic vs trust-required OTC
Every tile trades a confidential token amount for sats. Two trust models:
  • ⚡ Atomic (default filter) — settles in one Bitcoin tx. The maker can't take your sats without delivering, and you can't take the asset without paying. No counterparty trust.
    Buy: Claim (locks 5 min) → maker Fulfils → you Take (broadcasts settlement).
  • OTCopening publishes the exact UTXO amount; range hides the amount but proves ≥ X. Both settle off-chain: pay the maker's BTC address, then they broadcast the asset transfer to your pubkey. Counterparty trust required — the maker can take your sats and not deliver.
    Buy: Take → pay the maker → wait for their broadcast.
Bids: post a signed price intent (no sats locked). Holders can Fulfil a bid by spinning up an atomic intent targeted at you; settlement uses the trustless 3-step above.
Verify runs all crypto / UTXO-liveness checks without buying. Cancel pulls your own listings.
trustless only · switch for OTC
loading
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.
fair launch · public-mint assets
Permissionless-mint deployments — anyone can mint a fixed tranche until the cap fills. Supply is publicly auditable from chain alone (different from CETCH). SPEC §5.8 / §5.9.
loading
mixer · shielded pools (T_DEPOSIT / T_WITHDRAW)
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.
v1 preview. The zero-knowledge prover and verifier run live in your browser. Each pool needs a one-time trusted setup before it's safe to use — deposits and withdrawals stay locked until the ceremony below finalizes.
pools
Preview only — the trusted-setup ceremony above must be finalized before any pool operation is safe. Contribute or wait for finalization.
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
Preview only — the trusted-setup ceremony above must be finalized before depositing. Until then, deposits are gated.
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.
withdraw
Preview only — withdrawals consume the same trusted setup deposits do, and unlock once the ceremony above is finalized.
Withdraw a deposit to a fresh address. Paste the deposit record JSON your wallet saved at deposit time, or import via share-link. The privacy set size — how many other deposits yours hides among — is shown beneath each pool.
ceremony · contribute to the trusted setup
Each pool starts with a trusted setup ceremony that bootstraps its privacy. Anyone can contribute — and as long as one participant across the entire chain is honest, the pool is sound. More contributors means a stronger guarantee. Each turn takes ~30–60 seconds and ~10 MB of bandwidth in your browser.
Loading…
initialize a new pool
Preview only — pool initialization is gated on ceremony finalization, since every pool's deposits depend on the same trusted-setup transcript.
Anyone can open a pool for any asset and any deposit size. The first confirmed pool for a given asset + amount wins. You'll need a verifying-key file and a finalized ceremony transcript, both pinned to IPFS.
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 ‖ 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 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.