Skip to content

Cryptographic Mechanisms

Tacit's confidentiality is the same mechanism Greg Maxwell proposed for Bitcoin in 2015 and that the Mimblewimble family of protocols popularized: Pedersen Commitments for amount hiding, aggregated Bulletproof range proofs to prevent inflation, and Mimblewimble-style Kernel Signatures to enforce supply conservation. None of the primitives are new. The composition lets every secret be derived deterministically from a single privkey + on-chain anchors, so wallets recover from chain alone with no off-chain proof distribution. The Mixer page covers the additional Groth16 + Poseidon machinery that breaks the address-graph link.


Curve and generators

Tacit operates on secp256k1 — the same curve Bitcoin uses for signatures. There is no second curve in the confidential-token core. (The optional Automated Market Maker (AMM) introduces BabyJubJub for in-circuit arithmetic; that is out of scope here.)

Two generators are needed for Pedersen commitments:

GENERATOR ROLE DERIVATION
G Blinding generator Standard secp256k1 base point (the one Bitcoin uses)
H Value generator Nothing-Up-My-Sleeve (NUMS) point derived by hash-to-curve from SHA256("tacit-generator-H-v1") via try-and-increment

NUMS means there is no known discrete logarithm of H with respect to G. The try-and-increment derivation from a published seed string makes this verifiable: anyone re-runs the algorithm, gets the same canonical point, and confirms there is no trapdoor.

The canonical H in compressed form (full canonical value): 02bd7bf40fb5db2f7e0a1e8660ca13df55bb0d9f904e36e6297361f00376865e56.

The aggregated-bulletproof construction needs two vectors of 64·8 = 512 generators each (G_vec[i], H_vec[i]) plus one auxiliary generator Q, all derived by the same try-and-increment pattern with domains "tacit-bp-G-v1", "tacit-bp-H-v1", and "tacit-bp-Q-v1" + 4-byte LE index. The spec publishes the first four G_vec / H_vec points as cross-implementation test vectors — typo a domain string and your implementation produces different generators and rejects every proof from the canonical one. The vectors are the cross-check.


Pedersen commitments

A Pedersen commitment to amount a with blinding factor r is the elliptic-curve point:

C = a · H + r · G

Three properties make this construction load-bearing for confidential tokens:

PROPERTY TYPE WHAT IT MEANS
Hiding Perfect / information-theoretic For uniformly random r, C is uniformly distributed in the group regardless of a. Even an unbounded adversary learns nothing about a from C alone.
Binding Computational Finding a different opening (a', r') ≠ (a, r) producing the same C reduces to computing log_G(H) — hard under the NUMS assumption.
Additively homomorphic C₁ + C₂ = (a₁+a₂)·H + (r₁+r₂)·G. Sums of commitments commit to sums of amounts; differences commit to differences.

The homomorphism is what makes everything downstream work. Conservation of supply across a transfer becomes a single point equation; range proofs aggregate across multiple commitments via a shared inner-product argument; sum-of-balances disclosures (SPEC §5.6 "balance ≥ K") become a single bulletproof on Σ C_i − K · H. These are direct consequences of the homomorphism, not separate constructions.


Aggregated bulletproof range proofs

Hiding a by itself is not enough. If a sender could commit to a negative amount, they could create supply out of thin air (output commitments summing to less than input commitments in absolute terms but balancing via a wrap-around in the group). Range proofs close that gap.

Tacit uses the aggregated bulletproof construction from Bünz et al. 2017 §4.3 at n = 64 bits, with aggregation factor m ∈ {1, 2, 4, 8}. The proof asserts, for m Pedersen commitments V_j = v_j · H + γ_j · G, that every v_j ∈ [0, 2⁶⁴) — i.e., representable as a non-negative uint64.

Aggregation matters because a single inner-product argument covers all m commitments simultaneously, with witness size O(log(n·m)). The size-versus-aggregation curve:

m (aggregated commitments) PROOF SIZE TYPICAL USE
1 688 B CETCH, T_MINT, T_DCLAIM, T_PMINT (single output)
2 754 B CXFER with recipient + change (the common case)
4 820 B CXFER splitting to 4 outputs
8 886 B CXFER splitting to 8 outputs (the protocol's max)

The protocol caps m ∈ {1, 2, 4, 8} — there is no m = 3 or m = 5. Pure-JS in-browser performance per the reference implementation: ~250 ms to prove and ~150 ms to verify a single proof. Batch verification combines N proofs into one multi-exponentiation via random linear combination — failure probability ≤ 2/order ≈ 2⁻²⁵⁵.

The Bulletproof construction requires no trusted setup — every parameter is derived deterministically from the published domains.


Kernel signatures (the Mimblewimble move)

Range proofs prove every output amount is non-negative. They do not prove that the sum of outputs equals the sum of inputs. That second guarantee — supply conservation — is what kernel signatures provide.

The trick is structural: instead of signing the transaction with the input owner's key, sign with the balancing factor.

For a CXFER consuming inputs with commitments Σ C_in and producing outputs with commitments Σ C_out, define the excess point:

E' = (Σ C_out) − (Σ C_in)

If amounts balance (Σ output values = Σ input values), the H components cancel and:

E' = (Σ r_out − Σ r_in) · G   = excess · G

E' is a point on the curve whose H component is zero — i.e., a point in the subgroup generated by G alone, with known discrete log excess. A BIP-340 Schnorr signature under E'.x_only() is provable to construct if and only if you know excess. The signer (the input owner) computes excess = Σ r_out − Σ r_in and signs.

If amounts do not balance (an attacker tries to create value), E' has a non-zero H component:

E' = δ · H + excess · G    (δ = imbalance)

Producing a valid Schnorr signature under this E' requires solving for the combined scalar — equivalent to finding the discrete log of H with respect to G. That is the NUMS assumption from §3.1. A passing kernel signature is a proof that amounts balance.

The kernel message itself binds asset_id, every asset input's outpoint, every output commitment, and (for T_BURN) the public burned amount:

kernel_msg = SHA256(
    "tacit-kernel-v1"
    || asset_id(32)
    || in_count(1)  || (input_txid_BE(32) || input_vout_LE(4)) × in_count
    || out_count(1) || output_commitment(33) × out_count
    || burned_amount_LE(8)   # 0 for CXFER, > 0 for T_BURN
)

Tampering with any of those fields invalidates the signature. For T_BURN specifically, the public burned_amount enters the balance equation: Σ C_in = burned_amount · H + Σ C_out, equivalently E' = burned · H + Σ C_out − Σ C_in. The same Discrete Log Problem (DLP) argument applies — the only way to satisfy this with a Schnorr signature is to actually have burned exactly burned_amount of value.

flowchart TD
    A[Sender wallet has assets with known<br/>amounts a_in and blindings r_in]
    B[Pick recipient + change amounts<br/>a_recip + a_change equals sum of a_in]
    C[Derive recipient blinding r_recip<br/>via ECDH and HMAC<br/>per tag tacit-blind-v1]
    D[Derive change blinding r_change<br/>via HMAC of sender privkey<br/>per tag tacit-change-v1]
    E[Build commitments<br/>C_recip equals a_recip times H plus r_recip times G<br/>C_change equals a_change times H plus r_change times G]
    F[Compute aggregated bulletproof<br/>over both commitments]
    G[Compute excess equals<br/>r_recip plus r_change minus sum of r_in]
    H[Compute E prime equals<br/>sum of C_out minus sum of C_in<br/>which equals excess times G]
    I[BIP-340 Schnorr sign kernel_msg<br/>under E_prime x_only key]
    J[Broadcast reveal tx with<br/>witness 1 envelope script]
    A --> B
    B --> C
    B --> D
    C --> E
    D --> E
    E --> F
    C --> G
    D --> G
    G --> H
    H --> I
    F --> J
    I --> J

Figure 1: CXFER construction. The kernel signature's existence under E' depends on the excess having no H component — which requires amounts balance.


Domain-separated HMAC derivations

Every blinding factor and every amount-encryption keystream in Tacit is derived deterministically from one of two Hash-based Message Authentication Code (HMAC)-SHA256 keys:

  • The wallet's privkey (wallet_priv), for self-derivations (change outputs, the etcher's own supply opening)
  • SHA256(<term:ECDH>(my_priv, their_pub).x), for peer-derivations (recipient outputs in CXFER, recipient amount keystreams)

Each derivation is tagged by a domain string + per-output anchor (anchor || vout_LE). The full set of on-chain domain tags:

TAG PURPOSE WHERE USED
tacit-blind-v1 ECDH-derived recipient blinding scalar CXFER recipient output
tacit-change-v1 Self-derived change blinding scalar CXFER + T_BURN change outputs
tacit-etch-v1 Etcher's supply blinding scalar CETCH supply commitment
tacit-mint-blind-v1 Issuer's mint blinding scalar T_MINT new-supply commitment
tacit-amount-v1 ECDH-derived recipient amount keystream (8B) CXFER recipient amount_ct
tacit-amount-self-v1 Self-derived amount keystream (8B) CXFER + T_BURN change amount_ct
tacit-etch-amount-v1 Etcher's supply keystream (8B) CETCH amount_ct
tacit-mint-amount-v1 Issuer's mint keystream (8B) T_MINT amount_ct

The amount_ct field in every envelope is the 8-byte u64 LE amount XOR'd with an 8-byte HMAC keystream — recipient-decryptable for transfer outputs, self-decryptable for change and etch outputs. This is what gives wallets privkey-only recovery: scan the chain, derive the keystream from (my_priv, sender_pubkey, anchor, vout), XOR with amount_ct, get the amount in cleartext, verify it opens the on-chain Pedersen commitment with the same derivation's blinding factor.

The anchor is per-tx entropy that prevents cross-tx correlation. For CXFER, T_AXFER, and T_BURN: anchor = first_asset_input_txid_BE || first_asset_input_vout_LE — i.e., the spent UTXO's outpoint. For CETCH and T_MINT (which have no asset input), the anchor predates the envelope: anchor = first_commit_input_txid_BE || first_commit_input_vout_LE. Either way, Bitcoin consensus prevents any outpoint from being spent twice, so each anchor is unique across all valid transactions that reference it as a first input. Combined with the per-output vout_LE suffix, no two outputs across all valid envelopes can ever reuse the same (domain, anchor, vout) triple. That uniqueness is what makes deterministic recovery of openings from chain + privkey alone safe.


Privkey-only recovery

Tacit's claim — "your privkey plus the chain is enough" — is a direct consequence of the above:

sequenceDiagram
    autonumber
    participant W as Wallet (fresh install, privkey imported)
    participant E as Esplora (mempool.space + blockstream.info in parallel)
    participant L as Local validator

    W->>E: scan chain for outputs paying my pubkey
    E-->>W: candidate UTXO list
    loop for each candidate (txid, vout)
        W->>E: fetch parent envelope tx
        E-->>W: tx bytes
        W->>L: validateOutpoint(txid, vout)
        L->>L: walk ancestry back to CETCH / T_MINT
        L->>L: verify every range proof, every kernel sig, every mint sig
        L->>W: VALID + asset_id + sender_pubkey + anchor
        W->>W: derive ECDH(my_priv, sender_pub)
        W->>W: derive keystream via HMAC under correct tag + anchor + vout
        W->>W: XOR amount_ct → amount in cleartext
        W->>W: derive blinding via HMAC under correct tag + anchor + vout
        W->>W: verify pedersenCommit(amount, blinding) == on-chain commitment
        W->>W: credit balance
    end

Figure 2: Recovery flow. No off-chain proof exchange and no server in the trust path.

The one operational caveat is atomic-intent recipient UTXOs (SPEC §5.7.6) where uniform-random blinding is used so browse-and-take can publish a cleartext amount on-chain without leaking via baby-step-giant-step. Those specific UTXOs fall back to local cache or a 24-hour worker fulfilment record for recovery. Every other operation's outputs recover from privkey + chain alone, unconditionally.

This is the architectural difference from RGB and Taproot Assets — both of which buy on-chain privacy at the cost of pushing validity off-chain. The recipient must receive and store a proof chain from the sender; losing it loses the asset. Tacit pays for on-chain validity in larger witnesses and keeps recovery on-chain.


Range disclosures and openings

Two off-chain primitives extend the on-chain machinery without touching Bitcoin's witness data:

  • Per-UTXO opening. A holder publishes (amount, blinding) for a specific UTXO. Anyone verifies pedersenCommit(amount, blinding) == on-chain commitment. The UTXO's amount becomes publicly auditable while the holder's other UTXOs stay confidential. Used for Over-The-Counter (OTC) listings, collateral attestation, and supply attestation at etch time.
  • Range disclosure (balance ≥ K). A holder publishes a bulletproof on C_sum − K · H where C_sum = Σ C_i for a chosen set of UTXOs of an asset. The verifier reconstructs C_sum from the on-chain commitments and checks the proof. A valid proof asserts Σ a_i ≥ K — the holder proves they have at least K of the asset without revealing exactly how much they hold. Useful for collateral coverage attestation and "treasury size at least X" disclosures.

Both are signed disclosure_msg = SHA256("tacit-disclosure-v1" || asset_id || N || utxos || threshold || rangeproof || owner_pubkey) under the owner's BIP-340 Schnorr key. They live entirely on the worker (off-chain) but verify against on-chain commitments — so the worker storing them cannot tamper without invalidating the proof.


References