From 57c7958e0772c6b87bee9c5d4a7d78aa0026476c Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Tue, 3 Mar 2026 13:39:53 -0600 Subject: [PATCH 1/6] design: Shielded outputs --- text/0000-shielded-outputs.md | 884 ++++++++++++++++++++++++++++++++++ 1 file changed, 884 insertions(+) create mode 100644 text/0000-shielded-outputs.md diff --git a/text/0000-shielded-outputs.md b/text/0000-shielded-outputs.md new file mode 100644 index 0000000..2ca27d1 --- /dev/null +++ b/text/0000-shielded-outputs.md @@ -0,0 +1,884 @@ +- Feature Name: shielded_outputs +- Start Date: 2026-03-03 +- RFC PR: (leave this empty) +- Hathor Issue: (leave this empty) +- Author: Hathor Labs + +# Summary +[summary]: #summary + +This RFC introduces **shielded outputs** for Hathor transactions — a header-based extension that hides output amounts and, optionally, token types using Pedersen commitments, Bulletproof range proofs, and asset surjection proofs. Rather than defining a new transaction version, shielded outputs attach to existing transaction types (regular transactions and token creation transactions) via a `ShieldedOutputsHeader`, preserving full backward compatibility. Two privacy tiers are offered: `AmountShieldedOutput` (amount hidden, token visible) and `FullShieldedOutput` (both amount and token hidden). Recipients recover hidden values through ECDH-based range proof rewinding, requiring no out-of-band communication. + +# Motivation +[motivation]: #motivation + +**Privacy is a fundamental property of money.** Physical cash does not broadcast the denomination of every bill exchanged between two parties, yet transparent blockchains do exactly that. Every Hathor transaction today reveals the exact amount transferred, the token involved, and — combined with address analysis — a detailed picture of economic activity. + +This transparency creates real problems: + +- **Payroll and compensation.** An employer paying salaries on-chain reveals every employee's compensation to anyone who looks. +- **Business-to-business payments.** Suppliers, vendors, and partners can reverse-engineer pricing, margins, and deal terms from on-chain flows. +- **Trading and DeFi.** Front-runners and MEV extractors exploit visible amounts to sandwich trades or copy strategies. +- **Personal finance.** Any recipient of a payment can trace the sender's full balance and transaction history. +- **Multi-token privacy.** Hathor supports custom tokens. When token types are visible, observers can track the flow of specific assets (e.g., loyalty points, governance tokens, stablecoins), revealing business relationships and portfolio composition. + +Shielded outputs address these problems by making amounts and token types cryptographically opaque to everyone except the transaction participants, while preserving the ability of every full node to verify that no inflation or double-spending has occurred. + +**Expected outcome:** Users can opt into amount privacy (and optionally token-type privacy) on a per-output basis, within ordinary Hathor transactions, with no protocol-level changes to transaction versions, input formats, or the DAG structure. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## 1. What Are Shielded Outputs? + +A shielded output replaces the plaintext `(amount, token)` pair in a standard `TxOutput` with a cryptographic **commitment** — a value that provably encodes the correct amount and token without revealing either. + +``` +Standard output: 100 HTR to address H7bKm... + ^^^ visible to everyone + +Amount-shielded: [commitment] HTR to address H7bKm... + ^^^^^^^^^^^ ^^^ token still visible + amount hidden + +Fully shielded: [commitment] [asset commitment] to address H7bKm... + ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^ + amount hidden token hidden +``` + +Hathor offers **two privacy tiers**, selectable per output: + +| Tier | Hides Amount | Hides Token | Proof Overhead | Use Case | +|------|:---:|:---:|---|---| +| `AmountShieldedOutput` | Yes | No | Range proof (~675 B) | Hide salary amounts while token type is public | +| `FullShieldedOutput` | Yes | Yes | Range proof + surjection proof (~675 + ~130 B) | Full privacy for multi-token transactions | + +Both tiers use **Pedersen commitments** for amounts and **Bulletproof range proofs** to guarantee amounts are non-negative. `FullShieldedOutput` additionally uses **blinded asset tags** and **asset surjection proofs** to hide and validate the token type. + +### The Privacy Stack + +Shielded outputs are Phase B of Hathor's three-phase privacy roadmap: + +``` ++-----------------------------------------------------------+ +| Phase C: INPUT UNLINKABILITY (future) | +| Ring signatures or nullifiers | +| "Which output was spent?" -> Hidden | ++-----------------------------------------------------------+ ++-----------------------------------------------------------+ +| Phase B: SHIELDED OUTPUTS <- THIS RFC | +| Pedersen commitments + Bulletproofs + surjection proofs | +| "How much?" + "Which token?" -> Hidden | ++-----------------------------------------------------------+ ++-----------------------------------------------------------+ +| Phase A: ADDRESS PRIVACY (Silent Payments RFC) | +| ECDH-derived one-time addresses | +| "Who received it?" -> Hidden | ++-----------------------------------------------------------+ +``` + +Each phase is independently useful and composes with the others. With all three active, an observer learns almost nothing about a transaction beyond the number of inputs and outputs. + +## 2. How It Works + +### Shielding: Moving Funds into Privacy + +To shield funds, a user creates a transaction with transparent inputs and shielded outputs: + +``` +BEFORE (transparent): + Alice has 100 HTR (visible on-chain) + +SHIELDING TRANSACTION: + Input: 100 HTR (transparent, amount visible) + Output0: [commitment_A] (shielded, 90 HTR hidden inside) + Output1: [commitment_B] (shielded, 5 HTR hidden inside) + Fee: 5 HTR (always transparent) + +AFTER: + Alice has two shielded UTXOs totaling 95 HTR + Observer sees: "100 HTR entered the shielded pool" + Observer does NOT know: the split between Output0 and Output1 +``` + +Note: at least 2 shielded outputs are required when all inputs are transparent (Rule 4 — see Section 4.3). This prevents an observer from trivially deducing the single output's amount. + +### Unshielding: Returning to Transparency + +To unshield, a user spends shielded inputs into transparent outputs: + +``` +UNSHIELDING TRANSACTION: + Input: [commitment] (shielded, 90 HTR hidden) + Output0: 60 HTR to Bob (transparent) + Output1: [commitment'] (shielded change, 25 HTR hidden) + Fee: 5 HTR (transparent) + +Observer sees: 60 HTR left the shielded pool, plus a shielded change output +Observer does NOT know: the shielded input amount or the change amount +``` + +### Mixed Transactions + +Transparent and shielded inputs/outputs can be freely combined in a single transaction. The balance equation works uniformly: transparent amounts are treated as "trivial commitments" with a zero blinding factor, and the homomorphic verification covers everything in one check. + +| Type | Transparent In | Shielded In | Transparent Out | Shielded Out | Use Case | +|------|:---:|:---:|:---:|:---:|---| +| Standard | Yes | — | Yes | — | Legacy transaction | +| Shielding | Yes | — | Optional | Yes (>=2) | Enter shielded pool | +| Confidential | — | Yes | — | Yes | Fully private transfer | +| Unshielding | — | Yes | Yes | Optional | Exit shielded pool | +| Fully mixed | Yes | Yes | Yes | Yes | Maximum flexibility | + +### Fee Model + +Shielded outputs impose additional verification costs (Bulletproof verification ~1ms per output, surjection proof verification, larger storage). A per-output fee compensates the network: + +- `FEE_PER_AMOUNT_SHIELDED_OUTPUT`: charged per `AmountShieldedOutput` (default: 1 HTR base unit) +- `FEE_PER_FULL_SHIELDED_OUTPUT`: charged per `FullShieldedOutput` (default: 2 HTR base units) + +Fees are declared in the existing `FeeHeader`, are fully transparent, and are burned. See [Section 4.5](#45-fee-mechanism) for details. + +### What Remains Visible + +Even with shielded outputs, the following information is always public: + +- **Transaction structure**: number of inputs, number of outputs, which outputs are shielded vs. transparent +- **Fee amounts**: always plaintext HTR (Rule 2) +- **Authority outputs**: mint/melt authority tokens are always transparent (Rule 7) +- **Scripts**: the locking script (recipient address) remains visible unless combined with Silent Payments (Phase A) +- **Transparent inputs/outputs**: their amounts and tokens remain fully visible + +## 3. Shielded Addresses (Pending Decision) + +Receiving shielded outputs requires the sender to know the recipient's full public key (not just the address hash) in order to establish the ECDH shared secret for blinding factor communication. Today, P2PKH addresses only encode a hash of the public key. + +A new **shielded address type** would bundle the necessary keys into a single address the recipient can publish. + +### Key Components + +A shielded address encodes two keys: + +- **Scan public key** (`B_scan`, 33 bytes): Used for ECDH — the sender computes a shared secret with this key, and the recipient uses the corresponding private key to recover blinding factors. +- **Spend public key** (`B_spend`, 33 bytes): Controls spending authority. The output script locks funds to `hash(B_spend)` (standard P2PKH). + +> **On-chain representation:** The output script remains a standard P2PKH locking to `hash(B_spend)`. The scan public key is never stored on-chain — it is only known to the sender (from the shielded address) and the recipient. + +### Candidate Formats + +Three formats are under consideration: + +#### Option A: Compact (53 bytes of data) + +``` +scan_pubkey(33) || hash(spend_pubkey)(20) +``` + +The spend pubkey is represented as a 20-byte hash (like a standard P2PKH address). Shorter addresses, and fully sufficient for shielded outputs (ECDH uses the scan key; output locking uses the spend key hash). However, the full spend pubkey is not recoverable from the address alone, which prevents future Silent Payments integration (one-time address derivation requires EC point addition on `B_spend`). + +#### Option B: Full Keys (66 bytes of data) + +``` +scan_pubkey(33) || spend_pubkey(33) +``` + +Both keys are present in full. Longer addresses, but the sender has everything needed without any additional lookups. Compatible with BIP352-style Silent Payments, where the spend pubkey is required for one-time address derivation. + +#### Option C: Spend Key Only (33 bytes of data) + +``` +spend_pubkey(33) +``` + +Only the spend public key is encoded — no scan key at all. This is the shortest possible shielded address and the simplest to implement. However, without a scan key, the wallet cannot delegate blockchain scanning to a third-party service (since the scan private key is what enables a service to detect incoming payments without having spending authority). The wallet must download and trial-decrypt the entire shielded transaction history itself to compute its balance, which is impractical for light clients. + +### Tradeoff Analysis + +| Aspect | Option A (Compact) | Option B (Full Keys) | Option C (Spend Only) | +|--------|---|---|---| +| Address length | ~90 characters | ~115 characters | ~55 characters | +| Data payload | 53 bytes | 66 bytes | 33 bytes | +| Spend pubkey recovery | Requires lookup or out-of-band | Self-contained | Self-contained | +| Silent Payments compat. | No (need full spend pubkey for ECDH) | Yes | No (no scan key for ECDH) | +| QR code density | Lower (smaller) | Higher (larger) | Lowest (smallest) | +| Delegated scanning | Yes (scan key separates detection from spending) | Yes (scan key separates detection from spending) | No (wallet must scan entire history itself) | +| Light client support | Yes | Yes | No | + +**Status:** Decision pending. Both options are documented here for review. The implementation currently uses the full public key for ECDH, so Option B aligns with the existing code path. + +## 4. Wallet Integration Guide + +### Receiving Shielded Payments + +Every shielded output contains an `ephemeral_pubkey` field (33 bytes). The recipient recovers the hidden amount through **ECDH + range proof rewind**: + +1. **Address match**: The wallet checks if the output script contains `hash(spend_pubkey)` matching one of its keys. +2. **ECDH shared secret**: `s = SHA256(scan_privkey * ephemeral_pubkey)`. +3. **Nonce derivation**: `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. +4. **Range proof rewind**: Using the nonce as the rewind key, the wallet calls `rewind_range_proof()` which returns the committed `(value, blinding_factor, message)`. +5. **For `FullShieldedOutput`**: The `message` field contains `token_uid(32B) || asset_blinding_factor(32B)`. The wallet reconstructs the expected asset commitment from these values and cross-checks against the on-chain `asset_commitment` to prevent a malicious sender from claiming a worthless token is HTR. + +If rewind fails (wrong nonce), the output is not addressed to this wallet — skip it silently. + +### Sending Shielded Payments + +1. **Choose privacy tier** per output (`AmountShieldedOutput` or `FullShieldedOutput`). +2. **Generate ephemeral keypair** `(e, E = e*G)` for each output. +3. **Compute ECDH shared secret** with the recipient's scan public key (`B_scan`). +4. **Derive nonce** for range proof construction. +5. **Create Pedersen commitment**: `C = amount * H_token + blinding * G`. +6. **Create range proof** using the deterministic nonce (enables recipient rewind). +7. **For `FullShieldedOutput`**: Create blinded asset commitment and surjection proof. +8. **Balance blinding factors**: Assign random blinding factors to all shielded outputs except the last, which receives the balancing factor: `r_last = sum(r_inputs) - sum(r_other_outputs)`. +9. **Compute and attach fee** in `FeeHeader`. + +### Blinding Factor Management and Backup + +The wallet must store blinding factors for every owned shielded UTXO — without them, the funds cannot be spent. + +**Recovery guarantee**: Since blinding factors for received outputs are derived deterministically from `ECDH(scan_privkey, ephemeral_pubkey)` — and both the scan private key (derivable from the seed) and the ephemeral pubkey (stored on-chain) are always available — a **seed backup is sufficient** for full recovery. The wallet re-derives all blinding factors by scanning the blockchain. + +For change outputs, wallets should use deterministic derivation from the input blinding factors and output index to ensure recoverability. + +### Balance Display + +From the wallet owner's perspective, balances are fully accurate: + +- **Total balance**: sum of transparent UTXOs + sum of decrypted shielded UTXOs. +- **Per-token breakdown**: the wallet knows which token each shielded UTXO holds. +- **Shielded vs. transparent breakdown**: optionally shown per token. + +Privacy only affects external observers — the wallet owner always sees exact amounts. + +### Rule 4 Compliance + +When all inputs are transparent, the wallet must automatically ensure at least 2 shielded outputs (or include a transparent output). This is typically satisfied naturally (payment + change), but the wallet may need to create a zero-value absorber output in edge cases. + +### Fee Computation + +``` +shielded_fee = (count_amount_shielded * FEE_PER_AMOUNT_SHIELDED_OUTPUT) + + (count_full_shielded * FEE_PER_FULL_SHIELDED_OUTPUT) +total_fee = shielded_fee + standard_fee_if_any +``` + +The wallet should display the expected fee before the user confirms. + +## 5. Explorer and Indexer Impact + +### What Breaks + +| Capability | Status | Reason | +|---|---|---| +| Show output amounts | Broken for shielded | Hidden behind Pedersen commitments | +| Identify output token type | Broken for `FullShieldedOutput` | Hidden behind blinded asset commitments | +| Compute address balances | Broken for shielded | Cannot sum opaque curve points | +| Token supply tracking | Unaffected | Mint/melt transactions cannot contain shielded outputs (Rule 8) | +| Rich list / rankings | Broken for shielded | Balances not computable | + +### What Still Works + +- Transaction structure (input/output count, shielded vs. transparent) +- Transparent outputs (amounts, tokens, scripts — unchanged) +- Fee amounts (always transparent) +- Cryptographic proof verification (anyone can verify correctness) +- Authority UTXO tracking (always transparent, Rule 7) +- Token metadata (name, symbol from creation transaction) + +### Mitigations + +- **Shielded pool boundary tracking**: Explorers can track aggregate amounts entering/leaving the shielded pool from the transparent side of shielding/unshielding transactions. +- **View key delegation**: Users can optionally share view keys with trusted explorers for selective disclosure. +- **"Shielded" indicator**: Explorers should display shielded outputs with a clear indicator, showing commitment hex values but never placeholder amounts. + +## 6. Mint/Melt Transactions Are Always Transparent + +Transactions that exercise mint or melt authority **cannot** contain shielded outputs (Rule 8). Both the authority outputs (Rule 7) and all value outputs in a mint/melt transaction remain fully transparent. + +This means: + +- An explorer can see **when** mint authority is exercised and **exactly how many** tokens were minted or melted. +- Token supply remains fully auditable for all custom tokens — no trust in the token creator is required. +- Users who want amount privacy must move minted tokens into shielded outputs in a separate transaction. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## 4.1 Cryptographic Primitives + +### Pedersen Commitments + +A Pedersen commitment to a value `v` with blinding factor `r` using generator `H` is: + +``` +C = v * H + r * G + +Where: + v = amount (u64, range [0, 2^64)) + r = blinding factor (32-byte scalar) + H = generator point specific to the token type + G = secp256k1 base generator +``` + +**Properties:** +- **Hiding**: Given `C`, an observer cannot determine `v` without knowing `r`. +- **Binding**: Given `C`, one cannot find `(v', r')` such that `C = v'*H + r'*G` and `v' != v`, unless one knows the discrete log of `H` w.r.t. `G` (computationally infeasible for NUMS generators). +- **Homomorphic**: `C1 + C2 = (v1+v2)*H + (r1+r2)*G`. This enables balance verification without revealing amounts. + +### NUMS Asset Tag Derivation + +Each token has a deterministic generator `H_token` derived via a Nothing-Up-My-Sleeve (NUMS) construction: + +``` +H_token = NUMS_hash(token_uid) + +Algorithm: + tag = SHA256("Hathor_AssetTag_v1" || token_uid) + H_token = generator_from_tag(tag) +``` + +The domain separator `"Hathor_AssetTag_v1"` prevents cross-protocol collisions. The construction guarantees no one knows `x` such that `H_token = x*G`, which is essential for the binding property. + +**Token UID normalization**: HTR uses `token_uid = b'\x00'` (1 byte) internally, but the crypto library requires 32 bytes. The normalization function pads HTR's token UID with 31 zero bytes. + +### Blinded Asset Commitments + +For `FullShieldedOutput`, the asset tag is blinded to hide the token type: + +``` +A = H_token + r_asset * G + +Where: + H_token = unblinded NUMS generator for this token + r_asset = random asset blinding factor (32-byte scalar) + A = blinded asset commitment (33 bytes, compressed point) +``` + +An observer sees `A` — a random-looking curve point indistinguishable from any other token's blinded commitment. + +### Bulletproof Range Proofs + +Each shielded output includes a Bulletproof range proof demonstrating: + +``` +The committed amount v satisfies: 1 <= v < 2^64 +``` + +The lower bound of 1 (not 0) prevents zero-amount outputs that could be used in certain attack vectors. + +**Architecture: separate proofs, not aggregated.** Each output carries its own independent Bulletproof. This design supports: + +- **Multi-party transactions**: Each party generates proofs for their own outputs independently, without revealing amounts. +- **Atomic swaps**: No need to share blinding factors across parties. +- **UTXO pruning**: Spent output proofs can be discarded independently. + +Performance optimization is achieved through **batch verification** (`verify_multi`), which amortizes multi-exponentiation cost across proofs (estimated 30-50% CPU reduction for multi-output transactions), and parallel verification across transactions. + +**Proof size**: ~675 bytes typical, bounded by `MAX_RANGE_PROOF_SIZE = 1024` bytes. + +### Asset Surjection Proofs + +For `FullShieldedOutput` only. Proves the output's blinded asset commitment corresponds to one of the input asset commitments, without revealing which one. + +``` +Given: + Input asset commitments: A_1, A_2, ..., A_n + Output asset commitment: A_out + +Compute differences: d_i = A_out - A_i for each input i + +For the matching input j (same token): + d_j = (H_token + r_out*G) - (H_token + r_j*G) = (r_out - r_j) * G + -> discrete log is KNOWN + +For non-matching inputs i != j (different token): + d_i = (H_out + r_out*G) - (H_i + r_i*G) = (H_out - H_i) + (r_out - r_i)*G + -> discrete log is UNKNOWN + +A ring signature on {d_1, ..., d_n} proves knowledge of the discrete log +for exactly one d_i, without revealing which one. +``` + +**Proof size**: Grows linearly with the number of inputs in the surjection domain. For a typical transaction with 3 inputs: ~130 bytes. Maximum: `MAX_SURJECTION_PROOF_SIZE = 4096` bytes. + +### Homomorphic Balance Verification + +The balance equation covers all inputs and outputs uniformly: + +``` +sum(C_inputs) == sum(C_outputs) + sum(C_fee_entries) + +Where: + - Shielded inputs/outputs use their on-chain commitment directly + - Transparent inputs/outputs use trivial commitments: C = amount * H_token + - Fee entries from FeeHeader are treated as transparent outputs +``` + +Expanding the equation and grouping by generator: + +``` +(sum(v_in) - sum(v_out) - sum(fees)) * H + (sum(r_in) - sum(r_out)) * G == O +``` + +Since `H` and `G` are linearly independent (no known discrete log relationship), both scalar coefficients must be zero: + +1. `sum(v_in) = sum(v_out) + sum(fees)` — values balance. +2. `sum(r_in) = sum(r_out)` — blinding factors balance. + +The wallet enforces condition (2) by construction: it assigns random blinding factors to all outputs except the last, which receives the balancing residual. + +## 4.2 Data Structures + +### AmountShieldedOutput + +Hides the amount; token type remains visible. + +| Field | Type | Size | Description | +|-------|------|------|-------------| +| `commitment` | bytes | 33 B | Pedersen commitment `C = v*H_token + r*G` | +| `range_proof` | bytes | ~675 B (max 1024) | Bulletproof range proof | +| `script` | bytes | variable (max 1024) | Locking script (P2PKH, etc.) | +| `token_data` | int | 1 B | Token index (same semantics as `TxOutput.token_data`) | +| `ephemeral_pubkey` | bytes | 33 B | Compressed secp256k1 point for ECDH recovery | + +**Typical total size**: ~770 bytes per output (with P2PKH script). + +### FullShieldedOutput + +Hides both amount and token type. + +| Field | Type | Size | Description | +|-------|------|------|-------------| +| `commitment` | bytes | 33 B | Pedersen commitment `C = v*A + r*G` (uses blinded generator) | +| `range_proof` | bytes | ~675 B (max 1024) | Bulletproof range proof | +| `script` | bytes | variable (max 1024) | Locking script | +| `asset_commitment` | bytes | 33 B | Blinded asset tag `A = H_token + r_asset*G` | +| `surjection_proof` | bytes | ~130 B (max 4096) | Asset surjection proof | +| `ephemeral_pubkey` | bytes | 33 B | Compressed secp256k1 point for ECDH recovery | + +**Typical total size**: ~930 bytes per output (with P2PKH script, 3-input surjection domain). + +### ShieldedOutputsHeader + +Shielded outputs are carried in a transaction header, not in the standard `tx.outputs` list. + +| Field | Size | Description | +|-------|------|-------------| +| Header ID | 1 B | `0x12` (`VertexHeaderId.SHIELDED_OUTPUTS_HEADER`) | +| `num_outputs` | 1 B | Number of shielded outputs (max 32) | +| Outputs | variable | Concatenated serialized outputs | + +**Maximum shielded outputs per transaction**: `MAX_SHIELDED_OUTPUTS = 32`. + +### Wire Format + +``` +AmountShieldedOutput serialization: + mode(1B=0x01) | commitment(33B) | rp_len(2B BE) | range_proof(var) | + script_len(2B BE) | script(var) | token_data(1B) | ephemeral_pubkey(33B) + +FullShieldedOutput serialization: + mode(1B=0x02) | commitment(33B) | rp_len(2B BE) | range_proof(var) | + script_len(2B BE) | script(var) | asset_commitment(33B) | + sp_len(2B BE) | surjection_proof(var) | ephemeral_pubkey(33B) +``` + +The `mode` byte discriminates output types during deserialization. Length fields use big-endian unsigned 16-bit integers (`!H` struct format). + +### Sighash Coverage + +The transaction sighash includes: `mode`, `commitment`, `script`, `token_data` (amount-shielded) or `asset_commitment` (full-shielded), and `ephemeral_pubkey`. It **excludes** `range_proof` and `surjection_proof` — these are verified independently and do not affect the spending signature. + +## 4.3 Transaction Rules + +Seven rules govern shielded transactions: + +### Rule 1: Minimum Structure + +At least one input (or Nano Contract withdraw) and at least one output (transparent or shielded, or Nano Contract deposit) required. Standard transaction structure rules apply. + +### Rule 2: Fee Is Always Transparent + +The transaction fee is always a plaintext HTR amount, declared in a `FeeHeader`. The fee enters the balance equation as a trivial commitment: `C_fee = fee * H_HTR`. + +### Rule 3: Blinding Factors Must Balance + +``` +sum(r_input_shielded) = sum(r_output_shielded) +``` + +Transparent inputs and outputs contribute `r = 0`. The wallet enforces this by choosing the last shielded output's blinding factor as the balancing residual. For `FullShieldedOutput`, asset blinding factors must also balance: `sum(s_inputs) = sum(s_outputs)`. + +### Rule 4: Trivial Commitment Protection + +If **all** inputs are transparent, at least 2 shielded outputs are required. + +**Rationale**: With all transparent inputs, the total input amount is public. A single shielded output with no transparent outputs would have its blinding factor forced to zero (to satisfy Rule 3), making the commitment trivially deducible as `C = (total_input - fee) * H`. + +**Exception**: This rule is relaxed if any input is shielded (the input's non-zero blinding factor provides the necessary entropy). It also does not apply if there is a transparent output alongside the single shielded output. + +### Rule 5: Range Proofs + +Every shielded output MUST include a valid Bulletproof range proof proving the committed amount is in `[1, 2^64)`. The minimum value of 1 (not 0) prevents zero-amount outputs. + +### Rule 6: Surjection Proofs + +Every `FullShieldedOutput` MUST include a valid asset surjection proof proving its asset commitment corresponds to one of the input asset commitments. `AmountShieldedOutput` does not require a surjection proof (its token is visible via `token_data`). Transparent inputs contribute their unblinded NUMS asset tag to the surjection proof domain. + +### Rule 7: Authority Outputs Remain Transparent + +Mint and melt authority outputs MUST always be transparent `TxOutput`s. Attempting to set authority bits on a shielded output is invalid (`ShieldedAuthorityError`). Authority tokens control token supply and must remain auditable. + +### Rule 8: Mint/Melt Transactions Cannot Have Shielded Outputs + +A transaction that contains any mint or melt operation (i.e., spends a mint or melt authority input) MUST NOT include any shielded outputs (`ShieldedMintMeltForbiddenError`). All value outputs in a mint/melt transaction must be transparent. + +**Rationale**: Keeping mint/melt transactions fully transparent ensures that token supply remains publicly auditable. Explorers and users can always verify the total circulating supply of any custom token by summing its mint and melt operations. Users who want amount privacy can move minted tokens into shielded outputs in a subsequent transaction. + +## 4.4 Verification Pipeline + +Verification is split into two phases, matching Hathor's existing architecture. + +### Phase 1: Without Storage (Basic Verification) + +Called during `verify_without_storage`. No UTXO lookups needed. + +``` +verify_shielded_outputs() + |-- verify_commitments_valid() + | Checks: all commitments are 33-byte valid secp256k1 points + | Checks: asset_commitments (FullShielded) are valid points + | Checks: ephemeral_pubkeys are valid secp256k1 points + | + |-- verify_authority_restriction() [Rule 7] + | Checks: no shielded output has authority bits set + | + |-- verify_range_proofs() [Rule 5] + | Checks: each shielded output's Bulletproof verifies against + | its commitment and generator (unblinded for Amount, + | blinded for Full) + | + |-- verify_trivial_commitment_protection() [Rule 4, conservative] + | Checks: at least 2 shielded outputs (relaxed with storage) + | + |-- verify_shielded_fee() + Checks: FeeHeader exists + Checks: total_declared_fee >= shielded_fee (lower bound) +``` + +### Phase 2: With Storage (Full Verification) + +Called during `verify` / `_verify_shielded_header`. Requires UTXO lookups to resolve input types. + +``` +_verify_shielded_header() + |-- verify_surjection_proofs() [Rule 6] + | Builds surjection domain from input asset commitments: + | - Transparent inputs: derive_asset_tag(token_uid) + | - Shielded inputs: use on-chain asset_commitment + | Verifies each FullShieldedOutput's proof against the domain + | + |-- verify_shielded_balance() + | Collects all input commitments (shielded: direct, transparent: trivial) + | Collects all output commitments (shielded: direct, transparent: trivial) + | Appends fee entries as transparent outputs + | Verifies: sum(inputs) == sum(outputs) + | + |-- _verify_trivial_commitment_with_storage() [Rule 4, relaxed] + If any input is shielded: allow 1 shielded output + Otherwise: require >= 2 shielded outputs + +verify_token_rules(shielded_fee=X) [Fee exact match] + Existing fee verification, augmented with shielded_fee addend + Checks: standard_fee + shielded_fee == fees_from_fee_header (exact) +``` + +### Token UID Normalization + +The `_normalize_token_uid()` function handles the mismatch between Hathor's internal 1-byte HTR token UID (`b'\x00'`) and the crypto library's 32-byte requirement. HTR is padded with 31 zero bytes; custom tokens (already 32 bytes) pass through unchanged. + +## 4.5 Fee Mechanism + +### Fee Calculation + +```python +shielded_fee = (n_amount_shielded * FEE_PER_AMOUNT_SHIELDED_OUTPUT) + + (n_full_shielded * FEE_PER_FULL_SHIELDED_OUTPUT) +``` + +Settings in `HathorSettings`: + +| Setting | Default | Description | +|---------|---------|-------------| +| `FEE_PER_AMOUNT_SHIELDED_OUTPUT` | 1 | HTR base units per `AmountShieldedOutput` | +| `FEE_PER_FULL_SHIELDED_OUTPUT` | 2 | HTR base units per `FullShieldedOutput` | + +These settings are gated by `ENABLE_SHIELDED_TRANSACTIONS`. + +### FeeHeader Integration + +Fees are declared in the existing `FeeHeader` mechanism. The `FeeHeader` entries are treated as transparent outputs in the homomorphic balance equation: + +``` +sum(C_in) == sum(C_out) + sum(C_fee_entry) +``` + +Each `C_fee_entry = fee_amount * H_token` for the corresponding token. This means the balance verification function does not need a separate `fee` parameter — fees are simply part of the output side. + +### Two-Phase Fee Verification + +1. **Without storage (lower bound)**: `total_declared_fee >= shielded_fee`. Cannot compute exact expected fee without storage (standard token fees depend on `chargeable_outputs` which requires UTXO lookups). +2. **With storage (exact match)**: `standard_fee + shielded_fee == fees_from_fee_header`. Both over-payment and under-payment are rejected. + +### Shielded Fees Subsume Token Fees + +Shielded outputs are not counted in `chargeable_outputs` for standard FEE-versioned token fee calculation. To prevent fee avoidance (shielding a token output to dodge `FEE_PER_OUTPUT`), the shielded fee rates are configured to be at least as large as the standard token output fee. + +## 4.6 ECDH Recovery Mechanism + +### Overview + +Every shielded output contains an `ephemeral_pubkey` field (33 bytes). This enables the recipient to recover the committed value without any out-of-band communication. + +### Sender Flow + +1. Generate ephemeral keypair: `(e, E = e*G)` on secp256k1. +2. Obtain recipient's scan public key `P = B_scan` (from the shielded address). +3. Compute shared secret: `s = SHA256(e * P)`. +4. Derive deterministic nonce: `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. +5. Create range proof using `nonce` as the nonce key (not random). +6. For `FullShieldedOutput`: embed `token_uid(32B) || asset_blinding_factor(32B)` in the range proof message. +7. Store `E` (33 bytes, compressed) in the shielded output's `ephemeral_pubkey` field. + +### Recipient Flow + +1. Parse output script; check if script contains `hash(spend_pubkey)` matching a wallet key. +2. Extract ephemeral pubkey `E` from the shielded output. +3. Compute shared secret: `s = SHA256(scan_privkey * E)` (same result since `scan_privkey*E = scan_privkey*e*G = e*scan_privkey*G = e*B_scan`). +4. Derive nonce: `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. +5. Call `rewind_range_proof(proof, commitment, nonce, generator)`: + - `generator` = `derive_asset_tag(token_uid)` for `AmountShieldedOutput` + - `generator` = `output.asset_commitment` for `FullShieldedOutput` +6. Returns: `(value, blinding_factor, message)`. +7. For `FullShieldedOutput`: extract `token_uid` and `asset_blinding_factor` from `message`, reconstruct expected asset commitment, and cross-check against on-chain value. + +### Security Properties + +- **Sighash binding**: The ephemeral pubkey is included in the transaction sighash, preventing MITM replacement. +- **Nonce uniqueness**: Each output uses a fresh ephemeral keypair. +- **Failed rewind**: Returns an error (not garbage) when the nonce is wrong — no false positives. +- **Forward secrecy**: Ephemeral keys are single-use. +- **Domain separation**: `"Hathor_CT_nonce_v1"` prefix isolates this derivation from other uses of the ECDH shared secret. +- **Token UID cross-check**: For `FullShieldedOutput`, the recovered `token_uid` is verified against the `asset_commitment` to prevent a malicious sender from embedding a fraudulent token UID. +- **No value logging**: Recovered amounts are never logged, even at DEBUG level. + +## 4.7 Feature Activation + +### Feature Flag + +```python +# hathor/feature_activation/feature.py +class Feature(StrEnum): + SHIELDED_TRANSACTIONS = 'SHIELDED_TRANSACTIONS' +``` + +### Settings + +```python +# hathor/conf/settings.py +ENABLE_SHIELDED_TRANSACTIONS: FeatureSetting = FeatureSetting.DISABLED +``` + +`FeatureSetting` is an enum with values: `DISABLED`, `ENABLED`, `FEATURE_ACTIVATION`. + +### Crypto Library Availability + +At startup, if `ENABLE_SHIELDED_TRANSACTIONS != DISABLED`, the system validates that the native `hathor_ct_crypto` library is available via `validate_shielded_crypto_available()`. This prevents silent failures where all shielded output operations would fail at runtime. + +Build command: +```bash +poetry run maturin develop --manifest-path hathor-ct-crypto/Cargo.toml --features python +``` + +## 4.8 Interaction with Other Features + +### Silent Payments (Phase A) + +The `ephemeral_pubkey` in shielded outputs serves double duty when Silent Payments are active: + +- **Without SP**: The ephemeral key derives blinding factors only. The recipient address is visible in the script. +- **With SP**: The same ECDH shared secret derives both the one-time recipient address and the blinding factors. A single ECDH operation provides recipient privacy + blinding factor communication. + +Combined: a shielded output hides the recipient (one-time address), the amount (Pedersen commitment), and optionally the token type (blinded asset tag). + +### Ring Signatures / Nullifiers (Phase C) + +Shielded outputs simplify decoy selection for ring signatures: + +- **Without shielded outputs**: Decoys must match on amount (otherwise amount mismatch reveals the real input). This limits the anonymity set. +- **With shielded outputs**: All outputs are opaque commitments — any shielded output is a valid decoy regardless of hidden amount or token. Larger anonymity sets with simpler selection logic. + +Surjection proofs naturally compose with ring signatures: the surjection domain includes all ring members (real + decoys), hiding which input is real. + +### Nano Contracts + +The `nc_caller` field in Nano Contract transactions identifies the calling address. Shielded outputs do not affect `nc_caller` — it remains a standard address. However, if Nano Contracts consume or produce shielded outputs in the future, the balance equation and proof generation would need to account for the contract's logic. This is out of scope for this RFC. + +# Drawbacks +[drawbacks]: #drawbacks + +1. **Transaction size increase.** A shielded output is ~770-930 bytes vs ~40 bytes for a transparent output (~20-25x larger). This increases storage, bandwidth, and propagation time. However, Hathor's logarithmic weight formula means the fee increase is moderate (not proportional to the size increase). + +2. **Verification cost.** Bulletproof range proof verification takes ~1ms per proof. For a transaction with 4 shielded outputs, this adds ~4ms of CPU time per transaction. Batch verification and parallelization can amortize this, but it remains significantly more expensive than transparent output verification. + +3. **Wallet complexity.** Wallets must implement ECDH key exchange, range proof rewind, blinding factor management, surjection proof generation, and fee computation. This is a substantial increase in wallet complexity compared to transparent-only transactions. + +4. **Explorer capability reduction.** Block explorers lose the ability to display amounts and compute balances for shielded outputs. This fundamentally changes the user experience of public blockchain explorers, which are a key tool for transparency and debugging. + +5. **Recipient public key requirement.** The sender must know the recipient's full compressed public key (not just the address hash) to establish the ECDH shared secret. This requires either a prior on-chain spend, a payment protocol, or a new address format. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## Why header-based (not a new TxVersion)? + +A new `TxVersion` would require changes throughout the codebase — every `match vertex.version:` statement, serialization logic, feature gating, and verification path. The header-based approach reuses Hathor's existing header infrastructure (already used by `NanoHeader` and `FeeHeader`), attaching shielded outputs to standard transactions (`REGULAR_TRANSACTION` and `TOKEN_CREATION_TRANSACTION`) with zero structural changes. This means existing transaction processing, mempool logic, and DAG management continue to work unmodified. + +## Why two output types (not one)? + +`AmountShieldedOutput` (amount hidden, token visible) is substantially smaller and cheaper to verify than `FullShieldedOutput` (both hidden). Many use cases only need amount privacy — e.g., hiding salaries paid in HTR, where the fact that the token is HTR is not sensitive. Offering both tiers lets users pay only for the privacy they need. + +## Why separate Bulletproofs (not aggregated)? + +Aggregated Bulletproofs would require the prover to know all values and blinding factors for every output in the transaction. This is fundamentally incompatible with atomic swaps and multi-party transactions where each party independently constructs their outputs. The MPC workaround from Bünz et al. §4.3 is not implemented in `secp256k1-zkp` v0.11. Separate proofs also avoid permanently linking all outputs as created by the same party, and allow UTXO pruning. Performance is recovered through batch verification. + +## Why secp256k1-zkp (not custom crypto)? + +`secp256k1-zkp` is the industry-standard library for Confidential Transactions, maintained by Blockstream and used in production by Liquid/Elements. It provides battle-tested implementations of Pedersen commitments, Bulletproofs, and surjection proofs on the same secp256k1 curve Hathor already uses. Writing custom cryptographic primitives would be a security liability. + +## Why ECDH rewind (not encrypted messages)? + +Range proof rewinding is an established technique (used by Monero, Grin, Elements) that embeds recovery data inside the proof itself, adding zero bytes to the transaction. The alternative — encrypting amount/blinding data in a separate field — would increase transaction size and add a new encryption scheme to audit. Rewinding also provides a natural authentication mechanism: only the correct ECDH shared secret produces valid rewind results. + +## Alternative: ZK-SNARKs (Zcash model) + +Zcash's Sapling/Orchard circuits use Groth16/Halo 2 proofs to hide amounts, token types, and the sender simultaneously. While more powerful (combining Phases B and C), this approach requires: +- Trusted setup (Groth16) or substantially larger proofs (Halo 2 ~1.8KB vs Bulletproof ~675B) +- A different curve (BLS12-381 or Pallas/Vesta), incompatible with Hathor's secp256k1 +- Complex circuit design and auditing +- Significantly longer proof generation time (~25-274ms vs ~2ms for Bulletproofs) + +Hathor's phased approach achieves the same end-state with individually simpler, more auditable components. + +## Alternative: Mimblewimble + +Mimblewimble (used by Grin, Beam, Litecoin MWEB) provides amount hiding with transaction cut-through (pruning intermediate transactions). However: +- Mimblewimble requires interactive transaction construction (sender and receiver must communicate to build the transaction) +- It fundamentally changes the UTXO model and transaction structure +- It cannot support scripting or multi-asset transactions in their current form +- Hathor's DAG structure is incompatible with Mimblewimble's linear chain assumptions + +## Impact of not doing this + +Without shielded outputs, all Hathor transactions remain fully transparent. Users who need amount or token privacy would have no on-chain option, limiting Hathor's utility for business payments, DeFi, and personal finance. Competitors with privacy features (Monero, Zcash, Litecoin MWEB, Liquid) would have a structural advantage for privacy-sensitive use cases. + +# Prior art +[prior-art]: #prior-art + +## Liquid / Elements Confidential Assets + +The primary inspiration for this design. Liquid (Blockstream's Bitcoin sidechain) implements Confidential Transactions with Pedersen commitments, Bulletproofs, and asset surjection proofs on secp256k1. Hathor's implementation uses the same `secp256k1-zkp` library and the same cryptographic constructions. + +**Lessons learned:** +- Separate Bulletproofs per output are the practical choice (Liquid uses this). +- Asset surjection proofs are essential for multi-asset chains. +- ECDH-based range proof rewind works well for recovery. +- The two-output-type approach (amount-only vs. full) is a Hathor innovation not present in Liquid. + +## Monero (RingCT + Bulletproofs) + +Monero uses Pedersen commitments and Bulletproofs for amount hiding, combined with ring signatures for sender privacy. All transactions are mandatory CT. + +**Differences from Hathor:** +- Monero uses Ed25519, not secp256k1. +- Monero's CT is mandatory; Hathor's is opt-in per output. +- Monero combines amount hiding with sender privacy (ring signatures) in a single protocol; Hathor separates these into independent phases. +- Monero does not support custom tokens. + +**Lessons learned:** +- Bulletproof range proofs are production-proven at scale (Monero processes millions of CT transactions). +- ECDH-based recovery with deterministic nonces is the standard approach. +- Mandatory CT provides larger anonymity sets but increases chain size. + +## Zcash (Sapling / Orchard) + +Zcash uses ZK-SNARKs (Groth16 in Sapling, Halo 2 in Orchard) to provide full transaction privacy: hidden amounts, hidden token types, and hidden senders, all in a single proof. + +**Differences from Hathor:** +- Zcash uses different curves (BLS12-381, Pallas/Vesta). +- Zcash's proofs are more expensive to generate (~25ms Groth16, ~274ms Halo 2 vs ~2ms Bulletproof). +- Zcash requires circuit compilation and trusted setup (Groth16) or larger proofs (Halo 2). +- Zcash's shielded pool is separate from its transparent pool (different address types); Hathor mixes them freely. + +**Lessons learned:** +- Opt-in privacy (Zcash's shielded pools) means most transactions remain transparent, reducing the effective anonymity set. Hathor should encourage shielded usage to grow the anonymity set. +- ZIP 317 fee structure (per-action fees) is a good model for incentive alignment. + +## Grin / Beam (Mimblewimble) + +Mimblewimble protocols use Pedersen commitments with transaction cut-through for amount hiding and chain compaction. + +**Not adopted because:** Interactive transaction construction, incompatibility with scripting and multi-asset support, and incompatibility with Hathor's DAG structure. + +## Litecoin MWEB + +Litecoin's MimbleWimble Extension Blocks (activated May 2022) add opt-in confidential transactions via a sidechain-like extension block. + +**Relevant parallels:** +- Opt-in privacy (like Hathor). +- Shield/unshield operations at the boundary between transparent and MWEB pools. +- ECDH-based stealth addresses for recipient privacy. + +**Differences:** MWEB is a separate block structure with different consensus rules; Hathor integrates shielded outputs directly into the existing transaction format via headers. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +1. **Shielded address format.** Option A (compact, 53-byte payload) vs Option B (full keys, 66-byte payload). The key tradeoff is address length vs. Silent Payments compatibility. This should be resolved before wallet implementations begin. + +2. **Final fee amounts.** The placeholder values (`FEE_PER_AMOUNT_SHIELDED_OUTPUT = 1`, `FEE_PER_FULL_SHIELDED_OUTPUT = 2`) are not final. Production values should be determined based on actual verification cost differentials, network economics, and desired incentive structure. + +3. **Batch verification optimization timeline.** The implementation currently verifies range proofs individually. Batch verification (`verify_multi`) would reduce CPU cost by an estimated 30-50% for multi-output transactions. The secp256k1-zkp API supports this, but the integration timeline is not yet determined. + +4. **Light client scanning support.** Full nodes must verify all proofs, but light clients need an efficient protocol for detecting their own shielded payments without downloading every transaction. Possible approaches include SP-style scanning filters, trusted server-side scanning, or compact proof summaries. + +5. **Shielded Nano Contract interactions.** How should Nano Contracts interact with shielded outputs? Can a contract consume shielded inputs or produce shielded outputs? What are the implications for contract state visibility? This is deferred to a future RFC. + +6. **Fee adjustment mechanism.** Should per-output fees be fixed consensus constants or adjustable via feature activation? Fixed constants are simpler but cannot adapt to changing verification costs or network conditions. + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Sender Privacy (Phase C) + +Ring signatures or nullifier-based protocols would hide which input is being spent, completing the privacy trifecta (hidden recipient + hidden amount/token + hidden sender). Shielded outputs simplify Phase C by making all outputs valid decoys regardless of their hidden amount or token. + +## Aggregated Bulletproofs for Multi-Party + +If a multi-party computation (MPC) protocol for aggregated Bulletproofs becomes available in `secp256k1-zkp`, transactions where all outputs are controlled by the same party (the common case for non-atomic-swap transactions) could use a single aggregated proof, reducing total proof size logarithmically. + +## Shielded Fee Amounts + +The fee amount is currently transparent. A future extension could allow the fee itself to be committed — the sender would prove (via range proof) that the committed fee exceeds the minimum, without revealing the exact amount. This would hide the number of shielded outputs (which is currently inferrable from the fee). + +## Cross-Chain Atomic Swaps with CT + +Shielded outputs on both sides of a cross-chain atomic swap would hide the amounts exchanged. Since each party independently balances their own blinding factors (see Section 4.1), no blinding factor exchange is needed. The atomic swap protocol only needs to coordinate the hash-time-lock, not the privacy layer. + +## View Key Delegation for Compliance + +Users could derive a "view key" that allows a designated party (auditor, regulator, exchange) to decrypt all shielded outputs addressed to them, without granting spending authority. This is analogous to Monero's view keys and could be implemented by sharing the scan private key. + +## Tiered Privacy Fees + +As the privacy stack matures, fees could be tiered by privacy level: transparent < amount-shielded < full-shielded < ring-shielded. This naturally extends the per-output-type fee model established in this RFC. From da691a64fcefaae8e28c014311180172d9577cf9 Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Mon, 27 Apr 2026 00:38:49 -0500 Subject: [PATCH 2/6] design: Shielded outputs mint/melt extension Extends the shielded outputs RFC with token creation, mint, and melt support via two new headers (MintHeader, MeltHeader) carrying public (token_index, amount) entries that bind into the homomorphic balance equation. Replaces parent Rule 8 and lifts the prohibition on shielded outputs in TokenCreationTransaction. Co-Authored-By: Claude Opus 4.7 (1M context) --- text/0000-shielded-outputs-mint-melt.md | 568 ++++++++++++++++++++++++ 1 file changed, 568 insertions(+) create mode 100644 text/0000-shielded-outputs-mint-melt.md diff --git a/text/0000-shielded-outputs-mint-melt.md b/text/0000-shielded-outputs-mint-melt.md new file mode 100644 index 0000000..e1c3da5 --- /dev/null +++ b/text/0000-shielded-outputs-mint-melt.md @@ -0,0 +1,568 @@ +- Feature Name: shielded_outputs_mint_melt +- Start Date: 2026-04-27 +- RFC PR: (leave this empty) +- Hathor Issue: (leave this empty) +- Author: Hathor Labs + +# Summary +[summary]: #summary + +This RFC extends [shielded outputs](./0000-shielded-outputs.md) to support **token +creation, mint, and melt** operations inside shielded transactions. It introduces two +new transaction headers — `MintHeader` and `MeltHeader` — that publicly declare the +per-token supply changes inside an otherwise-shielded transaction. The declared +amounts are bound into the homomorphic balance equation as public scalar terms, +allowing the verifier to enforce supply correctness without revealing where the +value lands. As a consequence, parent RFC Rule 8 ("Mint/Melt Transactions Cannot +Have Shielded Outputs") is replaced by the rules in this document, and +`TokenCreationTransaction` may now carry shielded outputs of the new token. + +# Motivation +[motivation]: #motivation + +The parent RFC delivers amount and token-type privacy for ordinary value transfers +but explicitly excludes mint and melt operations (parent Rule 8). Two real +problems follow: + +- **Privacy continuity.** A token issuer who routinely transacts in shielded form + must drop to fully-transparent mode to mint or melt, and then create a follow-up + shielding transaction. This leaks the timing and economic shape of every + issuance event, and breaks privacy continuity for the issuer's downstream + business flows (payroll, vendor payments). +- **Confidential issuance.** Many issuance use cases — pre-funding salaries, B2B + receivables, treasury operations — should reveal *the supply change* but not + *the recipient set or the per-recipient amount*. Today, every minted unit is + visible in plaintext outputs. + +Auditability is preserved because the mint/melt amounts remain public per token — +declared in the new headers — so total supply per token is still publicly +computable. What becomes private is *where the new tokens land* and *which inputs +are melted from*. + +**Expected outcome.** A shielded transaction can mint or melt any custom token +and create new tokens directly into shielded outputs, with the same auditability +guarantees as today's transparent mint/melt operations. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## 1. What changes + +A shielded transaction may now exercise mint or melt authority. The amounts are +declared publicly via two new headers: + +- **`MintHeader`** — list of `(token_index, amount)` entries declaring supply + *created* in this transaction. +- **`MeltHeader`** — list of `(token_index, amount)` entries declaring supply + *destroyed* in this transaction. + +Both are required only for shielded transactions. Transparent-only mint/melt +transactions (no shielded inputs and no shielded outputs) continue to use the +existing implicit amount-balance equation — no header needed. + +A given token may appear in at most one entry across both headers. + +## 2. Token creation can now be shielded + +Token creation transactions previously rejected shielded outputs outright. With +this RFC: + +- The new token's UID is still `tx.hash` and occupies `tokens[0]` (token_index 1). +- Outputs of the new token may be transparent **or** shielded. +- The total initial supply is declared via a single `MintHeader` entry for the + new token's index. The Pedersen balance equation reconciles transparent + + shielded outputs against the declared supply. +- Mint/melt authority outputs for the new token remain transparent (parent + RFC Rule 7). + +## 3. Examples + +### 3.1 Confidential mint + +An issuer holds a transparent mint authority for token T and wants to mint +100,000 T directly into a single shielded output for a salary recipient. + +``` +Inputs: + - mint authority of T (transparent) + - HTR (shielded, for deposit + fee) + +Outputs: + - mint authority of T (transparent, retained) + - shielded HTR change (shielded) + - shielded T output: 100,000 T (shielded) + +Headers: + - FeeHeader: standard fee + - ShieldedOutputsHeader: 2 shielded outputs + - MintHeader: [(token_index=1, amount=100000)] +``` + +Observers learn: *100,000 T were minted*. They do not learn the recipient or +that the issuer's HTR change is non-zero. + +### 3.2 Confidential token creation + +An issuer creates token T with initial supply 1,000,000, distributed across +4 shielded outputs. + +``` +Inputs: + - HTR (transparent or shielded), enough for 1% deposit + fee + +Outputs: + - 4 shielded T outputs (shielded) + - transparent HTR change (transparent) + +Headers: + - FeeHeader + - ShieldedOutputsHeader: 4 shielded outputs + - MintHeader: [(token_index=1, amount=1000000)] +``` + +The token UID is the tx hash; the supply (`1,000,000`) is publicly declared. +Per-recipient amounts are private. + +### 3.3 Confidential melt + +A treasurer melts 50,000 T (e.g., burning seized supply) without revealing +which UTXO was destroyed. + +``` +Inputs: + - melt authority of T (transparent) + - shielded T input (shielded) + +Outputs: + - melt authority of T (transparent, retained) + - shielded HTR withdraw + change (shielded) + +Headers: + - FeeHeader + - ShieldedOutputsHeader: 1 shielded output + - MeltHeader: [(token_index=1, amount=50000)] +``` + +The melted amount is public; the input commitment is opaque on-chain. + +## 4. What remains visible + +For every shielded mint/melt transaction, the following remains public: + +- **Per-token supply delta**: each `(token_index, amount)` in MintHeader/MeltHeader. +- **Authority spend events**: mint/melt authority inputs and outputs are always + transparent (parent Rule 7), so observers see *when* an authority is exercised. +- **HTR deposit/withdraw**: derived from the declared mint/melt amounts using + Hathor's existing 1% deposit rule for `DEPOSIT`-version tokens. +- **Transaction structure**: input/output counts, transparent vs. shielded + partition, and which outputs are authority outputs. + +What becomes private: + +- Recipient sets and per-recipient amounts of newly minted tokens. +- Which shielded UTXOs are consumed by a melt operation. +- The HTR-side change resulting from a deposit/withdraw, if shielded. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## 4.1 Header layout + +### MintHeader + +| Field | Size | Description | +|-------|------|-------------| +| Header ID | 1 B | `0x14` (`VertexHeaderId.MINT_HEADER`) | +| `num_entries` | 1 B | `1 ≤ num_entries ≤ 16` | +| Entries | variable | Concatenated entries | + +Each entry: + +| Field | Size | Description | +|-------|------|-------------| +| `token_index` | 1 B | `1 ≤ token_index ≤ len(tx.tokens)` (HTR/index 0 forbidden) | +| `amount` | 8 B BE | Public mint amount, `amount ≥ 1` | + +Constraints: + +- All `token_index` values within a header are distinct. +- All `amount` values are `≥ 1`. Zero entries are forbidden (zero entries leak no + information and only inflate header size). +- `token_index = 0` (HTR) is forbidden — HTR is never minted by user + transactions. + +### MeltHeader + +Wire format and constraints identical to `MintHeader`, with header ID `0x15` +(`VertexHeaderId.MELT_HEADER`). `token_index = 0` is similarly forbidden. + +### Header order + +Canonical ordering is ascending `VertexHeaderId`: + +``` +0x10 NANO_HEADER +0x11 FEE_HEADER +0x12 SHIELDED_OUTPUTS_HEADER +0x13 UNSHIELD_BALANCE_HEADER +0x14 MINT_HEADER +0x15 MELT_HEADER +``` + +`MintHeader` precedes `MeltHeader` when both are present. + +### Maximum number of headers + +`get_maximum_number_of_headers()` is raised from 3 to **at least 5**, to allow +`FeeHeader + (ShieldedOutputsHeader | UnshieldBalanceHeader) + MintHeader + +MeltHeader` plus a margin for future additions. The exact value is a consensus +parameter and is gated by feature activation. + +### Sighash coverage + +The full serialization of `MintHeader` and `MeltHeader` (header_id + count + +entries) is included in the transaction sighash. Mutating any entry invalidates +all signatures over the transaction. This is required because the declared +amounts directly affect the verified balance equation. + +## 4.2 Verification rules + +Six new rules govern shielded mint/melt. All rules from the parent RFC +(Rules 1–7) continue to apply; **Rule 8 is replaced by Rules M1–M6 below**. + +### Rule M1: Headers are valid only on shielded transactions + +If `MintHeader` or `MeltHeader` is present, the transaction MUST be shielded +(`tx.is_shielded()`). Presence on a non-shielded transaction is rejected with +`HeaderNotSupported`. + +### Rule M2: Mint/melt authority required for each entry + +For each `(token_index, amount)` in `MintHeader`, the transaction MUST consume +at least one mint authority input for `tx.tokens[token_index − 1]`. + +Symmetric for `MeltHeader` and melt authority. Authority inputs and outputs +remain transparent (parent Rule 7). + +### Rule M3: No cross-token offsetting + +A given token MUST NOT appear in both `MintHeader` and `MeltHeader` of the +same transaction. Such offsetting is meaningless (the same net effect is +achieved by adjusting the amounts) and would complicate supply accounting. + +### Rule M4: Augmented homomorphic balance + +The shielded balance equation (parent §4.4) is augmented: + +``` +sum(C_in) + sum_T(mint_T · H_T) == + sum(C_out) + sum_T(melt_T · H_T) + sum(C_fee_entries) + + deposit · H_HTR − withdraw · H_HTR +``` + +Where: + +- For each `MintHeader` entry `(T, amount)`: add `amount · H_T` to the input + side. The minted amount has no associated blinding factor (it appears + unblinded in the balance equation). +- For each `MeltHeader` entry `(T, amount)`: add `amount · H_T` to the output + side, also unblinded. +- `H_T = derive_asset_tag(token_uid_T)`. +- `deposit = Σ get_deposit_token_deposit_amount(amount)` over `MintHeader` + entries whose token is `DEPOSIT`-version. +- `withdraw = Σ get_deposit_token_withdraw_amount(amount)` over `MeltHeader` + entries whose token is `DEPOSIT`-version. + +The `deposit` and `withdraw` terms move HTR through the equation in the same +way the existing transparent verifier handles them +(`_check_token_permissions_and_deposits`), but now sourced from the public +header amounts instead of inferred from transparent value flows. + +`FEE`-version tokens contribute neither deposit nor withdraw (matching current +behavior). + +### Rule M5: Trivial commitment protection still applies + +Parent RFC Rule 4 (≥ 2 shielded outputs when all inputs are transparent, or +include a transparent output) is **not relaxed** by the presence of a +`MintHeader`. The minted amount enters the balance equation unblinded, so it +provides no entropy that would otherwise mask a single shielded output. + +### Rule M6: FeeHeader still required + +A shielded mint/melt transaction MUST carry a `FeeHeader` (parent Rule 2). +Deposit and withdraw amounts derived from `MintHeader`/`MeltHeader` are NOT +declared in the FeeHeader — they are folded into the balance equation +separately as Rule M4 specifies. + +## 4.3 Surjection-domain extension + +`FullShieldedOutput` requires a surjection proof showing its asset commitment +corresponds to one of the input domain generators (parent §4.1). For minted +tokens, no input contributes that asset, which would otherwise prevent +`FullShieldedOutput` of a freshly-minted token. + +**Extension.** For each `(T, amount)` entry in `MintHeader`, the unblinded +NUMS asset tag `H_T = derive_asset_tag(token_uid_T)` is added to the surjection +proof domain. This permits a `FullShieldedOutput` of a minted token to prove +its asset is one of the (transparent inputs ∪ shielded inputs ∪ minted tokens). + +`MeltHeader` does NOT extend the surjection domain, because melt produces no +new outputs of the melted token. + +## 4.4 Token creation transactions + +`TokenCreationTransaction` (TCT) gains the ability to carry shielded outputs. +The existing rejection +`InvalidShieldedOutputError('shielded outputs are not allowed in +TokenCreationTransaction')` is removed. + +**TCT-specific rules.** + +1. The new token UID is `tx.hash` and occupies `tokens[0]` (token_index 1) per + existing semantics. +2. If the TCT is shielded (carries `ShieldedOutputsHeader`), it MUST carry a + `MintHeader` whose entries include exactly one entry for `token_index = 1`, + with `amount > 0`. +3. The single-entry-for-new-token rule replaces the existing + `verify_minted_tokens` check (`token_info.amount > 0`) for shielded TCTs. + Non-shielded TCTs retain the existing check. +4. The new token's `H_T` is added to the surjection-proof domain (per + §4.3), enabling `FullShieldedOutput` of the new token. +5. Authority outputs for the new token remain transparent (parent Rule 7). + +## 4.5 Verification pipeline integration + +### Phase 1 (without storage) + +- `verify_headers` (`vertex_verifier.py`): allow `MintHeader` and `MeltHeader` + on `REGULAR_TRANSACTION` and `TOKEN_CREATION_TRANSACTION` when the + `shielded_transactions` feature is active. Canonical-ordering check still + applies. +- New `verify_mint_melt_headers_well_formed`: + - Both headers, if present, are non-empty. + - All `token_index` values within a header are unique. + - No token appears in both headers (Rule M3). + - All `token_index` values are in `[1, len(tx.tokens)]`. + - All `amount` values are ≥ 1. +- New `verify_mint_melt_requires_shielded` (Rule M1). + +### Phase 2 (with storage) + +- New `verify_mint_melt_authority_inputs` (Rule M2): for each `MintHeader` + entry, walk `tx.inputs`; at least one mint authority input must reference + `tx.tokens[token_index − 1]`. Symmetric for melt. +- Updated `verify_shielded_balance` (Rule M4): incorporate the + `MintHeader`/`MeltHeader` terms in the homomorphic balance call. +- Updated `_check_token_permissions_and_deposits` (shielded path): compute + `deposit`/`withdraw` from `MintHeader`/`MeltHeader` entries instead of + skipping them; fold into the HTR balance equation. +- Updated `verify_surjection_proofs` (§4.3): augment the domain with + `derive_asset_tag(tokens[token_index − 1])` for each `MintHeader` entry. +- Removed: `verify_no_mint_melt`. Replaced by Rules M1–M2 + M4. +- Removed: TCT-specific blanket block on shielded outputs. + +## 4.6 Wire format example + +``` +MintHeader serialization: + header_id(1B=0x14) | num_entries(1B) | + entry_0: token_index(1B) | amount(8B BE) | + entry_1: token_index(1B) | amount(8B BE) | ... + +Single-entry MintHeader for 100,000 of token at index 1: + 0x14 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x01 0x86 0xA0 +``` + +`MeltHeader` is identical with `header_id = 0x15`. + +## 4.7 Feature activation + +The headers are gated by a sub-feature on top of the parent RFC's flag: + +```python +class Feature(StrEnum): + SHIELDED_TRANSACTIONS = 'SHIELDED_TRANSACTIONS' # parent RFC + SHIELDED_MINT_MELT = 'SHIELDED_MINT_MELT' # this RFC +``` + +```python +ENABLE_SHIELDED_MINT_MELT: FeatureSetting = FeatureSetting.DISABLED +``` + +`SHIELDED_MINT_MELT` requires `SHIELDED_TRANSACTIONS` to be active (validated at +startup). When `SHIELDED_MINT_MELT` is disabled, parent RFC Rule 8 remains in +force and `MintHeader`/`MeltHeader` are rejected. + +This phasing lets the parent RFC ship and stabilize before mint/melt support is +enabled, minimizing the blast radius if a verification bug is found. + +## 4.8 Indexer and explorer impact + +- **Token supply tracking** is fully restored: an indexer reconciles per-token + supply by reading `MintHeader`/`MeltHeader` entries on every shielded tx in + addition to existing transparent mint/melt detection. +- **Per-tx mint/melt event** is observable by anyone: `(token, amount, + "mint" | "melt")` is published as plain header data. +- The "skip shielded inputs in rocksdb tokens index" pattern (commit `316e68b7`) + extends naturally — shielded outputs of minted tokens are still skipped in + per-UTXO indexing because their amount is hidden, but supply totals are + derived from the headers. + +# Drawbacks +[drawbacks]: #drawbacks + +1. **Surface area growth.** Two new headers, new verifier paths, and a + relaxation of parent RFC Rule 8. Each is security-sensitive. +2. **Header limit pressure.** Bumping `get_maximum_number_of_headers()` from 3 + to ≥ 5 is a consensus parameter change that must be carefully gated. +3. **Subtle balance equation.** Rule M4 changes the homomorphic balance + equation. A bug here could enable inflation. Cross-checks against the + existing transparent-deposit arithmetic are mandatory. +4. **Synthetic surjection-domain entries.** Adding minted-token asset tags to + the surjection domain widens the anonymity set in a way the underlying + library should already support, but it is a new code path that must be + exercised in tests against `secp256k1-zkp`. +5. **Incomplete privacy.** Mint/melt amounts remain public per token. Issuers + seeking *total* privacy of issuance are not served by this RFC. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## Why declare amounts publicly? + +Pedersen commitments hide amounts but cannot, on their own, distinguish +"minted from nothing" from "received from an input of equal amount". Without a +public scalar declaring the mint, the prover could mint arbitrary amounts. The +public declaration binds the mint to a scalar that enters the balance equation, +preserving the Pedersen "no inflation" guarantee. + +A purely private alternative — proving "I have authority to mint up to X and +am minting Y ≤ X" via a ZK proof — is an order of magnitude more complex +(circuits, trusted setup or larger proofs, new cryptographic dependencies) and +loses the auditability property. Public declaration is the lightest mechanism +that delivers the desired privacy improvement (recipients/inputs hidden) while +keeping supply auditable. + +## Why a single MintHeader with a list (not multiple instances)? + +The existing vertex header model enforces "at most one of each header type" +(`vertex_verifier.py:273-284`). Allowing multiple `MintHeader` instances would +require a special case. A single header carrying a list: + +- Preserves the existing invariant. +- Enforces uniqueness across entries cheaply. +- Keeps the binary format compact. + +## Why prohibit cross-token offsetting (Rule M3)? + +Allowing the same token in both `MintHeader` and `MeltHeader` is meaningless: +the same net effect is achieved by adjusting amounts. Permitting it doubles +the verification surface for no benefit. + +## Why not extend FeeHeader? + +`FeeHeader` semantics are "transparent additions to the output side of the +balance equation, burned". Mint amounts are inputs (added to input side); melt +amounts are outputs but with deposit/withdraw side effects on HTR. Conflating +these into FeeHeader would obscure the equation and require the FeeHeader to +carry sign information. + +## Why not a single combined `MintMeltHeader`? + +Mint and melt have opposite signs in the balance equation and asymmetric +surjection-domain effects (mint extends the domain; melt does not). Separate +headers make the binary format and verifier state clearer, at the cost of one +extra header ID. + +## Why not a MAC instead of a public amount? + +A MAC of "this tx mints N of T" generated by the mint authority key adds a new +cryptographic dependency without strong benefit: the existing transaction +signature already authenticates the spend of the authority input. The amount +itself is what enters the balance equation. + +## Impact of not doing this + +Mint/melt operations remain forever transparent. Shielded users who need to +mint or melt drop to plaintext mode and create follow-up shielding transactions, +leaking timing and economic shape. Issuers seeking confidential issuance have +no on-chain option. + +# Prior art +[prior-art]: #prior-art + +## Liquid / Elements: Confidential Assets and Issuance + +Elements supports confidential asset issuance in a single transaction. The +issuance carries a public asset_id and amount, very similar in spirit to +`MintHeader`. Elements additionally supports *blinded issuance*, the privacy +upgrade analogous to a future ZK-based version of this RFC. + +## Zcash Sapling: value-balance field + +Sapling tracks net flows between transparent and shielded pools via a +`valueBalance` field on the transaction. This is the closest analog to +declaring a public per-asset delta on an otherwise-shielded transaction. + +## Monero + +Monero has no token concept and therefore no direct analog for token mint/melt. +However, Monero's `RingCT` design demonstrates the soundness of using public +scalar amounts (in coinbase emission) inside a Pedersen-commitment balance +equation. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +1. **Sub-feature flag vs single flag.** Should `SHIELDED_MINT_MELT` be a + distinct feature flag (allowing the parent RFC to ship first), or be folded + into `SHIELDED_TRANSACTIONS` and shipped together? +2. **`get_maximum_number_of_headers()` value.** 5 is the minimum; 6 leaves + margin for future headers but expands the consensus parameter further. Final + value to be determined. +3. **`NanoHeader` interaction.** May a transaction simultaneously carry a + `NanoHeader` and be a shielded mint/melt? Phase-1 simplification: forbid + the combination; reconsider when shielded Nano Contracts are designed + (parent RFC §4.8). +4. **Per-entry amount caps.** Should `amount` be bounded below `2^64` (e.g., + `2^53` for safe JSON serialization in clients)? Current parent RFC range + proofs already cover `[1, 2^64)`; aligning is the simplest choice. +5. **Authority-output presence requirement.** Rule M2 requires an authority + *input* but does not require the transaction to produce a corresponding + authority *output*. Should we additionally require authority retention by + default to prevent accidental authority burn? (Current Hathor behavior + permits intentional authority destruction, so no change recommended.) +6. **Shielded UnshieldBalanceHeader interaction.** A full-unshield transaction + that also mints (e.g., the issuer mints into a transparent recipient) + carries `UnshieldBalanceHeader + MintHeader`. The combined balance equation + is well-defined but warrants explicit test coverage. + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Blinded mint amounts + +Extend `MintHeader` to optionally carry a Pedersen commitment instead of a +plaintext amount, with a range proof and a "mint quota proof" demonstrating +the mint stays within an authority-bound limit. Requires a quota mechanism +attached to mint authorities. + +## Mint to specific recipients + +A future header could bind a mint event to a specific destination, useful for +compliance-aware tokens (e.g., regulated stablecoins where issuance must be +attributable to a registered counterparty). + +## Cross-token atomic batching + +Multi-token support in `MintHeader` already allows atomic multi-token +issuance. A future RFC could formalize a "token bundle" semantic on top of +this primitive (e.g., simultaneously minting a governance token and its +matching reward token). + +## Confidential authority provenance + +Combine with Phase C (input unlinkability): the mint authority input itself +could be obscured via ring signatures, hiding *which authority UTXO* was +exercised even when the authority is owned across multiple keys. From 9e9006b72208c1ac0831a37e516e021a4491b3e3 Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Mon, 27 Apr 2026 00:53:50 -0500 Subject: [PATCH 3/6] =?UTF-8?q?design:=20shielded=20mint/melt=20=E2=80=94?= =?UTF-8?q?=20disclosure=20model=20and=20version=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Disclosure model subsection clarifying that the token reference is always transparent (authority outputs already leak it; the verifier needs the version for deposit/fee logic) while amount visibility is a pending business decision. Adds worked examples for DEPOSIT-version mint, melt and FEE-version mint with concrete deposit/fee math. Reframes Rule M3 as "one direction per token" and elevates the transparent-vs-shielded amount trade-off into Unresolved Questions. Co-Authored-By: Claude Opus 4.7 (1M context) --- text/0000-shielded-outputs-mint-melt.md | 179 ++++++++++++++++++++++-- 1 file changed, 171 insertions(+), 8 deletions(-) diff --git a/text/0000-shielded-outputs-mint-melt.md b/text/0000-shielded-outputs-mint-melt.md index e1c3da5..81fb91d 100644 --- a/text/0000-shielded-outputs-mint-melt.md +++ b/text/0000-shielded-outputs-mint-melt.md @@ -60,7 +60,12 @@ Both are required only for shielded transactions. Transparent-only mint/melt transactions (no shielded inputs and no shielded outputs) continue to use the existing implicit amount-balance equation — no header needed. -A given token may appear in at most one entry across both headers. +A given token may appear in **either** `MintHeader` **or** `MeltHeader` in a +single transaction, never both (see Rule M3). + +Concrete worked examples for `DEPOSIT`-version tokens (with HTR deposit/withdraw +math) appear in §3.4 and §3.6; an example for `FEE`-version tokens (with +per-output fee math) appears in §3.5. ## 2. Token creation can now be shielded @@ -145,6 +150,113 @@ Headers: The melted amount is public; the input commitment is opaque on-chain. +### 3.4 DEPOSIT-version token: mint with HTR deposit + +Token `TD` is `DEPOSIT`-version. The issuer mints 100,000 TD across two +shielded outputs. The Hathor 1% deposit rule applies: minting 100,000 TD costs +1,000 HTR. Assume the standard per-output fee is 1 HTR and shielded outputs +cost 1 HTR each (`FEE_PER_AMOUNT_SHIELDED_OUTPUT`). + +``` +Inputs: + - mint authority of TD (transparent) + - HTR (transparent), 1,002 HTR available + +Outputs: + - mint authority of TD (transparent, retained) + - 2 shielded TD outputs (totals 100,000 TD) + +Headers: + - FeeHeader: 2 HTR fee (2 × FEE_PER_AMOUNT_SHIELDED_OUTPUT) + - ShieldedOutputsHeader: 2 shielded outputs + - MintHeader: [(token_index=1, amount=100000)] + +Verifier: + - deposit = 0.01 × 100,000 = 1,000 HTR (derived from MintHeader, applied + as a public output term on H_HTR). + - Augmented balance: + sum(C_in) + 100,000·H_TD + == sum(C_out) + 1,000·H_HTR + 2·H_HTR + - HTR side reduces to: 1,002·H_HTR (input) == 1,000·H_HTR (deposit) + 2·H_HTR (fee). + - TD side: 0·H_TD (no input of TD) + 100,000·H_TD == sum(shielded TD commitments). +``` + +What observers learn: 100,000 TD were minted; 1,000 HTR were burned for +deposit; 2 HTR fee was paid. They do not learn how the 100,000 TD were split +between the two shielded outputs or who the recipients are. + +### 3.5 FEE-version token: mint paying per-output fees + +Token `TF` is `FEE`-version. The issuer mints 50,000 TF into one transparent +output of 30,000 TF (paid to a public counterparty) plus one shielded output +of 20,000 TF (treasury). `FEE`-version tokens have no HTR deposit; per-output +fees apply instead. + +``` +Inputs: + - mint authority of TF (transparent) + - HTR (shielded), enough for fees + +Outputs: + - mint authority of TF (transparent, retained) + - transparent TF output: 30,000 TF (chargeable) + - shielded TF output: 20,000 TF + - shielded HTR change + +Headers: + - FeeHeader: + 1 × FEE_PER_OUTPUT (one chargeable transparent TF output) + + 2 × FEE_PER_AMOUNT_SHIELDED_OUTPUT (two shielded outputs) + - ShieldedOutputsHeader: 2 shielded outputs + - MintHeader: [(token_index=1, amount=50000)] + +Verifier: + - No deposit/withdraw on HTR (FEE-version tokens skip the 1% rule). + - Augmented balance: + sum(C_in) + 50,000·H_TF + == sum(C_out) + total_fee·H_HTR + where the TF side balances 50,000 newly-minted units against + 30,000 (transparent) + 20,000 (shielded committed). + - Total fee in FeeHeader must match exactly: + FEE_PER_OUTPUT × 1 + FEE_PER_AMOUNT_SHIELDED_OUTPUT × 2. +``` + +What observers learn: 50,000 TF were minted; 30,000 TF went to a publicly +visible transparent output; the remaining 20,000 TF entered the shielded +pool. They do not learn the recipient of the shielded output or the issuer's +HTR change. + +### 3.6 DEPOSIT-version token: melt with HTR withdraw + +Token `TD` is `DEPOSIT`-version. The treasurer melts 80,000 TD; this releases +800 HTR back from the deposit pool to the spender (1% withdraw rule). + +``` +Inputs: + - melt authority of TD (transparent) + - shielded TD input (shielded, holds at least 80,000 TD plus optional change) + +Outputs: + - melt authority of TD (transparent, retained) + - shielded HTR output (carries the 800 HTR withdraw + any change) + - optional shielded TD change (if input held more than 80,000) + +Headers: + - FeeHeader: shielded fees only + - ShieldedOutputsHeader + - MeltHeader: [(token_index=1, amount=80000)] + +Verifier: + - withdraw = 0.01 × 80,000 = 800 HTR. + - Augmented balance: + sum(C_in) + 800·H_HTR + == sum(C_out) + 80,000·H_TD + fee·H_HTR + - TD side: input commitment holds X TD; output side holds (X − 80,000) TD + (in optional change) plus 80,000·H_TD as a public melt term. + - HTR side: 800·H_HTR (input from withdraw) is balanced by the recipient's + shielded HTR output(s). +``` + ## 4. What remains visible For every shielded mint/melt transaction, the following remains public: @@ -225,6 +337,43 @@ entries) is included in the transaction sighash. Mutating any entry invalidates all signatures over the transaction. This is required because the declared amounts directly affect the verified balance equation. +### Disclosure model + +Each entry in `MintHeader` or `MeltHeader` discloses two things: the **token +reference** (`token_index`) and the **amount**. Their visibility differs. + +**Token reference: always transparent.** The token reference is plaintext for +two reasons that together rule out hiding it: + +1. The mint or melt authority output is itself transparent (parent Rule 7) and + exposes the token UID via its `token_data`. Hiding the token in the header + would leak nothing additional but provide no privacy benefit. +2. The verifier must read the token's version (`NATIVE` / `DEPOSIT` / `FEE`) to + apply the correct deposit-or-fee logic. A hidden token would block this + lookup, requiring an unbounded ZK proof of "I am applying the right rule for + the right version". + +**Amount: transparent in this RFC; shielded amounts are a pending business +decision.** The headers in this RFC carry plaintext `u64` amounts. The +trade-off is auditability vs. privacy: + +- *Transparent amounts (this RFC's choice).* Total token supply is publicly + computable per token by summing `MintHeader` and `MeltHeader` entries across + the chain. Compatible with light-client supply auditors. **Required** for + `DEPOSIT`-version tokens because the HTR deposit is `0.01 × amount` and must + be verifiable against the public amount. +- *Shielded amounts (alternative, deferred).* The header carries a Pedersen + commitment to the amount plus a range proof. Total supply becomes opaque on + chain. Feasible for `FEE`-version tokens (fees are per-output, not + per-amount). For `DEPOSIT`-version tokens this requires an additional ZK + proof binding the HTR deposit to 1% of the committed amount, which is + substantially more machinery than the rest of this RFC. + +Because the choice has direct consequences for auditability — and because the +two token versions admit different complexity for shielded amounts — the RFC +treats this as a pending business decision (see [Unresolved +Questions](#unresolved-questions)). + ## 4.2 Verification rules Six new rules govern shielded mint/melt. All rules from the parent RFC @@ -244,11 +393,16 @@ at least one mint authority input for `tx.tokens[token_index − 1]`. Symmetric for `MeltHeader` and melt authority. Authority inputs and outputs remain transparent (parent Rule 7). -### Rule M3: No cross-token offsetting +### Rule M3: One direction per token + +For any given token, a single transaction may declare **either** a mint +**or** a melt, never both. Concretely: a `token_index` that appears in +`MintHeader` MUST NOT appear in `MeltHeader`, and vice versa. -A given token MUST NOT appear in both `MintHeader` and `MeltHeader` of the -same transaction. Such offsetting is meaningless (the same net effect is -achieved by adjusting the amounts) and would complicate supply accounting. +Self-offsetting (mint X and melt Y of the same token in one tx) is meaningless +because the same net supply effect is achievable by adjusting amounts. +Permitting it would also complicate per-token supply accounting on the +indexer side without delivering any user-visible capability. ### Rule M4: Augmented homomorphic balance @@ -525,9 +679,18 @@ equation. `NanoHeader` and be a shielded mint/melt? Phase-1 simplification: forbid the combination; reconsider when shielded Nano Contracts are designed (parent RFC §4.8). -4. **Per-entry amount caps.** Should `amount` be bounded below `2^64` (e.g., - `2^53` for safe JSON serialization in clients)? Current parent RFC range - proofs already cover `[1, 2^64)`; aligning is the simplest choice. +4. **Transparent vs. shielded mint/melt amounts.** This RFC declares amounts + as plaintext `u64` (see [Disclosure model](#disclosure-model)). The + alternative — Pedersen commitments with range proofs — would hide token + supply entirely from public observers, at the cost of: (a) substantial + extra ZK machinery for `DEPOSIT`-version tokens (a proof that the HTR + deposit equals 1% of the committed amount); (b) loss of the public + "supply auditor" property that lets any node compute total supply per + token; (c) divergent behavior between `DEPOSIT`- and `FEE`-version tokens + (the latter is straightforward to shield since fees are per-output). The + choice is a business decision about how much auditability to trade away. + This RFC's plaintext design can be retrofitted later via a new feature + flag without invalidating already-issued tokens. 5. **Authority-output presence requirement.** Rule M2 requires an authority *input* but does not require the transaction to produce a corresponding authority *output*. Should we additionally require authority retention by From 75fd403b726494759541745179de9693a0536975 Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Mon, 27 Apr 2026 13:30:22 -0500 Subject: [PATCH 4/6] design(shielded): FEE-version mint/melt fee policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Charge one FEE_PER_OUTPUT per MintHeader/MeltHeader entry on a FEE-version token, folded into the augmented homomorphic balance equation directly (mirroring the DEPOSIT 1% mechanism). The charge is per header entry, not per shielded recipient, so it preserves the FEE-token-specific knob without leaking the recipient count. Update Rule M4 to add the fee_token_charge term and Rule M6 to clarify that per-entry FEE-token charges go through the balance equation, not FeeHeader. Adjust §3.5 to reflect the split between FeeHeader (transparent + shielded outputs) and the augmented balance equation (per-entry FEE-token charge). Refine the disclosure-model drawback note to describe the chosen policy. --- text/0000-shielded-outputs-mint-melt.md | 58 +++++++++++++++++++------ 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/text/0000-shielded-outputs-mint-melt.md b/text/0000-shielded-outputs-mint-melt.md index 81fb91d..dcb0bcf 100644 --- a/text/0000-shielded-outputs-mint-melt.md +++ b/text/0000-shielded-outputs-mint-melt.md @@ -190,7 +190,8 @@ between the two shielded outputs or who the recipients are. Token `TF` is `FEE`-version. The issuer mints 50,000 TF into one transparent output of 30,000 TF (paid to a public counterparty) plus one shielded output of 20,000 TF (treasury). `FEE`-version tokens have no HTR deposit; per-output -fees apply instead. +fees apply instead, plus one extra `FEE_PER_OUTPUT` per `MintHeader` / +`MeltHeader` entry on a `FEE`-version token (see Rule M4). ``` Inputs: @@ -211,20 +212,33 @@ Headers: - MintHeader: [(token_index=1, amount=50000)] Verifier: - - No deposit/withdraw on HTR (FEE-version tokens skip the 1% rule). + - No HTR deposit/withdraw on the 1% rule (FEE-version tokens skip it). + - Each MintHeader / MeltHeader entry on a FEE-version token contributes + one FEE_PER_OUTPUT charge to the output side of the augmented balance + equation (paid by the user from HTR transparent inputs). Like the 1% + deposit for DEPOSIT-version tokens, this charge is folded into the + balance equation directly — it is NOT declared in FeeHeader, which + continues to cover only chargeable transparent outputs and shielded + outputs. - Augmented balance: sum(C_in) + 50,000·H_TF - == sum(C_out) + total_fee·H_HTR + == sum(C_out) + 1·FEE_PER_OUTPUT·H_HTR + total_fee·H_HTR where the TF side balances 50,000 newly-minted units against - 30,000 (transparent) + 20,000 (shielded committed). + 30,000 (transparent) + 20,000 (shielded committed); the + 1·FEE_PER_OUTPUT term reflects the MintHeader entry charge; and + total_fee is the FeeHeader sum. - Total fee in FeeHeader must match exactly: - FEE_PER_OUTPUT × 1 + FEE_PER_AMOUNT_SHIELDED_OUTPUT × 2. + FEE_PER_OUTPUT × 1 (transparent TF output) + + FEE_PER_AMOUNT_SHIELDED_OUTPUT × 2 (two shielded outputs). ``` What observers learn: 50,000 TF were minted; 30,000 TF went to a publicly visible transparent output; the remaining 20,000 TF entered the shielded pool. They do not learn the recipient of the shielded output or the issuer's -HTR change. +HTR change. The per-entry FEE_PER_OUTPUT charge is visible (it's part of the +public balance equation), but it leaks no per-recipient information — every +MintHeader entry on a FEE-version token pays exactly one FEE_PER_OUTPUT +regardless of how many shielded recipients the entry is split across. ### 3.6 DEPOSIT-version token: melt with HTR withdraw @@ -412,6 +426,7 @@ The shielded balance equation (parent §4.4) is augmented: sum(C_in) + sum_T(mint_T · H_T) == sum(C_out) + sum_T(melt_T · H_T) + sum(C_fee_entries) + deposit · H_HTR − withdraw · H_HTR + + fee_token_charge · H_HTR ``` Where: @@ -426,14 +441,26 @@ Where: entries whose token is `DEPOSIT`-version. - `withdraw = Σ get_deposit_token_withdraw_amount(amount)` over `MeltHeader` entries whose token is `DEPOSIT`-version. +- `fee_token_charge = FEE_PER_OUTPUT × N_FEE_entries`, where `N_FEE_entries` + is the count of `MintHeader` plus `MeltHeader` entries whose token is + `FEE`-version. This charge is always paid by the user (it lands on the + output side regardless of mint vs. melt). The `deposit` and `withdraw` terms move HTR through the equation in the same way the existing transparent verifier handles them (`_check_token_permissions_and_deposits`), but now sourced from the public header amounts instead of inferred from transparent value flows. -`FEE`-version tokens contribute neither deposit nor withdraw (matching current -behavior). +`FEE`-version tokens skip the 1% deposit/withdraw rule but pay +`FEE_PER_OUTPUT` per `MintHeader` / `MeltHeader` entry (the +`fee_token_charge` term above). This matches the spirit of the transparent +per-output rule — every declared mint/melt action of a `FEE`-version token +costs the issuer one chargeable-output-equivalent — without leaking +recipient counts: the charge is per header entry, not per shielded +recipient. An `AmountShieldedOutput` of a `FEE`-version token does not also +incur a per-shielded-recipient `FEE_PER_OUTPUT` charge; only the generic +`FEE_PER_AMOUNT_SHIELDED_OUTPUT` (and `FEE_PER_FULL_SHIELDED_OUTPUT` for +`FullShieldedOutput`) applies on top of the per-entry charge. ### Rule M5: Trivial commitment protection still applies @@ -447,7 +474,10 @@ provides no entropy that would otherwise mask a single shielded output. A shielded mint/melt transaction MUST carry a `FeeHeader` (parent Rule 2). Deposit and withdraw amounts derived from `MintHeader`/`MeltHeader` are NOT declared in the FeeHeader — they are folded into the balance equation -separately as Rule M4 specifies. +separately as Rule M4 specifies. The per-entry `FEE_PER_OUTPUT` charge for +`FEE`-version `MintHeader`/`MeltHeader` entries is also handled in the +balance equation (not in FeeHeader); FeeHeader continues to cover only +chargeable transparent outputs and shielded outputs. ## 4.3 Surjection-domain extension @@ -687,10 +717,12 @@ equation. deposit equals 1% of the committed amount); (b) loss of the public "supply auditor" property that lets any node compute total supply per token; (c) divergent behavior between `DEPOSIT`- and `FEE`-version tokens - (the latter is straightforward to shield since fees are per-output). The - choice is a business decision about how much auditability to trade away. - This RFC's plaintext design can be retrofitted later via a new feature - flag without invalidating already-issued tokens. + (this RFC charges `FEE`-version entries one `FEE_PER_OUTPUT` per + `MintHeader`/`MeltHeader` entry rather than per shielded recipient, so + the per-entry charge is preserved without leaking the recipient count). + The choice is a business decision about how much auditability to trade + away. This RFC's plaintext design can be retrofitted later via a new + feature flag without invalidating already-issued tokens. 5. **Authority-output presence requirement.** Rule M2 requires an authority *input* but does not require the transaction to produce a corresponding authority *output*. Should we additionally require authority retention by From 5f21ae0cabd570e085d45d3662b41dc8960f1092 Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Thu, 30 Apr 2026 11:44:55 -0500 Subject: [PATCH 5/6] design: shielded outputs supply audit (binding signature) Extends ShieldedOutputsHeader with a per-tx kernel-excess point and Schnorr binding signature, enabling network-wide supply auditability across the shielded pool. Removes the wallet-side "force e_tx = 0" construction in favor of independently random output blindings, unlocking receiver-chosen blindings and multi-party tx construction. Folds the role of UnshieldBalanceHeader into ShieldedOutputsHeader and retires the standalone header. Co-Authored-By: Claude Opus 4.7 (1M context) --- text/0000-shielded-outputs-audit.md | 576 ++++++++++++++++++++++++++++ 1 file changed, 576 insertions(+) create mode 100644 text/0000-shielded-outputs-audit.md diff --git a/text/0000-shielded-outputs-audit.md b/text/0000-shielded-outputs-audit.md new file mode 100644 index 0000000..74d9966 --- /dev/null +++ b/text/0000-shielded-outputs-audit.md @@ -0,0 +1,576 @@ +- Feature Name: shielded_outputs_audit +- Start Date: 2026-04-29 +- RFC PR: (leave this empty) +- Hathor Issue: (leave this empty) +- Author: Hathor Labs + +# Summary +[summary]: #summary + +This RFC extends [Shielded Outputs](0000-shielded-outputs.md) with a per-transaction **binding signature** that enables network-wide supply auditability. The existing `ShieldedOutputsHeader` is augmented with a 33-byte kernel-excess point `E_tx = e_tx · G` together with a 64-byte Schnorr signature proving knowledge of the discrete log. The signature simultaneously serves as a non-interactive proof of value-balance and binds the proof to the transaction. A chain-wide audit equation then permits any external observer to verify, in a single curve-equation per token, that no inflation has occurred across the entire transaction history. The construction is structurally identical to Zcash Sapling's binding signature, generalized to Hathor's per-token confidential transactions. As a side benefit, the change removes the "wallet forces `e_tx = 0`" pattern from the current shielded design, allowing every output's blinding factor to be independently uniformly random — unlocking receiver-chosen blindings, multi-party transaction construction, and composable output addition. + +# Motivation +[motivation]: #motivation + +The shielded outputs feature hides individual UTXO amounts behind Pedersen commitments. While per-transaction balance is enforced by every full node during verification, **chain-wide supply auditability is lost**: an external monitor can no longer sum cleartext UTXO amounts and compare against expected supply. + +This matters for two distinct populations: + +1. **Inflation-bug monitors.** A bug in any verifier path — range proof check, surjection proof check, balance equation, or any future addition — could allow a transaction to create value from nothing. Without a chain-wide invariant, such a bug remains silent until it is exploited and economic damage manifests. With a chain-wide audit equation, a single check against the UTXO set detects any past inflation regardless of which verifier path was buggy. + +2. **External auditors and explorers.** Token issuers, regulators, exchanges, and explorers need to verify total circulating supply per token. For tokens whose issuer chooses transparent mint/melt, supply is currently auditable from cleartext fields alone. But this depends on the issuer's good faith. A protocol-level audit equation makes supply verification independent of issuer choices for the entire shielded-pool history. + +The current shielded design supports per-tx balance via the equation `Σ C_in = Σ C_out + fee · H_HTR`, with the wallet constructing the last shielded output's blinding factor as the residual `r_n = Σ r_in − Σ r_others` so that the per-tx kernel excess is structurally zero. This per-tx zero-excess pattern has three consequences: + +- **No chain-wide telescoping.** The kernel excess is never published, so the standard CT/Mimblewimble chain-wide audit construction does not apply. +- **Within-tx blinding correlation.** If any `n−1` blindings of a transaction leak (RNG flaw, partial seed compromise, side channel), the last output's blinding is fully determined. +- **Sender must know all blindings.** The construction requires the sender to compute the residual blinding, which precludes Sapling-style receiver-derived blindings, multi-party transaction construction, and easy output addition. + +This RFC introduces a per-tx binding signature that simultaneously closes the audit gap and removes the construction constraints, at a cost of 98 bytes per shielded transaction. + +**Expected outcome.** Any external observer, given the entire chain history and the current UTXO set, can verify in a single curve-point equation per token that no value has been created or destroyed across the entire shielded-pool history. Wallet construction simplifies: every output's blinding factor is independently uniformly random. The change is a backward-incompatible extension to `ShieldedOutputsHeader` (gated by network upgrade) and removes the now-redundant `UnshieldBalanceHeader`. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## 1. The Audit Equation + +After this RFC, an external monitor (anyone holding the chain history and the current UTXO set) can verify total supply per token via: + +``` +Σ C_utxo == total_supply · H_token + Σ_tx E_tx +``` + +- `Σ C_utxo` — sum of all unspent value commitments. Transparent UTXOs are treated as commitments with `r = 0`, i.e., `C = v · H_token`. Shielded UTXOs use their on-chain commitment directly. +- `total_supply` — the publicly computable expected supply, derived from cleartext fields: `initial + Σ mint − Σ melt ± Σ shield/unshield deltas`. +- `Σ_tx E_tx` — the running sum of every shielded transaction's binding-signature public key (`E_tx = e_tx · G`). Maintained incrementally as a single 33-byte curve point. + +Both sides are points on secp256k1. The verifier computes both and checks equality. If any historical transaction inflated the supply (for any reason — verifier bug, broken range proof, miscomputed mint), the equation fails. + +The equation is the multi-asset generalization of Zcash Sapling's chain-wide value balance. The same machinery proves both per-tx balance (during verification) and chain-wide balance (during audit) — see [Section 4.4](#44-the-binding-signature-as-proof-of-balance). + +## 2. The Binding Signature + +Each shielded transaction's `ShieldedOutputsHeader` is extended with three trailing fields: + +| Field | Size | Purpose | +|---|---|---| +| `excess_point` | 33 B | Kernel excess `E_tx = e_tx · G` — public key commitment to the transaction's blinding-factor balance | +| `signature_R` | 33 B | Schnorr signature commitment `R = k · G` | +| `signature_z` | 32 B | Schnorr signature response `z = k + c · e_tx (mod q)` | + +Together these prove knowledge of `e_tx` such that `excess_point = e_tx · G`, bound to the transaction sighash. The verifier checks two things: + +``` +1. Σ C_out − Σ C_in − fee · H_HTR == E_tx (balance equation) +2. Schnorr.Verify(E_tx, sighash, signature) (binding signature) +``` + +The first check enforces homomorphic balance against the published `E_tx`. The second check proves that `E_tx` lies on the `G`-subgroup with a known discrete log — which forces value terms in the LHS to cancel exactly, since `G` and `H_token` are independent generators with no known DL relation. + +## 3. What Changes for Wallets + +The wallet no longer needs to compute the last output's blinding factor as a residual. Instead: + +``` +Old construction: + for i in 0..n-1: r_i ←$ Z_q + r_n := Σ r_in − Σ_{i= H_audit} E_tx + legacy_offset +``` + +where `legacy_offset = Σ r_pre_H_audit_utxos · G`, fixed at `H_audit` and computed once by summing the pre-fork UTXO commitments and subtracting `pre_fork_supply · H_token`. This single "snapshot offset" point is published as part of the network upgrade and lets the chain-wide equation remain valid across the transition. + +Wallets and full nodes MUST be updated before `H_audit`. Old wallets producing pre-RFC `ShieldedOutputsHeader` (without trailer) after `H_audit` will produce invalid transactions, rejected by full nodes for missing binding-signature fields. The deserializer distinguishes the two formats by network-upgrade context — there is no in-band version byte, since the header layout is determined by the activation height. + +# Drawbacks +[drawbacks]: #drawbacks + +- **98 byte per-tx wire cost.** Every shielded transaction grows by a fixed-size trailer in `ShieldedOutputsHeader`. For a transaction with two shielded outputs (~1.6 KB), this is ~6% overhead. Negligible per-tx, but accumulates over chain history. + +- **One additional cryptographic verification per transaction.** Schnorr verification is a single multi-exp on secp256k1 — fast, but not free. Batch verification across a block amortizes the cost. + +- **Increased complexity for wallet implementations.** Wallets must compute `e_tx`, derive the binding-signature key, sign with deterministic nonces, and serialize the extended `ShieldedOutputsHeader` trailer. The change is well-isolated (mechanical sign/verify, fixed-size fields) but is still an additional surface area. + +- **Single-output shield-in privacy footgun.** The structural amount leak when all inputs are transparent and there is one shielded output remains. This RFC documents the issue and recommends a wallet warning, but consensus does not enforce a multiple-shielded-output rule. A user who constructs such a transaction reveals the shielded value through the balance equation, regardless of the binding signature. + +- **No per-token isolation.** The chain-wide audit equation is a single check across all tokens jointly. A monitor cannot isolate HTR auditability from custom token correctness without either (a) constraining HTR to transparent operations or (b) adding per-asset binding signatures (loses asset privacy). This is discussed in [Future Possibilities](#future-possibilities). + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## Why publish `E_tx` as a curve point rather than the scalar `e_tx`? + +A scalar `e_tx` would also prevent inflation: the verifier checks `Σ C_out − Σ C_in − fee · H_HTR = e_tx · G`, computing `e_tx · G` from the published scalar; the RHS is structurally on the `G`-subgroup with known discrete log. No Schnorr signature would be needed for inflation prevention. Wire format: 32 B vs 33 + 64 = 97 B. + +The privacy cost of publishing the scalar is unacceptable: + +1. **Direct blinding-factor leak in the single-output corner case.** Where the point form leaks only `r_o · G` (recovering `r_o` requires solving DLP), the scalar form leaks `r_o` directly. Defense-in-depth lost. + +2. **`Σ r_utxo` becomes a publicly computable scalar.** Any single UTXO blinding leak via side channel composes with the chain-wide sum to reveal further blindings. Point form forces an attacker to solve DLP for each composition. + +3. **Receiver-chosen blinding is impossible.** Sapling-style receivers contribute `r_recipient · G` (a public point from their viewing key) without revealing `r_recipient`. Sender aggregation works on points, not on scalars. + +4. **No path to multi-party / non-interactive aggregation.** Combining contributions from multiple parties requires summing kernel excesses without revealing individual contributions. Point form supports this; scalar form does not. + +5. **Cross-tx statistical analysis** of published scalars could expose RNG biases. Points are uniform group elements with no exploitable structure. + +The 65 B saved are not worth losing defense-in-depth, receiver-chosen blindings, and aggregation friendliness. + +## Why a Schnorr signature rather than a different proof of DL knowledge? + +Schnorr is the standard proof of knowledge of discrete log, with three desirable properties: 64 B size, single multi-exp verification, batch-verifiable. Alternatives considered: + +- **ECDSA**: also 64 B, also batch-verifiable in some constructions, but more complex and lacks linearity (cannot trivially aggregate). Schnorr is preferable. +- **EdDSA / Ed25519**: requires a different curve. The shielded outputs design uses secp256k1 throughout; introducing a second curve is unwarranted complexity. +- **BLS signature**: smaller (48 B) and aggregatable, but requires pairing-friendly curves (BLS12-381) and pairings for verification — expensive and inconsistent with the rest of the shielded design. +- **Bulletproofs proof of knowledge**: overkill (and 600+ B). + +Schnorr over secp256k1 with deterministic nonces, `(R, z)` form for batch-friendly verification. + +## Why fold the binding signature into `ShieldedOutputsHeader` rather than introducing a new header? + +The binding signature is required iff `ShieldedOutputsHeader` is present (Rule 3). Two headers that always co-occur are arguably one header. Folding has three concrete benefits and one cost. + +Benefits: + +1. **Saves the 1 B header tag** — small but free, and accumulates over chain history. +2. **One less header type** in `VertexHeaderId`. Hathor's enum stays smaller; one fewer mental object for wallet authors and verifier readers. +3. **Replaces the deprecated `UnshieldBalanceHeader` cleanly.** That header was a separate balance-data header; extending the precedent with a third one (`BalanceSignatureHeader`) is the wrong direction. Folding lets us collapse balance attestation into the existing shielded-outputs envelope and retire `UnshieldBalanceHeader` outright. + +Cost: + +- **Mild conceptual misnaming.** The binding signature is a transaction-level value-balance proof, not specifically about shielded outputs — but in practice the two are 1-to-1 in this RFC, and the misnaming is paid for by simplicity. If a future RFC adds asset-balance binding signatures or block-level aggregated signatures, those can be addressed when the time comes (either by extending `ShieldedOutputsHeader` further or by introducing a separate header for aggregation). + +A separate `BalanceSignatureHeader` was considered earlier in this RFC's drafting and rejected on the grounds above. + +## Why not enforce single-shielded-output protection? + +The previous Rule 4 required at least 2 shielded outputs when all inputs are transparent, originally to avoid the cryptographic trivial-commitment issue caused by `r_n = 0`. With independent random blindings under this RFC, the cryptographic issue is gone; the remaining issue is a structural amount-balance leak that no wallet rule can fully prevent. + +Two arguments for *not* enforcing: + +- **The leak is structural.** `v_o = Σ v_transparent_in − Σ v_transparent_out − fee` is a balance identity. Adding a second shielded output hides individual values but not their sum, and a wallet that constructs the second output's value as a fixed multiple of the first is no different from a single-output transaction in terms of leaked information. +- **User intent.** A user who constructs a single-shielded-output shield-in may have deliberate reasons (testing, atomic commitments, low-privacy regimes) and should not be blocked by consensus. + +Two arguments for enforcing: + +- **Footgun reduction.** Most users will not understand the balance leak and will assume "shielded = private." +- **Cheap to enforce.** Adding a second 700 B output is small overhead. + +This RFC takes a permissive stance (warn but don't enforce) and flags the question as unresolved. See [Unresolved Questions](#unresolved-questions). + +## Impact of not doing this + +Without a chain-wide audit equation: + +- Inflation bugs in any verifier path remain silent until economically exploited. +- External auditors must trust the per-tx verification of every full node. +- Receiver-chosen blindings, multi-party transaction construction, and composable output addition are blocked at the wallet level. +- The `UnshieldBalanceHeader` scalar-excess publication remains the only chain-public excess data — covering only full-unshield transactions and exposing the scalar directly. + +Cumulatively: weaker safety story for shielded outputs, less wallet flexibility, and a privacy footgun (scalar excess in `UnshieldBalanceHeader`) that is preferable to remove anyway. + +# Prior art +[prior-art]: #prior-art + +## Zcash Sapling (NU2, 2018) + +Sapling's **binding signature** is the direct precedent. Sapling publishes a per-tx `cv_balance` (value commitment to the net balance) and a Schnorr signature over the sighash with `cv_balance · G` as the public key. The construction enforces value-balance non-interactively and is structurally identical to this RFC's binding signature. + +Differences: + +- Sapling has a single shielded pool with one value generator (homogeneous shielded value); Hathor has per-token generators (multi-asset CT). +- Sapling uses Jubjub curve (an Edwards curve embedded inside BLS12-381 for SNARK efficiency); Hathor uses secp256k1. +- Sapling combines binding signatures with zk-SNARK-based spend authorization; Hathor uses range proofs and surjection proofs separately. + +The audit equation, telescoping argument, and signature mechanic are identical. + +## Zcash Orchard (NU5, 2022) + +Orchard preserves the binding-signature pattern with updates: Pallas/Vesta curves (cycle-friendly for Halo 2), no trusted setup, redesigned commitments. The cryptographic argument for value-balance is unchanged — the binding signature is still a Schnorr-style proof of knowledge of the value-balance discrete log. + +## Mimblewimble (Grin, Beam, since 2018) + +Mimblewimble's "kernel excess" is a per-tx point with a Schnorr signature, identical in spirit to Sapling's binding signature. MW additionally publishes a "kernel offset" scalar to enable transaction aggregation and cut-through unlinkability (a different concern from inflation prevention). Hathor does not aggregate transactions across authors at the network layer, so the kernel offset is not needed. + +## Confidential Transactions (Liquid, original Maxwell proposal) + +The original Confidential Transactions proposal by Greg Maxwell (2015) uses a per-tx Pedersen-commitment-balance signature with the same structure. Liquid (Blockstream's federated sidechain) implements this in production. The chain-wide supply audit equation is the same as proposed here, generalized over per-asset commitments. + +## Monero RingCT + +Monero's RingCT also uses a per-tx commitment-balance proof, integrated with the ring signature for spend authorization. The proof of value-balance is functionally a Schnorr-style DL knowledge proof; the difference is the integration with ring authorization. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +1. **Single-shielded-output shield-in policy.** Should consensus require at least 2 shielded outputs when all inputs are transparent? This RFC currently recommends but does not enforce. Arguments on both sides documented in [Section 4.5](#45-updated-transaction-rules) and [Rationale](#rationale-and-alternatives). Resolve before activation. + +2. **`UnshieldBalanceHeader` removal lifecycle.** This RFC removes `UnshieldBalanceHeader` (`0x13`) and folds its role into the extended `ShieldedOutputsHeader`. Under migration Path A (co-activation), no legacy transactions exist. Under Path B (post-shielded hard-fork), pre-`H_audit` transactions retain `UnshieldBalanceHeader` in chain history, but it is rejected for new transactions after `H_audit`. The exact lifecycle for the existing `UnshieldBalanceHeader` code path (delete immediately, retain for verifying historical blocks only, etc.) needs explicit decision based on the chosen migration path. + +3. **Asset balance binding signatures.** This RFC's binding signature covers value balance but not asset balance. For `FullShieldedOutput` transactions, asset blinding factors must also balance (`Σ s_inputs = Σ s_outputs`), enforced today by wallet construction analogous to the value blinding. Should we add a second binding signature for asset balance? Or rely on the existing surjection proof guarantees? Trade-off: one more 99 B header per shielded tx vs. weaker chain-wide asset audit. + +4. **Per-token kernel excesses (HTR-only audit).** The current chain-wide equation is joint across all tokens. To audit HTR alone (independent of custom token issuer behavior), either constrain HTR to transparent operations only or publish per-asset kernel excesses (loses asset privacy). The right choice depends on HTR's deployment policy. Resolve before activation. + +5. **Block-level binding signature aggregation.** Schnorr signatures within a block could be aggregated using MuSig-style techniques, reducing per-block cryptographic verification cost and storage. This is an optimization, not a correctness concern; defer to future RFC. + +6. **Genesis-state initialization.** The audit equation requires a `total_supply[token]` initial value at genesis. For HTR, this is the publicly known initial allocation. For custom tokens, supply is zero at token creation and grows via mint operations. Specify the exact initial state and how it interacts with the snapshot offset under migration Path B. + +7. **Sighash domain separator.** The signature's challenge hash uses domain separator `"HathorBindingSig/v1"`. Confirm this domain separator does not collide with any existing protocol hash (transaction sighash, ECDH key derivation, NUMS asset tag derivation). + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Asset Balance Binding Signature + +An analogous binding signature for asset blinding factors would enable chain-wide *asset* audit (in addition to value audit). The construction mirrors this RFC's value binding signature: publish `A_tx = a_tx · G` where `a_tx = Σ s_out − Σ s_in` (asset blindings), and sign with `A_tx` as pubkey. Cost: another 99 B header per shielded tx. Benefit: external observers can detect cross-asset forgery in the same way they detect inflation. + +## Block-Level Binding Signature Aggregation + +Multiple binding signatures within a block can be combined using MuSig or related multi-signature aggregation, reducing the per-block signature size from `64 · n` bytes to `64 + n_extra` bytes (where `n_extra` is small overhead). Verification also amortizes. Particularly valuable as shielded transaction volume grows. + +## Cross-Block UTXO Pruning Invariants + +The chain-wide audit equation depends only on `Σ C_utxo` and `Σ_tx E_tx`. If UTXO set commitments (e.g., utreexo or merkle-summed UTXO accumulators) are introduced, the audit equation can be incorporated into the accumulator, enabling stateless verification: a light client could verify supply without storing the entire UTXO set, given only the accumulator root and the running excess sum. + +## Per-Token Public Audit Snapshots + +Token issuers could publish periodic "audit snapshots" — signed claims about their token's supply at a given block height — verifiable against the audit equation. This combines the audit with issuer attestation for regulatory or transparency purposes. + +## Bridging to Other Privacy Pools + +If Hathor introduces a second shielded pool (e.g., an Orchard-style pool for stronger asset privacy), binding signatures from each pool combine homomorphically. Cross-pool transfers can be audited via a unified equation. + +## HTR-Only Audit via Constrained HTR Operations + +Independent of this RFC, a separate policy decision could constrain HTR mint, melt, shield, and unshield operations to be transparent, making HTR supply auditable from cleartext fields alone — independent of custom-token issuer behavior. The two changes compose: this RFC provides chain-wide auditability for all tokens jointly; the HTR-transparent policy provides HTR-specific auditability without depending on custom-token issuer correctness. + +## Removal of Asset Privacy Tradeoff + +Per-asset binding signatures (mentioned in Unresolved Question 4) leak which tokens appear in each transaction, weakening asset privacy. A future construction using zero-knowledge per-asset balance proofs (rather than per-asset binding signatures) could provide both per-asset chain-wide audit AND asset privacy. This is an open research direction; reasonable approaches include Halo 2 / Plonk-based asset-balance circuits, or Sapling's asset-aware extensions. From e75b36d5b22383dca251c70d342684c0a1162fe9 Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Fri, 26 Jun 2026 16:05:23 -0500 Subject: [PATCH 6/6] design: consolidate shielded outputs RFC to match implementation Rewrite 0000-shielded-outputs.md as a single self-contained spec matching the feat/shielded-outputs-rebased implementation, and remove the satellite mint/melt and audit RFCs (folded in). Key corrections to match the code: - Range proofs are secp256k1-zkp Borromean at fixed 40 bits ([1, 2^40), ~3213 B), not Bulletproofs; output sizes and fees updated accordingly. - Fold in MintHeader (0x14) / MeltHeader (0x15): entry format, Rule M4 augmented balance, DEPOSIT 1% deposit/withdraw, FEE-token per-entry charge, surjection-domain extension, and TCT rules. - No binding signatures: full-unshield uses UnshieldBalanceHeader carrying a 32-byte scalar excess. Binding-signature proposals moved to Future possibilities; document the scalar-based network-wide supply audit that works today. - Single SHIELDED_TRANSACTIONS feature flag (no SHIELDED_MINT_MELT); max headers 5 when enabled. - Crypto layout htr-ct-crypto -> htr-lib -> hathorlib/crypto/shielded with real domain separators; exact data model, header IDs, constants, sighash coverage, and exception names. - Rule 4 enforced unconditionally (>=2 shielded outputs); add Rules M5/M6. Co-Authored-By: Claude Opus 4.8 (1M context) --- text/0000-shielded-outputs-audit.md | 576 --------------- text/0000-shielded-outputs-mint-melt.md | 763 ------------------- text/0000-shielded-outputs.md | 942 +++++++++++++++--------- 3 files changed, 589 insertions(+), 1692 deletions(-) delete mode 100644 text/0000-shielded-outputs-audit.md delete mode 100644 text/0000-shielded-outputs-mint-melt.md diff --git a/text/0000-shielded-outputs-audit.md b/text/0000-shielded-outputs-audit.md deleted file mode 100644 index 74d9966..0000000 --- a/text/0000-shielded-outputs-audit.md +++ /dev/null @@ -1,576 +0,0 @@ -- Feature Name: shielded_outputs_audit -- Start Date: 2026-04-29 -- RFC PR: (leave this empty) -- Hathor Issue: (leave this empty) -- Author: Hathor Labs - -# Summary -[summary]: #summary - -This RFC extends [Shielded Outputs](0000-shielded-outputs.md) with a per-transaction **binding signature** that enables network-wide supply auditability. The existing `ShieldedOutputsHeader` is augmented with a 33-byte kernel-excess point `E_tx = e_tx · G` together with a 64-byte Schnorr signature proving knowledge of the discrete log. The signature simultaneously serves as a non-interactive proof of value-balance and binds the proof to the transaction. A chain-wide audit equation then permits any external observer to verify, in a single curve-equation per token, that no inflation has occurred across the entire transaction history. The construction is structurally identical to Zcash Sapling's binding signature, generalized to Hathor's per-token confidential transactions. As a side benefit, the change removes the "wallet forces `e_tx = 0`" pattern from the current shielded design, allowing every output's blinding factor to be independently uniformly random — unlocking receiver-chosen blindings, multi-party transaction construction, and composable output addition. - -# Motivation -[motivation]: #motivation - -The shielded outputs feature hides individual UTXO amounts behind Pedersen commitments. While per-transaction balance is enforced by every full node during verification, **chain-wide supply auditability is lost**: an external monitor can no longer sum cleartext UTXO amounts and compare against expected supply. - -This matters for two distinct populations: - -1. **Inflation-bug monitors.** A bug in any verifier path — range proof check, surjection proof check, balance equation, or any future addition — could allow a transaction to create value from nothing. Without a chain-wide invariant, such a bug remains silent until it is exploited and economic damage manifests. With a chain-wide audit equation, a single check against the UTXO set detects any past inflation regardless of which verifier path was buggy. - -2. **External auditors and explorers.** Token issuers, regulators, exchanges, and explorers need to verify total circulating supply per token. For tokens whose issuer chooses transparent mint/melt, supply is currently auditable from cleartext fields alone. But this depends on the issuer's good faith. A protocol-level audit equation makes supply verification independent of issuer choices for the entire shielded-pool history. - -The current shielded design supports per-tx balance via the equation `Σ C_in = Σ C_out + fee · H_HTR`, with the wallet constructing the last shielded output's blinding factor as the residual `r_n = Σ r_in − Σ r_others` so that the per-tx kernel excess is structurally zero. This per-tx zero-excess pattern has three consequences: - -- **No chain-wide telescoping.** The kernel excess is never published, so the standard CT/Mimblewimble chain-wide audit construction does not apply. -- **Within-tx blinding correlation.** If any `n−1` blindings of a transaction leak (RNG flaw, partial seed compromise, side channel), the last output's blinding is fully determined. -- **Sender must know all blindings.** The construction requires the sender to compute the residual blinding, which precludes Sapling-style receiver-derived blindings, multi-party transaction construction, and easy output addition. - -This RFC introduces a per-tx binding signature that simultaneously closes the audit gap and removes the construction constraints, at a cost of 98 bytes per shielded transaction. - -**Expected outcome.** Any external observer, given the entire chain history and the current UTXO set, can verify in a single curve-point equation per token that no value has been created or destroyed across the entire shielded-pool history. Wallet construction simplifies: every output's blinding factor is independently uniformly random. The change is a backward-incompatible extension to `ShieldedOutputsHeader` (gated by network upgrade) and removes the now-redundant `UnshieldBalanceHeader`. - -# Guide-level explanation -[guide-level-explanation]: #guide-level-explanation - -## 1. The Audit Equation - -After this RFC, an external monitor (anyone holding the chain history and the current UTXO set) can verify total supply per token via: - -``` -Σ C_utxo == total_supply · H_token + Σ_tx E_tx -``` - -- `Σ C_utxo` — sum of all unspent value commitments. Transparent UTXOs are treated as commitments with `r = 0`, i.e., `C = v · H_token`. Shielded UTXOs use their on-chain commitment directly. -- `total_supply` — the publicly computable expected supply, derived from cleartext fields: `initial + Σ mint − Σ melt ± Σ shield/unshield deltas`. -- `Σ_tx E_tx` — the running sum of every shielded transaction's binding-signature public key (`E_tx = e_tx · G`). Maintained incrementally as a single 33-byte curve point. - -Both sides are points on secp256k1. The verifier computes both and checks equality. If any historical transaction inflated the supply (for any reason — verifier bug, broken range proof, miscomputed mint), the equation fails. - -The equation is the multi-asset generalization of Zcash Sapling's chain-wide value balance. The same machinery proves both per-tx balance (during verification) and chain-wide balance (during audit) — see [Section 4.4](#44-the-binding-signature-as-proof-of-balance). - -## 2. The Binding Signature - -Each shielded transaction's `ShieldedOutputsHeader` is extended with three trailing fields: - -| Field | Size | Purpose | -|---|---|---| -| `excess_point` | 33 B | Kernel excess `E_tx = e_tx · G` — public key commitment to the transaction's blinding-factor balance | -| `signature_R` | 33 B | Schnorr signature commitment `R = k · G` | -| `signature_z` | 32 B | Schnorr signature response `z = k + c · e_tx (mod q)` | - -Together these prove knowledge of `e_tx` such that `excess_point = e_tx · G`, bound to the transaction sighash. The verifier checks two things: - -``` -1. Σ C_out − Σ C_in − fee · H_HTR == E_tx (balance equation) -2. Schnorr.Verify(E_tx, sighash, signature) (binding signature) -``` - -The first check enforces homomorphic balance against the published `E_tx`. The second check proves that `E_tx` lies on the `G`-subgroup with a known discrete log — which forces value terms in the LHS to cancel exactly, since `G` and `H_token` are independent generators with no known DL relation. - -## 3. What Changes for Wallets - -The wallet no longer needs to compute the last output's blinding factor as a residual. Instead: - -``` -Old construction: - for i in 0..n-1: r_i ←$ Z_q - r_n := Σ r_in − Σ_{i= H_audit} E_tx + legacy_offset -``` - -where `legacy_offset = Σ r_pre_H_audit_utxos · G`, fixed at `H_audit` and computed once by summing the pre-fork UTXO commitments and subtracting `pre_fork_supply · H_token`. This single "snapshot offset" point is published as part of the network upgrade and lets the chain-wide equation remain valid across the transition. - -Wallets and full nodes MUST be updated before `H_audit`. Old wallets producing pre-RFC `ShieldedOutputsHeader` (without trailer) after `H_audit` will produce invalid transactions, rejected by full nodes for missing binding-signature fields. The deserializer distinguishes the two formats by network-upgrade context — there is no in-band version byte, since the header layout is determined by the activation height. - -# Drawbacks -[drawbacks]: #drawbacks - -- **98 byte per-tx wire cost.** Every shielded transaction grows by a fixed-size trailer in `ShieldedOutputsHeader`. For a transaction with two shielded outputs (~1.6 KB), this is ~6% overhead. Negligible per-tx, but accumulates over chain history. - -- **One additional cryptographic verification per transaction.** Schnorr verification is a single multi-exp on secp256k1 — fast, but not free. Batch verification across a block amortizes the cost. - -- **Increased complexity for wallet implementations.** Wallets must compute `e_tx`, derive the binding-signature key, sign with deterministic nonces, and serialize the extended `ShieldedOutputsHeader` trailer. The change is well-isolated (mechanical sign/verify, fixed-size fields) but is still an additional surface area. - -- **Single-output shield-in privacy footgun.** The structural amount leak when all inputs are transparent and there is one shielded output remains. This RFC documents the issue and recommends a wallet warning, but consensus does not enforce a multiple-shielded-output rule. A user who constructs such a transaction reveals the shielded value through the balance equation, regardless of the binding signature. - -- **No per-token isolation.** The chain-wide audit equation is a single check across all tokens jointly. A monitor cannot isolate HTR auditability from custom token correctness without either (a) constraining HTR to transparent operations or (b) adding per-asset binding signatures (loses asset privacy). This is discussed in [Future Possibilities](#future-possibilities). - -# Rationale and alternatives -[rationale-and-alternatives]: #rationale-and-alternatives - -## Why publish `E_tx` as a curve point rather than the scalar `e_tx`? - -A scalar `e_tx` would also prevent inflation: the verifier checks `Σ C_out − Σ C_in − fee · H_HTR = e_tx · G`, computing `e_tx · G` from the published scalar; the RHS is structurally on the `G`-subgroup with known discrete log. No Schnorr signature would be needed for inflation prevention. Wire format: 32 B vs 33 + 64 = 97 B. - -The privacy cost of publishing the scalar is unacceptable: - -1. **Direct blinding-factor leak in the single-output corner case.** Where the point form leaks only `r_o · G` (recovering `r_o` requires solving DLP), the scalar form leaks `r_o` directly. Defense-in-depth lost. - -2. **`Σ r_utxo` becomes a publicly computable scalar.** Any single UTXO blinding leak via side channel composes with the chain-wide sum to reveal further blindings. Point form forces an attacker to solve DLP for each composition. - -3. **Receiver-chosen blinding is impossible.** Sapling-style receivers contribute `r_recipient · G` (a public point from their viewing key) without revealing `r_recipient`. Sender aggregation works on points, not on scalars. - -4. **No path to multi-party / non-interactive aggregation.** Combining contributions from multiple parties requires summing kernel excesses without revealing individual contributions. Point form supports this; scalar form does not. - -5. **Cross-tx statistical analysis** of published scalars could expose RNG biases. Points are uniform group elements with no exploitable structure. - -The 65 B saved are not worth losing defense-in-depth, receiver-chosen blindings, and aggregation friendliness. - -## Why a Schnorr signature rather than a different proof of DL knowledge? - -Schnorr is the standard proof of knowledge of discrete log, with three desirable properties: 64 B size, single multi-exp verification, batch-verifiable. Alternatives considered: - -- **ECDSA**: also 64 B, also batch-verifiable in some constructions, but more complex and lacks linearity (cannot trivially aggregate). Schnorr is preferable. -- **EdDSA / Ed25519**: requires a different curve. The shielded outputs design uses secp256k1 throughout; introducing a second curve is unwarranted complexity. -- **BLS signature**: smaller (48 B) and aggregatable, but requires pairing-friendly curves (BLS12-381) and pairings for verification — expensive and inconsistent with the rest of the shielded design. -- **Bulletproofs proof of knowledge**: overkill (and 600+ B). - -Schnorr over secp256k1 with deterministic nonces, `(R, z)` form for batch-friendly verification. - -## Why fold the binding signature into `ShieldedOutputsHeader` rather than introducing a new header? - -The binding signature is required iff `ShieldedOutputsHeader` is present (Rule 3). Two headers that always co-occur are arguably one header. Folding has three concrete benefits and one cost. - -Benefits: - -1. **Saves the 1 B header tag** — small but free, and accumulates over chain history. -2. **One less header type** in `VertexHeaderId`. Hathor's enum stays smaller; one fewer mental object for wallet authors and verifier readers. -3. **Replaces the deprecated `UnshieldBalanceHeader` cleanly.** That header was a separate balance-data header; extending the precedent with a third one (`BalanceSignatureHeader`) is the wrong direction. Folding lets us collapse balance attestation into the existing shielded-outputs envelope and retire `UnshieldBalanceHeader` outright. - -Cost: - -- **Mild conceptual misnaming.** The binding signature is a transaction-level value-balance proof, not specifically about shielded outputs — but in practice the two are 1-to-1 in this RFC, and the misnaming is paid for by simplicity. If a future RFC adds asset-balance binding signatures or block-level aggregated signatures, those can be addressed when the time comes (either by extending `ShieldedOutputsHeader` further or by introducing a separate header for aggregation). - -A separate `BalanceSignatureHeader` was considered earlier in this RFC's drafting and rejected on the grounds above. - -## Why not enforce single-shielded-output protection? - -The previous Rule 4 required at least 2 shielded outputs when all inputs are transparent, originally to avoid the cryptographic trivial-commitment issue caused by `r_n = 0`. With independent random blindings under this RFC, the cryptographic issue is gone; the remaining issue is a structural amount-balance leak that no wallet rule can fully prevent. - -Two arguments for *not* enforcing: - -- **The leak is structural.** `v_o = Σ v_transparent_in − Σ v_transparent_out − fee` is a balance identity. Adding a second shielded output hides individual values but not their sum, and a wallet that constructs the second output's value as a fixed multiple of the first is no different from a single-output transaction in terms of leaked information. -- **User intent.** A user who constructs a single-shielded-output shield-in may have deliberate reasons (testing, atomic commitments, low-privacy regimes) and should not be blocked by consensus. - -Two arguments for enforcing: - -- **Footgun reduction.** Most users will not understand the balance leak and will assume "shielded = private." -- **Cheap to enforce.** Adding a second 700 B output is small overhead. - -This RFC takes a permissive stance (warn but don't enforce) and flags the question as unresolved. See [Unresolved Questions](#unresolved-questions). - -## Impact of not doing this - -Without a chain-wide audit equation: - -- Inflation bugs in any verifier path remain silent until economically exploited. -- External auditors must trust the per-tx verification of every full node. -- Receiver-chosen blindings, multi-party transaction construction, and composable output addition are blocked at the wallet level. -- The `UnshieldBalanceHeader` scalar-excess publication remains the only chain-public excess data — covering only full-unshield transactions and exposing the scalar directly. - -Cumulatively: weaker safety story for shielded outputs, less wallet flexibility, and a privacy footgun (scalar excess in `UnshieldBalanceHeader`) that is preferable to remove anyway. - -# Prior art -[prior-art]: #prior-art - -## Zcash Sapling (NU2, 2018) - -Sapling's **binding signature** is the direct precedent. Sapling publishes a per-tx `cv_balance` (value commitment to the net balance) and a Schnorr signature over the sighash with `cv_balance · G` as the public key. The construction enforces value-balance non-interactively and is structurally identical to this RFC's binding signature. - -Differences: - -- Sapling has a single shielded pool with one value generator (homogeneous shielded value); Hathor has per-token generators (multi-asset CT). -- Sapling uses Jubjub curve (an Edwards curve embedded inside BLS12-381 for SNARK efficiency); Hathor uses secp256k1. -- Sapling combines binding signatures with zk-SNARK-based spend authorization; Hathor uses range proofs and surjection proofs separately. - -The audit equation, telescoping argument, and signature mechanic are identical. - -## Zcash Orchard (NU5, 2022) - -Orchard preserves the binding-signature pattern with updates: Pallas/Vesta curves (cycle-friendly for Halo 2), no trusted setup, redesigned commitments. The cryptographic argument for value-balance is unchanged — the binding signature is still a Schnorr-style proof of knowledge of the value-balance discrete log. - -## Mimblewimble (Grin, Beam, since 2018) - -Mimblewimble's "kernel excess" is a per-tx point with a Schnorr signature, identical in spirit to Sapling's binding signature. MW additionally publishes a "kernel offset" scalar to enable transaction aggregation and cut-through unlinkability (a different concern from inflation prevention). Hathor does not aggregate transactions across authors at the network layer, so the kernel offset is not needed. - -## Confidential Transactions (Liquid, original Maxwell proposal) - -The original Confidential Transactions proposal by Greg Maxwell (2015) uses a per-tx Pedersen-commitment-balance signature with the same structure. Liquid (Blockstream's federated sidechain) implements this in production. The chain-wide supply audit equation is the same as proposed here, generalized over per-asset commitments. - -## Monero RingCT - -Monero's RingCT also uses a per-tx commitment-balance proof, integrated with the ring signature for spend authorization. The proof of value-balance is functionally a Schnorr-style DL knowledge proof; the difference is the integration with ring authorization. - -# Unresolved questions -[unresolved-questions]: #unresolved-questions - -1. **Single-shielded-output shield-in policy.** Should consensus require at least 2 shielded outputs when all inputs are transparent? This RFC currently recommends but does not enforce. Arguments on both sides documented in [Section 4.5](#45-updated-transaction-rules) and [Rationale](#rationale-and-alternatives). Resolve before activation. - -2. **`UnshieldBalanceHeader` removal lifecycle.** This RFC removes `UnshieldBalanceHeader` (`0x13`) and folds its role into the extended `ShieldedOutputsHeader`. Under migration Path A (co-activation), no legacy transactions exist. Under Path B (post-shielded hard-fork), pre-`H_audit` transactions retain `UnshieldBalanceHeader` in chain history, but it is rejected for new transactions after `H_audit`. The exact lifecycle for the existing `UnshieldBalanceHeader` code path (delete immediately, retain for verifying historical blocks only, etc.) needs explicit decision based on the chosen migration path. - -3. **Asset balance binding signatures.** This RFC's binding signature covers value balance but not asset balance. For `FullShieldedOutput` transactions, asset blinding factors must also balance (`Σ s_inputs = Σ s_outputs`), enforced today by wallet construction analogous to the value blinding. Should we add a second binding signature for asset balance? Or rely on the existing surjection proof guarantees? Trade-off: one more 99 B header per shielded tx vs. weaker chain-wide asset audit. - -4. **Per-token kernel excesses (HTR-only audit).** The current chain-wide equation is joint across all tokens. To audit HTR alone (independent of custom token issuer behavior), either constrain HTR to transparent operations only or publish per-asset kernel excesses (loses asset privacy). The right choice depends on HTR's deployment policy. Resolve before activation. - -5. **Block-level binding signature aggregation.** Schnorr signatures within a block could be aggregated using MuSig-style techniques, reducing per-block cryptographic verification cost and storage. This is an optimization, not a correctness concern; defer to future RFC. - -6. **Genesis-state initialization.** The audit equation requires a `total_supply[token]` initial value at genesis. For HTR, this is the publicly known initial allocation. For custom tokens, supply is zero at token creation and grows via mint operations. Specify the exact initial state and how it interacts with the snapshot offset under migration Path B. - -7. **Sighash domain separator.** The signature's challenge hash uses domain separator `"HathorBindingSig/v1"`. Confirm this domain separator does not collide with any existing protocol hash (transaction sighash, ECDH key derivation, NUMS asset tag derivation). - -# Future possibilities -[future-possibilities]: #future-possibilities - -## Asset Balance Binding Signature - -An analogous binding signature for asset blinding factors would enable chain-wide *asset* audit (in addition to value audit). The construction mirrors this RFC's value binding signature: publish `A_tx = a_tx · G` where `a_tx = Σ s_out − Σ s_in` (asset blindings), and sign with `A_tx` as pubkey. Cost: another 99 B header per shielded tx. Benefit: external observers can detect cross-asset forgery in the same way they detect inflation. - -## Block-Level Binding Signature Aggregation - -Multiple binding signatures within a block can be combined using MuSig or related multi-signature aggregation, reducing the per-block signature size from `64 · n` bytes to `64 + n_extra` bytes (where `n_extra` is small overhead). Verification also amortizes. Particularly valuable as shielded transaction volume grows. - -## Cross-Block UTXO Pruning Invariants - -The chain-wide audit equation depends only on `Σ C_utxo` and `Σ_tx E_tx`. If UTXO set commitments (e.g., utreexo or merkle-summed UTXO accumulators) are introduced, the audit equation can be incorporated into the accumulator, enabling stateless verification: a light client could verify supply without storing the entire UTXO set, given only the accumulator root and the running excess sum. - -## Per-Token Public Audit Snapshots - -Token issuers could publish periodic "audit snapshots" — signed claims about their token's supply at a given block height — verifiable against the audit equation. This combines the audit with issuer attestation for regulatory or transparency purposes. - -## Bridging to Other Privacy Pools - -If Hathor introduces a second shielded pool (e.g., an Orchard-style pool for stronger asset privacy), binding signatures from each pool combine homomorphically. Cross-pool transfers can be audited via a unified equation. - -## HTR-Only Audit via Constrained HTR Operations - -Independent of this RFC, a separate policy decision could constrain HTR mint, melt, shield, and unshield operations to be transparent, making HTR supply auditable from cleartext fields alone — independent of custom-token issuer behavior. The two changes compose: this RFC provides chain-wide auditability for all tokens jointly; the HTR-transparent policy provides HTR-specific auditability without depending on custom-token issuer correctness. - -## Removal of Asset Privacy Tradeoff - -Per-asset binding signatures (mentioned in Unresolved Question 4) leak which tokens appear in each transaction, weakening asset privacy. A future construction using zero-knowledge per-asset balance proofs (rather than per-asset binding signatures) could provide both per-asset chain-wide audit AND asset privacy. This is an open research direction; reasonable approaches include Halo 2 / Plonk-based asset-balance circuits, or Sapling's asset-aware extensions. diff --git a/text/0000-shielded-outputs-mint-melt.md b/text/0000-shielded-outputs-mint-melt.md deleted file mode 100644 index dcb0bcf..0000000 --- a/text/0000-shielded-outputs-mint-melt.md +++ /dev/null @@ -1,763 +0,0 @@ -- Feature Name: shielded_outputs_mint_melt -- Start Date: 2026-04-27 -- RFC PR: (leave this empty) -- Hathor Issue: (leave this empty) -- Author: Hathor Labs - -# Summary -[summary]: #summary - -This RFC extends [shielded outputs](./0000-shielded-outputs.md) to support **token -creation, mint, and melt** operations inside shielded transactions. It introduces two -new transaction headers — `MintHeader` and `MeltHeader` — that publicly declare the -per-token supply changes inside an otherwise-shielded transaction. The declared -amounts are bound into the homomorphic balance equation as public scalar terms, -allowing the verifier to enforce supply correctness without revealing where the -value lands. As a consequence, parent RFC Rule 8 ("Mint/Melt Transactions Cannot -Have Shielded Outputs") is replaced by the rules in this document, and -`TokenCreationTransaction` may now carry shielded outputs of the new token. - -# Motivation -[motivation]: #motivation - -The parent RFC delivers amount and token-type privacy for ordinary value transfers -but explicitly excludes mint and melt operations (parent Rule 8). Two real -problems follow: - -- **Privacy continuity.** A token issuer who routinely transacts in shielded form - must drop to fully-transparent mode to mint or melt, and then create a follow-up - shielding transaction. This leaks the timing and economic shape of every - issuance event, and breaks privacy continuity for the issuer's downstream - business flows (payroll, vendor payments). -- **Confidential issuance.** Many issuance use cases — pre-funding salaries, B2B - receivables, treasury operations — should reveal *the supply change* but not - *the recipient set or the per-recipient amount*. Today, every minted unit is - visible in plaintext outputs. - -Auditability is preserved because the mint/melt amounts remain public per token — -declared in the new headers — so total supply per token is still publicly -computable. What becomes private is *where the new tokens land* and *which inputs -are melted from*. - -**Expected outcome.** A shielded transaction can mint or melt any custom token -and create new tokens directly into shielded outputs, with the same auditability -guarantees as today's transparent mint/melt operations. - -# Guide-level explanation -[guide-level-explanation]: #guide-level-explanation - -## 1. What changes - -A shielded transaction may now exercise mint or melt authority. The amounts are -declared publicly via two new headers: - -- **`MintHeader`** — list of `(token_index, amount)` entries declaring supply - *created* in this transaction. -- **`MeltHeader`** — list of `(token_index, amount)` entries declaring supply - *destroyed* in this transaction. - -Both are required only for shielded transactions. Transparent-only mint/melt -transactions (no shielded inputs and no shielded outputs) continue to use the -existing implicit amount-balance equation — no header needed. - -A given token may appear in **either** `MintHeader` **or** `MeltHeader` in a -single transaction, never both (see Rule M3). - -Concrete worked examples for `DEPOSIT`-version tokens (with HTR deposit/withdraw -math) appear in §3.4 and §3.6; an example for `FEE`-version tokens (with -per-output fee math) appears in §3.5. - -## 2. Token creation can now be shielded - -Token creation transactions previously rejected shielded outputs outright. With -this RFC: - -- The new token's UID is still `tx.hash` and occupies `tokens[0]` (token_index 1). -- Outputs of the new token may be transparent **or** shielded. -- The total initial supply is declared via a single `MintHeader` entry for the - new token's index. The Pedersen balance equation reconciles transparent + - shielded outputs against the declared supply. -- Mint/melt authority outputs for the new token remain transparent (parent - RFC Rule 7). - -## 3. Examples - -### 3.1 Confidential mint - -An issuer holds a transparent mint authority for token T and wants to mint -100,000 T directly into a single shielded output for a salary recipient. - -``` -Inputs: - - mint authority of T (transparent) - - HTR (shielded, for deposit + fee) - -Outputs: - - mint authority of T (transparent, retained) - - shielded HTR change (shielded) - - shielded T output: 100,000 T (shielded) - -Headers: - - FeeHeader: standard fee - - ShieldedOutputsHeader: 2 shielded outputs - - MintHeader: [(token_index=1, amount=100000)] -``` - -Observers learn: *100,000 T were minted*. They do not learn the recipient or -that the issuer's HTR change is non-zero. - -### 3.2 Confidential token creation - -An issuer creates token T with initial supply 1,000,000, distributed across -4 shielded outputs. - -``` -Inputs: - - HTR (transparent or shielded), enough for 1% deposit + fee - -Outputs: - - 4 shielded T outputs (shielded) - - transparent HTR change (transparent) - -Headers: - - FeeHeader - - ShieldedOutputsHeader: 4 shielded outputs - - MintHeader: [(token_index=1, amount=1000000)] -``` - -The token UID is the tx hash; the supply (`1,000,000`) is publicly declared. -Per-recipient amounts are private. - -### 3.3 Confidential melt - -A treasurer melts 50,000 T (e.g., burning seized supply) without revealing -which UTXO was destroyed. - -``` -Inputs: - - melt authority of T (transparent) - - shielded T input (shielded) - -Outputs: - - melt authority of T (transparent, retained) - - shielded HTR withdraw + change (shielded) - -Headers: - - FeeHeader - - ShieldedOutputsHeader: 1 shielded output - - MeltHeader: [(token_index=1, amount=50000)] -``` - -The melted amount is public; the input commitment is opaque on-chain. - -### 3.4 DEPOSIT-version token: mint with HTR deposit - -Token `TD` is `DEPOSIT`-version. The issuer mints 100,000 TD across two -shielded outputs. The Hathor 1% deposit rule applies: minting 100,000 TD costs -1,000 HTR. Assume the standard per-output fee is 1 HTR and shielded outputs -cost 1 HTR each (`FEE_PER_AMOUNT_SHIELDED_OUTPUT`). - -``` -Inputs: - - mint authority of TD (transparent) - - HTR (transparent), 1,002 HTR available - -Outputs: - - mint authority of TD (transparent, retained) - - 2 shielded TD outputs (totals 100,000 TD) - -Headers: - - FeeHeader: 2 HTR fee (2 × FEE_PER_AMOUNT_SHIELDED_OUTPUT) - - ShieldedOutputsHeader: 2 shielded outputs - - MintHeader: [(token_index=1, amount=100000)] - -Verifier: - - deposit = 0.01 × 100,000 = 1,000 HTR (derived from MintHeader, applied - as a public output term on H_HTR). - - Augmented balance: - sum(C_in) + 100,000·H_TD - == sum(C_out) + 1,000·H_HTR + 2·H_HTR - - HTR side reduces to: 1,002·H_HTR (input) == 1,000·H_HTR (deposit) + 2·H_HTR (fee). - - TD side: 0·H_TD (no input of TD) + 100,000·H_TD == sum(shielded TD commitments). -``` - -What observers learn: 100,000 TD were minted; 1,000 HTR were burned for -deposit; 2 HTR fee was paid. They do not learn how the 100,000 TD were split -between the two shielded outputs or who the recipients are. - -### 3.5 FEE-version token: mint paying per-output fees - -Token `TF` is `FEE`-version. The issuer mints 50,000 TF into one transparent -output of 30,000 TF (paid to a public counterparty) plus one shielded output -of 20,000 TF (treasury). `FEE`-version tokens have no HTR deposit; per-output -fees apply instead, plus one extra `FEE_PER_OUTPUT` per `MintHeader` / -`MeltHeader` entry on a `FEE`-version token (see Rule M4). - -``` -Inputs: - - mint authority of TF (transparent) - - HTR (shielded), enough for fees - -Outputs: - - mint authority of TF (transparent, retained) - - transparent TF output: 30,000 TF (chargeable) - - shielded TF output: 20,000 TF - - shielded HTR change - -Headers: - - FeeHeader: - 1 × FEE_PER_OUTPUT (one chargeable transparent TF output) - + 2 × FEE_PER_AMOUNT_SHIELDED_OUTPUT (two shielded outputs) - - ShieldedOutputsHeader: 2 shielded outputs - - MintHeader: [(token_index=1, amount=50000)] - -Verifier: - - No HTR deposit/withdraw on the 1% rule (FEE-version tokens skip it). - - Each MintHeader / MeltHeader entry on a FEE-version token contributes - one FEE_PER_OUTPUT charge to the output side of the augmented balance - equation (paid by the user from HTR transparent inputs). Like the 1% - deposit for DEPOSIT-version tokens, this charge is folded into the - balance equation directly — it is NOT declared in FeeHeader, which - continues to cover only chargeable transparent outputs and shielded - outputs. - - Augmented balance: - sum(C_in) + 50,000·H_TF - == sum(C_out) + 1·FEE_PER_OUTPUT·H_HTR + total_fee·H_HTR - where the TF side balances 50,000 newly-minted units against - 30,000 (transparent) + 20,000 (shielded committed); the - 1·FEE_PER_OUTPUT term reflects the MintHeader entry charge; and - total_fee is the FeeHeader sum. - - Total fee in FeeHeader must match exactly: - FEE_PER_OUTPUT × 1 (transparent TF output) - + FEE_PER_AMOUNT_SHIELDED_OUTPUT × 2 (two shielded outputs). -``` - -What observers learn: 50,000 TF were minted; 30,000 TF went to a publicly -visible transparent output; the remaining 20,000 TF entered the shielded -pool. They do not learn the recipient of the shielded output or the issuer's -HTR change. The per-entry FEE_PER_OUTPUT charge is visible (it's part of the -public balance equation), but it leaks no per-recipient information — every -MintHeader entry on a FEE-version token pays exactly one FEE_PER_OUTPUT -regardless of how many shielded recipients the entry is split across. - -### 3.6 DEPOSIT-version token: melt with HTR withdraw - -Token `TD` is `DEPOSIT`-version. The treasurer melts 80,000 TD; this releases -800 HTR back from the deposit pool to the spender (1% withdraw rule). - -``` -Inputs: - - melt authority of TD (transparent) - - shielded TD input (shielded, holds at least 80,000 TD plus optional change) - -Outputs: - - melt authority of TD (transparent, retained) - - shielded HTR output (carries the 800 HTR withdraw + any change) - - optional shielded TD change (if input held more than 80,000) - -Headers: - - FeeHeader: shielded fees only - - ShieldedOutputsHeader - - MeltHeader: [(token_index=1, amount=80000)] - -Verifier: - - withdraw = 0.01 × 80,000 = 800 HTR. - - Augmented balance: - sum(C_in) + 800·H_HTR - == sum(C_out) + 80,000·H_TD + fee·H_HTR - - TD side: input commitment holds X TD; output side holds (X − 80,000) TD - (in optional change) plus 80,000·H_TD as a public melt term. - - HTR side: 800·H_HTR (input from withdraw) is balanced by the recipient's - shielded HTR output(s). -``` - -## 4. What remains visible - -For every shielded mint/melt transaction, the following remains public: - -- **Per-token supply delta**: each `(token_index, amount)` in MintHeader/MeltHeader. -- **Authority spend events**: mint/melt authority inputs and outputs are always - transparent (parent Rule 7), so observers see *when* an authority is exercised. -- **HTR deposit/withdraw**: derived from the declared mint/melt amounts using - Hathor's existing 1% deposit rule for `DEPOSIT`-version tokens. -- **Transaction structure**: input/output counts, transparent vs. shielded - partition, and which outputs are authority outputs. - -What becomes private: - -- Recipient sets and per-recipient amounts of newly minted tokens. -- Which shielded UTXOs are consumed by a melt operation. -- The HTR-side change resulting from a deposit/withdraw, if shielded. - -# Reference-level explanation -[reference-level-explanation]: #reference-level-explanation - -## 4.1 Header layout - -### MintHeader - -| Field | Size | Description | -|-------|------|-------------| -| Header ID | 1 B | `0x14` (`VertexHeaderId.MINT_HEADER`) | -| `num_entries` | 1 B | `1 ≤ num_entries ≤ 16` | -| Entries | variable | Concatenated entries | - -Each entry: - -| Field | Size | Description | -|-------|------|-------------| -| `token_index` | 1 B | `1 ≤ token_index ≤ len(tx.tokens)` (HTR/index 0 forbidden) | -| `amount` | 8 B BE | Public mint amount, `amount ≥ 1` | - -Constraints: - -- All `token_index` values within a header are distinct. -- All `amount` values are `≥ 1`. Zero entries are forbidden (zero entries leak no - information and only inflate header size). -- `token_index = 0` (HTR) is forbidden — HTR is never minted by user - transactions. - -### MeltHeader - -Wire format and constraints identical to `MintHeader`, with header ID `0x15` -(`VertexHeaderId.MELT_HEADER`). `token_index = 0` is similarly forbidden. - -### Header order - -Canonical ordering is ascending `VertexHeaderId`: - -``` -0x10 NANO_HEADER -0x11 FEE_HEADER -0x12 SHIELDED_OUTPUTS_HEADER -0x13 UNSHIELD_BALANCE_HEADER -0x14 MINT_HEADER -0x15 MELT_HEADER -``` - -`MintHeader` precedes `MeltHeader` when both are present. - -### Maximum number of headers - -`get_maximum_number_of_headers()` is raised from 3 to **at least 5**, to allow -`FeeHeader + (ShieldedOutputsHeader | UnshieldBalanceHeader) + MintHeader + -MeltHeader` plus a margin for future additions. The exact value is a consensus -parameter and is gated by feature activation. - -### Sighash coverage - -The full serialization of `MintHeader` and `MeltHeader` (header_id + count + -entries) is included in the transaction sighash. Mutating any entry invalidates -all signatures over the transaction. This is required because the declared -amounts directly affect the verified balance equation. - -### Disclosure model - -Each entry in `MintHeader` or `MeltHeader` discloses two things: the **token -reference** (`token_index`) and the **amount**. Their visibility differs. - -**Token reference: always transparent.** The token reference is plaintext for -two reasons that together rule out hiding it: - -1. The mint or melt authority output is itself transparent (parent Rule 7) and - exposes the token UID via its `token_data`. Hiding the token in the header - would leak nothing additional but provide no privacy benefit. -2. The verifier must read the token's version (`NATIVE` / `DEPOSIT` / `FEE`) to - apply the correct deposit-or-fee logic. A hidden token would block this - lookup, requiring an unbounded ZK proof of "I am applying the right rule for - the right version". - -**Amount: transparent in this RFC; shielded amounts are a pending business -decision.** The headers in this RFC carry plaintext `u64` amounts. The -trade-off is auditability vs. privacy: - -- *Transparent amounts (this RFC's choice).* Total token supply is publicly - computable per token by summing `MintHeader` and `MeltHeader` entries across - the chain. Compatible with light-client supply auditors. **Required** for - `DEPOSIT`-version tokens because the HTR deposit is `0.01 × amount` and must - be verifiable against the public amount. -- *Shielded amounts (alternative, deferred).* The header carries a Pedersen - commitment to the amount plus a range proof. Total supply becomes opaque on - chain. Feasible for `FEE`-version tokens (fees are per-output, not - per-amount). For `DEPOSIT`-version tokens this requires an additional ZK - proof binding the HTR deposit to 1% of the committed amount, which is - substantially more machinery than the rest of this RFC. - -Because the choice has direct consequences for auditability — and because the -two token versions admit different complexity for shielded amounts — the RFC -treats this as a pending business decision (see [Unresolved -Questions](#unresolved-questions)). - -## 4.2 Verification rules - -Six new rules govern shielded mint/melt. All rules from the parent RFC -(Rules 1–7) continue to apply; **Rule 8 is replaced by Rules M1–M6 below**. - -### Rule M1: Headers are valid only on shielded transactions - -If `MintHeader` or `MeltHeader` is present, the transaction MUST be shielded -(`tx.is_shielded()`). Presence on a non-shielded transaction is rejected with -`HeaderNotSupported`. - -### Rule M2: Mint/melt authority required for each entry - -For each `(token_index, amount)` in `MintHeader`, the transaction MUST consume -at least one mint authority input for `tx.tokens[token_index − 1]`. - -Symmetric for `MeltHeader` and melt authority. Authority inputs and outputs -remain transparent (parent Rule 7). - -### Rule M3: One direction per token - -For any given token, a single transaction may declare **either** a mint -**or** a melt, never both. Concretely: a `token_index` that appears in -`MintHeader` MUST NOT appear in `MeltHeader`, and vice versa. - -Self-offsetting (mint X and melt Y of the same token in one tx) is meaningless -because the same net supply effect is achievable by adjusting amounts. -Permitting it would also complicate per-token supply accounting on the -indexer side without delivering any user-visible capability. - -### Rule M4: Augmented homomorphic balance - -The shielded balance equation (parent §4.4) is augmented: - -``` -sum(C_in) + sum_T(mint_T · H_T) == - sum(C_out) + sum_T(melt_T · H_T) + sum(C_fee_entries) + - deposit · H_HTR − withdraw · H_HTR - + fee_token_charge · H_HTR -``` - -Where: - -- For each `MintHeader` entry `(T, amount)`: add `amount · H_T` to the input - side. The minted amount has no associated blinding factor (it appears - unblinded in the balance equation). -- For each `MeltHeader` entry `(T, amount)`: add `amount · H_T` to the output - side, also unblinded. -- `H_T = derive_asset_tag(token_uid_T)`. -- `deposit = Σ get_deposit_token_deposit_amount(amount)` over `MintHeader` - entries whose token is `DEPOSIT`-version. -- `withdraw = Σ get_deposit_token_withdraw_amount(amount)` over `MeltHeader` - entries whose token is `DEPOSIT`-version. -- `fee_token_charge = FEE_PER_OUTPUT × N_FEE_entries`, where `N_FEE_entries` - is the count of `MintHeader` plus `MeltHeader` entries whose token is - `FEE`-version. This charge is always paid by the user (it lands on the - output side regardless of mint vs. melt). - -The `deposit` and `withdraw` terms move HTR through the equation in the same -way the existing transparent verifier handles them -(`_check_token_permissions_and_deposits`), but now sourced from the public -header amounts instead of inferred from transparent value flows. - -`FEE`-version tokens skip the 1% deposit/withdraw rule but pay -`FEE_PER_OUTPUT` per `MintHeader` / `MeltHeader` entry (the -`fee_token_charge` term above). This matches the spirit of the transparent -per-output rule — every declared mint/melt action of a `FEE`-version token -costs the issuer one chargeable-output-equivalent — without leaking -recipient counts: the charge is per header entry, not per shielded -recipient. An `AmountShieldedOutput` of a `FEE`-version token does not also -incur a per-shielded-recipient `FEE_PER_OUTPUT` charge; only the generic -`FEE_PER_AMOUNT_SHIELDED_OUTPUT` (and `FEE_PER_FULL_SHIELDED_OUTPUT` for -`FullShieldedOutput`) applies on top of the per-entry charge. - -### Rule M5: Trivial commitment protection still applies - -Parent RFC Rule 4 (≥ 2 shielded outputs when all inputs are transparent, or -include a transparent output) is **not relaxed** by the presence of a -`MintHeader`. The minted amount enters the balance equation unblinded, so it -provides no entropy that would otherwise mask a single shielded output. - -### Rule M6: FeeHeader still required - -A shielded mint/melt transaction MUST carry a `FeeHeader` (parent Rule 2). -Deposit and withdraw amounts derived from `MintHeader`/`MeltHeader` are NOT -declared in the FeeHeader — they are folded into the balance equation -separately as Rule M4 specifies. The per-entry `FEE_PER_OUTPUT` charge for -`FEE`-version `MintHeader`/`MeltHeader` entries is also handled in the -balance equation (not in FeeHeader); FeeHeader continues to cover only -chargeable transparent outputs and shielded outputs. - -## 4.3 Surjection-domain extension - -`FullShieldedOutput` requires a surjection proof showing its asset commitment -corresponds to one of the input domain generators (parent §4.1). For minted -tokens, no input contributes that asset, which would otherwise prevent -`FullShieldedOutput` of a freshly-minted token. - -**Extension.** For each `(T, amount)` entry in `MintHeader`, the unblinded -NUMS asset tag `H_T = derive_asset_tag(token_uid_T)` is added to the surjection -proof domain. This permits a `FullShieldedOutput` of a minted token to prove -its asset is one of the (transparent inputs ∪ shielded inputs ∪ minted tokens). - -`MeltHeader` does NOT extend the surjection domain, because melt produces no -new outputs of the melted token. - -## 4.4 Token creation transactions - -`TokenCreationTransaction` (TCT) gains the ability to carry shielded outputs. -The existing rejection -`InvalidShieldedOutputError('shielded outputs are not allowed in -TokenCreationTransaction')` is removed. - -**TCT-specific rules.** - -1. The new token UID is `tx.hash` and occupies `tokens[0]` (token_index 1) per - existing semantics. -2. If the TCT is shielded (carries `ShieldedOutputsHeader`), it MUST carry a - `MintHeader` whose entries include exactly one entry for `token_index = 1`, - with `amount > 0`. -3. The single-entry-for-new-token rule replaces the existing - `verify_minted_tokens` check (`token_info.amount > 0`) for shielded TCTs. - Non-shielded TCTs retain the existing check. -4. The new token's `H_T` is added to the surjection-proof domain (per - §4.3), enabling `FullShieldedOutput` of the new token. -5. Authority outputs for the new token remain transparent (parent Rule 7). - -## 4.5 Verification pipeline integration - -### Phase 1 (without storage) - -- `verify_headers` (`vertex_verifier.py`): allow `MintHeader` and `MeltHeader` - on `REGULAR_TRANSACTION` and `TOKEN_CREATION_TRANSACTION` when the - `shielded_transactions` feature is active. Canonical-ordering check still - applies. -- New `verify_mint_melt_headers_well_formed`: - - Both headers, if present, are non-empty. - - All `token_index` values within a header are unique. - - No token appears in both headers (Rule M3). - - All `token_index` values are in `[1, len(tx.tokens)]`. - - All `amount` values are ≥ 1. -- New `verify_mint_melt_requires_shielded` (Rule M1). - -### Phase 2 (with storage) - -- New `verify_mint_melt_authority_inputs` (Rule M2): for each `MintHeader` - entry, walk `tx.inputs`; at least one mint authority input must reference - `tx.tokens[token_index − 1]`. Symmetric for melt. -- Updated `verify_shielded_balance` (Rule M4): incorporate the - `MintHeader`/`MeltHeader` terms in the homomorphic balance call. -- Updated `_check_token_permissions_and_deposits` (shielded path): compute - `deposit`/`withdraw` from `MintHeader`/`MeltHeader` entries instead of - skipping them; fold into the HTR balance equation. -- Updated `verify_surjection_proofs` (§4.3): augment the domain with - `derive_asset_tag(tokens[token_index − 1])` for each `MintHeader` entry. -- Removed: `verify_no_mint_melt`. Replaced by Rules M1–M2 + M4. -- Removed: TCT-specific blanket block on shielded outputs. - -## 4.6 Wire format example - -``` -MintHeader serialization: - header_id(1B=0x14) | num_entries(1B) | - entry_0: token_index(1B) | amount(8B BE) | - entry_1: token_index(1B) | amount(8B BE) | ... - -Single-entry MintHeader for 100,000 of token at index 1: - 0x14 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x01 0x86 0xA0 -``` - -`MeltHeader` is identical with `header_id = 0x15`. - -## 4.7 Feature activation - -The headers are gated by a sub-feature on top of the parent RFC's flag: - -```python -class Feature(StrEnum): - SHIELDED_TRANSACTIONS = 'SHIELDED_TRANSACTIONS' # parent RFC - SHIELDED_MINT_MELT = 'SHIELDED_MINT_MELT' # this RFC -``` - -```python -ENABLE_SHIELDED_MINT_MELT: FeatureSetting = FeatureSetting.DISABLED -``` - -`SHIELDED_MINT_MELT` requires `SHIELDED_TRANSACTIONS` to be active (validated at -startup). When `SHIELDED_MINT_MELT` is disabled, parent RFC Rule 8 remains in -force and `MintHeader`/`MeltHeader` are rejected. - -This phasing lets the parent RFC ship and stabilize before mint/melt support is -enabled, minimizing the blast radius if a verification bug is found. - -## 4.8 Indexer and explorer impact - -- **Token supply tracking** is fully restored: an indexer reconciles per-token - supply by reading `MintHeader`/`MeltHeader` entries on every shielded tx in - addition to existing transparent mint/melt detection. -- **Per-tx mint/melt event** is observable by anyone: `(token, amount, - "mint" | "melt")` is published as plain header data. -- The "skip shielded inputs in rocksdb tokens index" pattern (commit `316e68b7`) - extends naturally — shielded outputs of minted tokens are still skipped in - per-UTXO indexing because their amount is hidden, but supply totals are - derived from the headers. - -# Drawbacks -[drawbacks]: #drawbacks - -1. **Surface area growth.** Two new headers, new verifier paths, and a - relaxation of parent RFC Rule 8. Each is security-sensitive. -2. **Header limit pressure.** Bumping `get_maximum_number_of_headers()` from 3 - to ≥ 5 is a consensus parameter change that must be carefully gated. -3. **Subtle balance equation.** Rule M4 changes the homomorphic balance - equation. A bug here could enable inflation. Cross-checks against the - existing transparent-deposit arithmetic are mandatory. -4. **Synthetic surjection-domain entries.** Adding minted-token asset tags to - the surjection domain widens the anonymity set in a way the underlying - library should already support, but it is a new code path that must be - exercised in tests against `secp256k1-zkp`. -5. **Incomplete privacy.** Mint/melt amounts remain public per token. Issuers - seeking *total* privacy of issuance are not served by this RFC. - -# Rationale and alternatives -[rationale-and-alternatives]: #rationale-and-alternatives - -## Why declare amounts publicly? - -Pedersen commitments hide amounts but cannot, on their own, distinguish -"minted from nothing" from "received from an input of equal amount". Without a -public scalar declaring the mint, the prover could mint arbitrary amounts. The -public declaration binds the mint to a scalar that enters the balance equation, -preserving the Pedersen "no inflation" guarantee. - -A purely private alternative — proving "I have authority to mint up to X and -am minting Y ≤ X" via a ZK proof — is an order of magnitude more complex -(circuits, trusted setup or larger proofs, new cryptographic dependencies) and -loses the auditability property. Public declaration is the lightest mechanism -that delivers the desired privacy improvement (recipients/inputs hidden) while -keeping supply auditable. - -## Why a single MintHeader with a list (not multiple instances)? - -The existing vertex header model enforces "at most one of each header type" -(`vertex_verifier.py:273-284`). Allowing multiple `MintHeader` instances would -require a special case. A single header carrying a list: - -- Preserves the existing invariant. -- Enforces uniqueness across entries cheaply. -- Keeps the binary format compact. - -## Why prohibit cross-token offsetting (Rule M3)? - -Allowing the same token in both `MintHeader` and `MeltHeader` is meaningless: -the same net effect is achieved by adjusting amounts. Permitting it doubles -the verification surface for no benefit. - -## Why not extend FeeHeader? - -`FeeHeader` semantics are "transparent additions to the output side of the -balance equation, burned". Mint amounts are inputs (added to input side); melt -amounts are outputs but with deposit/withdraw side effects on HTR. Conflating -these into FeeHeader would obscure the equation and require the FeeHeader to -carry sign information. - -## Why not a single combined `MintMeltHeader`? - -Mint and melt have opposite signs in the balance equation and asymmetric -surjection-domain effects (mint extends the domain; melt does not). Separate -headers make the binary format and verifier state clearer, at the cost of one -extra header ID. - -## Why not a MAC instead of a public amount? - -A MAC of "this tx mints N of T" generated by the mint authority key adds a new -cryptographic dependency without strong benefit: the existing transaction -signature already authenticates the spend of the authority input. The amount -itself is what enters the balance equation. - -## Impact of not doing this - -Mint/melt operations remain forever transparent. Shielded users who need to -mint or melt drop to plaintext mode and create follow-up shielding transactions, -leaking timing and economic shape. Issuers seeking confidential issuance have -no on-chain option. - -# Prior art -[prior-art]: #prior-art - -## Liquid / Elements: Confidential Assets and Issuance - -Elements supports confidential asset issuance in a single transaction. The -issuance carries a public asset_id and amount, very similar in spirit to -`MintHeader`. Elements additionally supports *blinded issuance*, the privacy -upgrade analogous to a future ZK-based version of this RFC. - -## Zcash Sapling: value-balance field - -Sapling tracks net flows between transparent and shielded pools via a -`valueBalance` field on the transaction. This is the closest analog to -declaring a public per-asset delta on an otherwise-shielded transaction. - -## Monero - -Monero has no token concept and therefore no direct analog for token mint/melt. -However, Monero's `RingCT` design demonstrates the soundness of using public -scalar amounts (in coinbase emission) inside a Pedersen-commitment balance -equation. - -# Unresolved questions -[unresolved-questions]: #unresolved-questions - -1. **Sub-feature flag vs single flag.** Should `SHIELDED_MINT_MELT` be a - distinct feature flag (allowing the parent RFC to ship first), or be folded - into `SHIELDED_TRANSACTIONS` and shipped together? -2. **`get_maximum_number_of_headers()` value.** 5 is the minimum; 6 leaves - margin for future headers but expands the consensus parameter further. Final - value to be determined. -3. **`NanoHeader` interaction.** May a transaction simultaneously carry a - `NanoHeader` and be a shielded mint/melt? Phase-1 simplification: forbid - the combination; reconsider when shielded Nano Contracts are designed - (parent RFC §4.8). -4. **Transparent vs. shielded mint/melt amounts.** This RFC declares amounts - as plaintext `u64` (see [Disclosure model](#disclosure-model)). The - alternative — Pedersen commitments with range proofs — would hide token - supply entirely from public observers, at the cost of: (a) substantial - extra ZK machinery for `DEPOSIT`-version tokens (a proof that the HTR - deposit equals 1% of the committed amount); (b) loss of the public - "supply auditor" property that lets any node compute total supply per - token; (c) divergent behavior between `DEPOSIT`- and `FEE`-version tokens - (this RFC charges `FEE`-version entries one `FEE_PER_OUTPUT` per - `MintHeader`/`MeltHeader` entry rather than per shielded recipient, so - the per-entry charge is preserved without leaking the recipient count). - The choice is a business decision about how much auditability to trade - away. This RFC's plaintext design can be retrofitted later via a new - feature flag without invalidating already-issued tokens. -5. **Authority-output presence requirement.** Rule M2 requires an authority - *input* but does not require the transaction to produce a corresponding - authority *output*. Should we additionally require authority retention by - default to prevent accidental authority burn? (Current Hathor behavior - permits intentional authority destruction, so no change recommended.) -6. **Shielded UnshieldBalanceHeader interaction.** A full-unshield transaction - that also mints (e.g., the issuer mints into a transparent recipient) - carries `UnshieldBalanceHeader + MintHeader`. The combined balance equation - is well-defined but warrants explicit test coverage. - -# Future possibilities -[future-possibilities]: #future-possibilities - -## Blinded mint amounts - -Extend `MintHeader` to optionally carry a Pedersen commitment instead of a -plaintext amount, with a range proof and a "mint quota proof" demonstrating -the mint stays within an authority-bound limit. Requires a quota mechanism -attached to mint authorities. - -## Mint to specific recipients - -A future header could bind a mint event to a specific destination, useful for -compliance-aware tokens (e.g., regulated stablecoins where issuance must be -attributable to a registered counterparty). - -## Cross-token atomic batching - -Multi-token support in `MintHeader` already allows atomic multi-token -issuance. A future RFC could formalize a "token bundle" semantic on top of -this primitive (e.g., simultaneously minting a governance token and its -matching reward token). - -## Confidential authority provenance - -Combine with Phase C (input unlinkability): the mint authority input itself -could be obscured via ring signatures, hiding *which authority UTXO* was -exercised even when the authority is owned across multiple keys. diff --git a/text/0000-shielded-outputs.md b/text/0000-shielded-outputs.md index 2ca27d1..b165801 100644 --- a/text/0000-shielded-outputs.md +++ b/text/0000-shielded-outputs.md @@ -7,7 +7,16 @@ # Summary [summary]: #summary -This RFC introduces **shielded outputs** for Hathor transactions — a header-based extension that hides output amounts and, optionally, token types using Pedersen commitments, Bulletproof range proofs, and asset surjection proofs. Rather than defining a new transaction version, shielded outputs attach to existing transaction types (regular transactions and token creation transactions) via a `ShieldedOutputsHeader`, preserving full backward compatibility. Two privacy tiers are offered: `AmountShieldedOutput` (amount hidden, token visible) and `FullShieldedOutput` (both amount and token hidden). Recipients recover hidden values through ECDH-based range proof rewinding, requiring no out-of-band communication. +This RFC introduces **shielded outputs** for Hathor transactions — a header-based extension that hides output amounts and, optionally, token types using Pedersen commitments, range proofs, and asset surjection proofs. Rather than defining a new transaction version, shielded outputs attach to existing transaction types (regular transactions and token creation transactions) via a set of transaction headers, preserving full backward compatibility. Two privacy tiers are offered: `AmountShieldedOutput` (amount hidden, token visible) and `FullShieldedOutput` (both amount and token hidden). Recipients recover hidden values through ECDH-based range proof rewinding, requiring no out-of-band communication. + +The design covers four headers: `ShieldedOutputsHeader` (carries the shielded outputs), `UnshieldBalanceHeader` (carries the balance residual for full-unshield transactions), and `MintHeader` / `MeltHeader` (publicly declare per-token supply changes so that mint and melt operations may participate in shielded transactions while remaining auditable). This document is self-contained: it specifies the full data model, cryptography, transaction rules, verification pipeline, fee mechanism, and supply-audit properties as actually implemented. + +> **Implementation status.** This RFC documents the implementation on branch +> `feat/shielded-outputs-rebased` of `hathor-core`. Where the design differs from +> earlier drafts (range proof construction, balance-residual representation, fee +> rates, library layout), this document reflects the **code**, and the earlier +> framing is preserved only under [Future possibilities](#future-possibilities) +> when it describes work not yet implemented. # Motivation [motivation]: #motivation @@ -21,10 +30,11 @@ This transparency creates real problems: - **Trading and DeFi.** Front-runners and MEV extractors exploit visible amounts to sandwich trades or copy strategies. - **Personal finance.** Any recipient of a payment can trace the sender's full balance and transaction history. - **Multi-token privacy.** Hathor supports custom tokens. When token types are visible, observers can track the flow of specific assets (e.g., loyalty points, governance tokens, stablecoins), revealing business relationships and portfolio composition. +- **Confidential issuance.** A token issuer who routinely transacts in shielded form should be able to mint or melt without dropping to fully-transparent mode, while still letting anyone audit total supply. -Shielded outputs address these problems by making amounts and token types cryptographically opaque to everyone except the transaction participants, while preserving the ability of every full node to verify that no inflation or double-spending has occurred. +Shielded outputs address these problems by making amounts and token types cryptographically opaque to everyone except the transaction participants, while preserving the ability of every full node to verify that no inflation or double-spending has occurred — and the ability of anyone to audit per-token supply. -**Expected outcome:** Users can opt into amount privacy (and optionally token-type privacy) on a per-output basis, within ordinary Hathor transactions, with no protocol-level changes to transaction versions, input formats, or the DAG structure. +**Expected outcome:** Users can opt into amount privacy (and optionally token-type privacy) on a per-output basis, within ordinary Hathor transactions, with no protocol-level changes to transaction versions, input formats, or the DAG structure. Issuers can mint, melt, and create tokens directly into shielded outputs. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation @@ -50,10 +60,10 @@ Hathor offers **two privacy tiers**, selectable per output: | Tier | Hides Amount | Hides Token | Proof Overhead | Use Case | |------|:---:|:---:|---|---| -| `AmountShieldedOutput` | Yes | No | Range proof (~675 B) | Hide salary amounts while token type is public | -| `FullShieldedOutput` | Yes | Yes | Range proof + surjection proof (~675 + ~130 B) | Full privacy for multi-token transactions | +| `AmountShieldedOutput` | Yes | No | Range proof (~3213 B) | Hide salary amounts while token type is public | +| `FullShieldedOutput` | Yes | Yes | Range proof + surjection proof (~3213 + variable B) | Full privacy for multi-token transactions | -Both tiers use **Pedersen commitments** for amounts and **Bulletproof range proofs** to guarantee amounts are non-negative. `FullShieldedOutput` additionally uses **blinded asset tags** and **asset surjection proofs** to hide and validate the token type. +Both tiers use **Pedersen commitments** for amounts and **Borromean range proofs** to guarantee amounts are non-negative (specifically in `[1, 2^40)`). `FullShieldedOutput` additionally uses **blinded asset tags** and **asset surjection proofs** to hide and validate the token type. ### The Privacy Stack @@ -67,7 +77,7 @@ Shielded outputs are Phase B of Hathor's three-phase privacy roadmap: +-----------------------------------------------------------+ +-----------------------------------------------------------+ | Phase B: SHIELDED OUTPUTS <- THIS RFC | -| Pedersen commitments + Bulletproofs + surjection proofs | +| Pedersen commitments + range proofs + surjection proofs | | "How much?" + "Which token?" -> Hidden | +-----------------------------------------------------------+ +-----------------------------------------------------------+ @@ -101,53 +111,79 @@ AFTER: Observer does NOT know: the split between Output0 and Output1 ``` -Note: at least 2 shielded outputs are required when all inputs are transparent (Rule 4 — see Section 4.3). This prevents an observer from trivially deducing the single output's amount. +Note: at least 2 shielded outputs are required whenever a transaction has any shielded outputs (Rule 4 — see Section 4.3). This prevents an observer from trivially deducing a single output's amount. ### Unshielding: Returning to Transparency -To unshield, a user spends shielded inputs into transparent outputs: +To unshield, a user spends shielded inputs into transparent outputs. There are two shapes: + +**Partial unshield** — at least one shielded output remains (e.g. shielded change). The balance equation is the ordinary one (`sum(C_in) == sum(C_out) + fee·H_HTR`), and the wallet makes the last shielded output absorb the blinding residual. + +**Full unshield** — *no* shielded outputs remain (every output is transparent). There is no shielded output to absorb the blinding residual, so the transaction carries an **`UnshieldBalanceHeader`** holding the scalar `excess = sum(r_in) − sum(r_out)`, and the balance equation becomes `sum(C_in) == sum(C_out) + excess·G + fee·H_HTR`. ``` -UNSHIELDING TRANSACTION: +FULL-UNSHIELD TRANSACTION: Input: [commitment] (shielded, 90 HTR hidden) - Output0: 60 HTR to Bob (transparent) - Output1: [commitment'] (shielded change, 25 HTR hidden) + Output0: 85 HTR to Bob (transparent) Fee: 5 HTR (transparent) -Observer sees: 60 HTR left the shielded pool, plus a shielded change output -Observer does NOT know: the shielded input amount or the change amount + Header: UnshieldBalanceHeader { excess_blinding_factor (32B scalar) } + +Observer sees: 85 HTR left the shielded pool +Observer does NOT know: the shielded input amount (other than that it equals 85 + 5) ``` +> **Privacy footgun.** The scalar form of `UnshieldBalanceHeader` exposes +> `sum(r_in)` of the unshielded inputs as a raw scalar. A point-form replacement +> (`E_tx = excess·G` plus a Schnorr binding signature) closes this; it is **not +> implemented** and is described in [Future possibilities](#future-possibilities). + ### Mixed Transactions Transparent and shielded inputs/outputs can be freely combined in a single transaction. The balance equation works uniformly: transparent amounts are treated as "trivial commitments" with a zero blinding factor, and the homomorphic verification covers everything in one check. -| Type | Transparent In | Shielded In | Transparent Out | Shielded Out | Use Case | -|------|:---:|:---:|:---:|:---:|---| -| Standard | Yes | — | Yes | — | Legacy transaction | -| Shielding | Yes | — | Optional | Yes (>=2) | Enter shielded pool | -| Confidential | — | Yes | — | Yes | Fully private transfer | -| Unshielding | — | Yes | Yes | Optional | Exit shielded pool | -| Fully mixed | Yes | Yes | Yes | Yes | Maximum flexibility | +| Type | Transparent In | Shielded In | Transparent Out | Shielded Out | Balance header | Use Case | +|------|:---:|:---:|:---:|:---:|---|---| +| Standard | Yes | — | Yes | — | — | Legacy transaction | +| Shielding | Yes | — | Optional | Yes (≥2) | — | Enter shielded pool | +| Confidential | — | Yes | — | Yes (≥2) | — | Fully private transfer | +| Partial unshield | — | Yes | Yes | Yes (≥2) | — | Exit, keep shielded change | +| Full unshield | — | Yes | Yes | — | `UnshieldBalanceHeader` | Exit shielded pool entirely | +| Fully mixed | Yes | Yes | Yes | Yes (≥2) | — | Maximum flexibility | + +A shielded transaction must carry **exactly one** of `ShieldedOutputsHeader` or `UnshieldBalanceHeader` — never both and never neither (enforced inside balance verification). + +### Confidential Mint, Melt, and Token Creation + +A shielded transaction may exercise mint or melt authority and may create new tokens directly into shielded outputs. The per-token supply change is declared publicly via two headers: + +- **`MintHeader`** — list of `(token_index, amount)` entries declaring supply *created* in this transaction. +- **`MeltHeader`** — list of `(token_index, amount)` entries declaring supply *destroyed* in this transaction. + +The declared amounts are bound into the homomorphic balance equation as public scalar terms (Rule M4), so the verifier enforces supply correctness without learning where the new value lands or which UTXO was melted. Because the amounts are public, total per-token supply remains computable by anyone. What becomes private is the *recipient set*, the *per-recipient amounts*, and *which shielded UTXO is melted*. + +A given token may appear in **either** `MintHeader` **or** `MeltHeader` in a single transaction, never both (Rule M3). Worked examples appear in [§2.7](#27-mintmelt-examples). ### Fee Model -Shielded outputs impose additional verification costs (Bulletproof verification ~1ms per output, surjection proof verification, larger storage). A per-output fee compensates the network: +Shielded outputs impose additional verification costs (range-proof verification, surjection-proof verification, larger storage). A per-output fee compensates the network: -- `FEE_PER_AMOUNT_SHIELDED_OUTPUT`: charged per `AmountShieldedOutput` (default: 1 HTR base unit) -- `FEE_PER_FULL_SHIELDED_OUTPUT`: charged per `FullShieldedOutput` (default: 2 HTR base units) +- `FEE_PER_AMOUNT_SHIELDED_OUTPUT`: charged per `AmountShieldedOutput` (default: `1` HTR base unit) +- `FEE_PER_FULL_SHIELDED_OUTPUT`: charged per `FullShieldedOutput` (default: `2` HTR base units) -Fees are declared in the existing `FeeHeader`, are fully transparent, and are burned. See [Section 4.5](#45-fee-mechanism) for details. +Fees are declared in the existing `FeeHeader`, are fully transparent, and are burned. See [Section 4.6](#46-fee-mechanism) for details. ### What Remains Visible Even with shielded outputs, the following information is always public: -- **Transaction structure**: number of inputs, number of outputs, which outputs are shielded vs. transparent -- **Fee amounts**: always plaintext HTR (Rule 2) -- **Authority outputs**: mint/melt authority tokens are always transparent (Rule 7) -- **Scripts**: the locking script (recipient address) remains visible unless combined with Silent Payments (Phase A) -- **Transparent inputs/outputs**: their amounts and tokens remain fully visible +- **Transaction structure**: number of inputs, number of outputs, which outputs are shielded vs. transparent. +- **Fee amounts**: always plaintext HTR (Rule 2). +- **Authority outputs**: mint/melt authority tokens are always transparent (Rule 7). +- **Per-token supply delta**: each `(token_index, amount)` declared in `MintHeader`/`MeltHeader`. +- **HTR deposit/withdraw**: for `DEPOSIT`-version tokens, derived from the declared mint/melt amounts via Hathor's 1% rule. +- **Scripts**: the locking script (recipient address) remains visible unless combined with Silent Payments (Phase A). +- **Transparent inputs/outputs**: their amounts and tokens remain fully visible. ## 3. Shielded Addresses (Pending Decision) @@ -190,7 +226,7 @@ Both keys are present in full. Longer addresses, but the sender has everything n spend_pubkey(33) ``` -Only the spend public key is encoded — no scan key at all. This is the shortest possible shielded address and the simplest to implement. However, without a scan key, the wallet cannot delegate blockchain scanning to a third-party service (since the scan private key is what enables a service to detect incoming payments without having spending authority). The wallet must download and trial-decrypt the entire shielded transaction history itself to compute its balance, which is impractical for light clients. +Only the spend public key is encoded — no scan key at all. This is the shortest possible shielded address and the simplest to implement. However, without a scan key, the wallet cannot delegate blockchain scanning to a third-party service (the scan private key is what enables a service to detect incoming payments without spending authority). The wallet must download and trial-decrypt the entire shielded transaction history itself to compute its balance, which is impractical for light clients. ### Tradeoff Analysis @@ -204,16 +240,16 @@ Only the spend public key is encoded — no scan key at all. This is the shortes | Delegated scanning | Yes (scan key separates detection from spending) | Yes (scan key separates detection from spending) | No (wallet must scan entire history itself) | | Light client support | Yes | Yes | No | -**Status:** Decision pending. Both options are documented here for review. The implementation currently uses the full public key for ECDH, so Option B aligns with the existing code path. +**Status:** Decision pending. The implementation currently uses the full public key for ECDH, so Option B aligns with the existing code path. ## 4. Wallet Integration Guide ### Receiving Shielded Payments -Every shielded output contains an `ephemeral_pubkey` field (33 bytes). The recipient recovers the hidden amount through **ECDH + range proof rewind**: +Every shielded output contains an `ephemeral_pubkey` field (33 bytes; all-zero means "not present"). The recipient recovers the hidden amount through **ECDH + range proof rewind**: 1. **Address match**: The wallet checks if the output script contains `hash(spend_pubkey)` matching one of its keys. -2. **ECDH shared secret**: `s = SHA256(scan_privkey * ephemeral_pubkey)`. +2. **ECDH shared secret**: `s = ECDH(scan_privkey, ephemeral_pubkey)` (secp256k1 ECDH; SHA256 of the shared point). 3. **Nonce derivation**: `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. 4. **Range proof rewind**: Using the nonce as the rewind key, the wallet calls `rewind_range_proof()` which returns the committed `(value, blinding_factor, message)`. 5. **For `FullShieldedOutput`**: The `message` field contains `token_uid(32B) || asset_blinding_factor(32B)`. The wallet reconstructs the expected asset commitment from these values and cross-checks against the on-chain `asset_commitment` to prevent a malicious sender from claiming a worthless token is HTR. @@ -226,17 +262,17 @@ If rewind fails (wrong nonce), the output is not addressed to this wallet — sk 2. **Generate ephemeral keypair** `(e, E = e*G)` for each output. 3. **Compute ECDH shared secret** with the recipient's scan public key (`B_scan`). 4. **Derive nonce** for range proof construction. -5. **Create Pedersen commitment**: `C = amount * H_token + blinding * G`. +5. **Create Pedersen commitment**: `C = amount * H_token + blinding * G` (with `H_token` the blinded asset generator for `FullShieldedOutput`). 6. **Create range proof** using the deterministic nonce (enables recipient rewind). -7. **For `FullShieldedOutput`**: Create blinded asset commitment and surjection proof. -8. **Balance blinding factors**: Assign random blinding factors to all shielded outputs except the last, which receives the balancing factor: `r_last = sum(r_inputs) - sum(r_other_outputs)`. -9. **Compute and attach fee** in `FeeHeader`. +7. **For `FullShieldedOutput`**: Create the blinded asset commitment and surjection proof, and embed `token_uid || asset_blinding_factor` in the range proof message. +8. **Balance blinding factors**: Assign random blinding factors to all shielded outputs except the last, which receives the balancing residual computed by `compute_balancing_blinding_factor()`. (For full-unshield transactions there is no last shielded output, so the residual is published as the `UnshieldBalanceHeader` scalar.) +9. **Compute and attach fee** in `FeeHeader`; attach `MintHeader`/`MeltHeader` if minting or melting. ### Blinding Factor Management and Backup The wallet must store blinding factors for every owned shielded UTXO — without them, the funds cannot be spent. -**Recovery guarantee**: Since blinding factors for received outputs are derived deterministically from `ECDH(scan_privkey, ephemeral_pubkey)` — and both the scan private key (derivable from the seed) and the ephemeral pubkey (stored on-chain) are always available — a **seed backup is sufficient** for full recovery. The wallet re-derives all blinding factors by scanning the blockchain. +**Recovery guarantee**: Since blinding factors for received outputs are derived deterministically from `ECDH(scan_privkey, ephemeral_pubkey)` — and both the scan private key (derivable from the seed) and the ephemeral pubkey (stored on-chain) are always available — a **seed backup is sufficient** for full recovery. The wallet re-derives all blinding factors by scanning the blockchain and rewinding the range proofs. For change outputs, wallets should use deterministic derivation from the input blinding factors and output index to ensure recoverability. @@ -252,7 +288,7 @@ Privacy only affects external observers — the wallet owner always sees exact a ### Rule 4 Compliance -When all inputs are transparent, the wallet must automatically ensure at least 2 shielded outputs (or include a transparent output). This is typically satisfied naturally (payment + change), but the wallet may need to create a zero-value absorber output in edge cases. +Whenever a transaction has any shielded outputs, the wallet must include at least 2 of them. This is typically satisfied naturally (payment + change). Note that zero-value shielded outputs are forbidden by the range proof's `min_value = 1`, so in edge cases the wallet should split a real output or add a small decoy output it owns rather than emit an empty placeholder. ### Fee Computation @@ -262,7 +298,7 @@ shielded_fee = (count_amount_shielded * FEE_PER_AMOUNT_SHIELDED_OUTPUT) total_fee = shielded_fee + standard_fee_if_any ``` -The wallet should display the expected fee before the user confirms. +For `DEPOSIT`-version tokens being minted/melted, the wallet must also fund (or will receive) the 1% HTR deposit/withdraw, which is folded into the balance equation rather than the `FeeHeader`. For `FEE`-version tokens, each `MintHeader`/`MeltHeader` entry costs one extra `FEE_PER_OUTPUT` (also folded into the balance equation). The wallet should display the expected fee before the user confirms. ## 5. Explorer and Indexer Impact @@ -273,37 +309,191 @@ The wallet should display the expected fee before the user confirms. | Show output amounts | Broken for shielded | Hidden behind Pedersen commitments | | Identify output token type | Broken for `FullShieldedOutput` | Hidden behind blinded asset commitments | | Compute address balances | Broken for shielded | Cannot sum opaque curve points | -| Token supply tracking | Unaffected | Mint/melt transactions cannot contain shielded outputs (Rule 8) | +| Token supply tracking | **Unaffected** | Mint/melt amounts are declared publicly in `MintHeader`/`MeltHeader` | | Rich list / rankings | Broken for shielded | Balances not computable | ### What Still Works -- Transaction structure (input/output count, shielded vs. transparent) -- Transparent outputs (amounts, tokens, scripts — unchanged) -- Fee amounts (always transparent) -- Cryptographic proof verification (anyone can verify correctness) -- Authority UTXO tracking (always transparent, Rule 7) -- Token metadata (name, symbol from creation transaction) +- Transaction structure (input/output count, shielded vs. transparent). +- Transparent outputs (amounts, tokens, scripts — unchanged). +- Fee amounts (always transparent). +- Cryptographic proof verification (anyone can verify correctness). +- Authority UTXO tracking (always transparent, Rule 7). +- Token metadata (name, symbol from creation transaction). +- **Per-token supply**: an indexer reconciles supply by reading `MintHeader`/`MeltHeader` entries on every shielded tx in addition to existing transparent mint/melt detection. Shielded UTXOs are skipped in per-UTXO token indexing (their amount is hidden), but supply totals come from the headers. +- **Network-wide supply audit**: `Σ C_utxo == Σ_token (total_supply · H_token)` holds for the whole chain with no protocol change. See [§4.7](#47-network-wide-supply-audit). ### Mitigations -- **Shielded pool boundary tracking**: Explorers can track aggregate amounts entering/leaving the shielded pool from the transparent side of shielding/unshielding transactions. -- **View key delegation**: Users can optionally share view keys with trusted explorers for selective disclosure. +- **Shielded pool boundary tracking**: Explorers can track aggregate amounts entering/leaving the shielded pool from the transparent side of shielding/unshielding transactions and from the mint/melt headers. +- **View key delegation**: Users can optionally share scan keys with trusted explorers for selective disclosure. - **"Shielded" indicator**: Explorers should display shielded outputs with a clear indicator, showing commitment hex values but never placeholder amounts. -## 6. Mint/Melt Transactions Are Always Transparent - -Transactions that exercise mint or melt authority **cannot** contain shielded outputs (Rule 8). Both the authority outputs (Rule 7) and all value outputs in a mint/melt transaction remain fully transparent. +## 6. Confidential Issuance Is Auditable -This means: +Mint, melt, and token-creation transactions can now be shielded, but the supply change is always declared in plaintext via `MintHeader`/`MeltHeader`. This means: -- An explorer can see **when** mint authority is exercised and **exactly how many** tokens were minted or melted. +- An explorer can see **when** mint/melt authority is exercised and **exactly how many** tokens were minted or melted. - Token supply remains fully auditable for all custom tokens — no trust in the token creator is required. -- Users who want amount privacy must move minted tokens into shielded outputs in a separate transaction. +- What is hidden is only the *recipient set*, the *per-recipient split*, and *which shielded UTXO was melted*. + +## 7. Mint/Melt Examples +[27-mintmelt-examples]: #27-mintmelt-examples + +### 7.1 Confidential mint + +An issuer holds a transparent mint authority for token T and wants to mint 100,000 T directly into a single shielded output for a salary recipient (plus a shielded change output to satisfy Rule 4). + +``` +Inputs: + - mint authority of T (transparent) + - HTR (shielded, for deposit + fee) + +Outputs: + - mint authority of T (transparent, retained) + - shielded HTR change (shielded) + - shielded T output: 100,000 T (shielded) + +Headers: + - FeeHeader: standard fee + - ShieldedOutputsHeader: 2 shielded outputs + - MintHeader: [(token_index=1, amount=100000)] +``` + +Observers learn: *100,000 T were minted*. They do not learn the recipient or the issuer's HTR change. + +### 7.2 Confidential token creation + +An issuer creates token T with initial supply 1,000,000, distributed across 4 shielded outputs. + +``` +Inputs: + - HTR (transparent or shielded), enough for 1% deposit + fee + +Outputs: + - 4 shielded T outputs (shielded) + - transparent HTR change (transparent) + +Headers: + - FeeHeader + - ShieldedOutputsHeader: 4 shielded outputs + - MintHeader: [(token_index=1, amount=1000000)] +``` + +The token UID is the tx hash; the supply (`1,000,000`) is publicly declared. Per-recipient amounts are private. A shielded `TokenCreationTransaction` MUST declare its initial supply via exactly one `MintHeader` entry for `token_index = 1`, and MUST NOT melt the new token. (`MeltHeader` is not permitted on a token creation transaction.) + +### 7.3 DEPOSIT-version token: mint with HTR deposit + +Token `TD` is `DEPOSIT`-version. The issuer mints 100,000 TD across two shielded outputs. The 1% deposit rule applies: minting 100,000 TD costs 1,000 HTR. Assume shielded outputs cost 1 HTR each (`FEE_PER_AMOUNT_SHIELDED_OUTPUT`). + +``` +Inputs: + - mint authority of TD (transparent) + - HTR (transparent), 1,002 HTR available + +Outputs: + - mint authority of TD (transparent, retained) + - 2 shielded TD outputs (total 100,000 TD) + +Headers: + - FeeHeader: 2 HTR fee (2 × FEE_PER_AMOUNT_SHIELDED_OUTPUT) + - ShieldedOutputsHeader: 2 shielded outputs + - MintHeader: [(token_index=1, amount=100000)] + +Verifier (Rule M4): + - deposit = 0.01 × 100,000 = 1,000 HTR, folded onto the HTR output side. + - Augmented balance: + sum(C_in) + 100,000·H_TD + == sum(C_out) + 1,000·H_HTR (deposit) + 2·H_HTR (fee) + - HTR side: 1,002·H_HTR (input) == 1,000·H_HTR + 2·H_HTR. + - TD side: 100,000·H_TD == sum(shielded TD commitments). +``` + +### 7.4 FEE-version token: mint paying per-output fees + +Token `TF` is `FEE`-version. The issuer mints 50,000 TF into one transparent output of 30,000 TF plus one shielded output of 20,000 TF. `FEE`-version tokens have no HTR deposit; per-output fees apply, plus one extra `FEE_PER_OUTPUT` per `MintHeader`/`MeltHeader` entry on a `FEE`-version token (Rule M4). + +``` +Inputs: + - mint authority of TF (transparent) + - HTR (shielded), enough for fees + +Outputs: + - mint authority of TF (transparent, retained) + - transparent TF output: 30,000 TF (chargeable) + - shielded TF output: 20,000 TF + - shielded HTR change + +Headers: + - FeeHeader: + 1 × FEE_PER_OUTPUT (one chargeable transparent TF output) + + 2 × FEE_PER_AMOUNT_SHIELDED_OUTPUT (two shielded outputs) + - ShieldedOutputsHeader: 2 shielded outputs + - MintHeader: [(token_index=1, amount=50000)] + +Verifier (Rule M4): + - No 1% deposit (FEE-version skips it). + - Each MintHeader/MeltHeader entry on a FEE-version token contributes one + FEE_PER_OUTPUT charge to the output side of the balance equation (folded + directly, NOT declared in FeeHeader). + - TF side: 50,000·H_TF balances 30,000 (transparent) + 20,000 (shielded). + - FeeHeader total must match exactly: + FEE_PER_OUTPUT × 1 (transparent TF output) + + FEE_PER_AMOUNT_SHIELDED_OUTPUT × 2 (two shielded outputs). +``` + +The per-entry `FEE_PER_OUTPUT` is visible but leaks no per-recipient information — every entry pays exactly one `FEE_PER_OUTPUT` regardless of how many shielded recipients the entry is split across. + +### 7.5 DEPOSIT-version token: melt with HTR withdraw + +Token `TD` is `DEPOSIT`-version. The treasurer melts 80,000 TD; this releases 800 HTR back from the deposit pool (1% withdraw rule). + +``` +Inputs: + - melt authority of TD (transparent) + - shielded TD input (holds ≥ 80,000 TD plus optional change) + +Outputs: + - melt authority of TD (transparent, retained) + - shielded HTR output (carries the 800 HTR withdraw + change) + - optional shielded TD change + +Headers: + - FeeHeader: shielded fees only + - ShieldedOutputsHeader + - MeltHeader: [(token_index=1, amount=80000)] + +Verifier (Rule M4): + - withdraw = 0.01 × 80,000 = 800 HTR, folded onto the HTR input side. + - Augmented balance: + sum(C_in) + 800·H_HTR + == sum(C_out) + 80,000·H_TD + fee·H_HTR +``` # Reference-level explanation [reference-level-explanation]: #reference-level-explanation +## 4.0 Cryptographic Library Layout + +The cryptography is implemented in Rust and exposed to Python: + +``` +htr-rs/crates/htr-ct-crypto Rust core (secp256k1-zkp v0.11): + pedersen, generators, rangeproof, + surjection, ecdh, balance, error + | + v +htr-rs/crates/htr-lib PyO3 native extension. Registers the + `htr_lib.shielded` submodule. + | + v (import: `from htr_lib import shielded`) +hathorlib/hathorlib/crypto/shielded/ Thin Python wrappers: + asset_tag.py, commitment.py, range_proof.py, + surjection.py, ecdh.py, balance.py, recover.py +``` + +`hathor-core` consumes only the `hathorlib` wrappers; it never calls `secp256k1-zkp` directly. The Python data model (output types, headers) lives in `hathorlib` and is reused by `hathor-core`. + ## 4.1 Cryptographic Primitives ### Pedersen Commitments @@ -314,32 +504,28 @@ A Pedersen commitment to a value `v` with blinding factor `r` using generator `H C = v * H + r * G Where: - v = amount (u64, range [0, 2^64)) + v = amount (u64) r = blinding factor (32-byte scalar) H = generator point specific to the token type G = secp256k1 base generator ``` -**Properties:** -- **Hiding**: Given `C`, an observer cannot determine `v` without knowing `r`. -- **Binding**: Given `C`, one cannot find `(v', r')` such that `C = v'*H + r'*G` and `v' != v`, unless one knows the discrete log of `H` w.r.t. `G` (computationally infeasible for NUMS generators). -- **Homomorphic**: `C1 + C2 = (v1+v2)*H + (r1+r2)*G`. This enables balance verification without revealing amounts. +**Properties:** hiding, binding, and homomorphic (`C1 + C2 = (v1+v2)*H + (r1+r2)*G`). Commitments are 33 bytes (compressed point). A *trivial* commitment `C = v*H` (zero blinding) represents transparent amounts in the balance equation. + +Implemented in `htr-ct-crypto/src/pedersen.rs` (`create_commitment`, `create_trivial_commitment`, `verify_commitments_sum`). ### NUMS Asset Tag Derivation -Each token has a deterministic generator `H_token` derived via a Nothing-Up-My-Sleeve (NUMS) construction: +Each token has a deterministic generator `H_token` derived via a Nothing-Up-My-Sleeve (NUMS) construction (`htr-ct-crypto/src/generators.rs`): ``` -H_token = NUMS_hash(token_uid) - -Algorithm: - tag = SHA256("Hathor_AssetTag_v1" || token_uid) - H_token = generator_from_tag(tag) +tag = SHA256("Hathor_AssetTag_v1" || token_uid_32B) +H_token = Generator::new_unblinded(tag) # 33-byte point ``` -The domain separator `"Hathor_AssetTag_v1"` prevents cross-protocol collisions. The construction guarantees no one knows `x` such that `H_token = x*G`, which is essential for the binding property. +The domain separator `"Hathor_AssetTag_v1"` prevents cross-protocol collisions. The construction guarantees no one knows `x` such that `H_token = x*G`, which is essential for binding. The HTR generator is computed once and cached. -**Token UID normalization**: HTR uses `token_uid = b'\x00'` (1 byte) internally, but the crypto library requires 32 bytes. The normalization function pads HTR's token UID with 31 zero bytes. +**Token UID normalization** (`hathorlib/crypto/shielded/asset_tag.py::normalize_token_uid`): Hathor uses `b'\x00'` (1 byte) for HTR and 32-byte hashes for custom tokens; the crypto library always expects 32 bytes. A 1-byte UID is right-padded with zeros to 32 bytes (`token_uid.ljust(32, b'\x00')`); a 32-byte UID passes through unchanged; any other length is an error. ### Blinded Asset Commitments @@ -356,79 +542,76 @@ Where: An observer sees `A` — a random-looking curve point indistinguishable from any other token's blinded commitment. -### Bulletproof Range Proofs +### Borromean Range Proofs -Each shielded output includes a Bulletproof range proof demonstrating: +Each shielded output includes a range proof (secp256k1-zkp's `RangeProof`, a Borromean-style proof — **not** a Bulletproof) demonstrating: ``` -The committed amount v satisfies: 1 <= v < 2^64 +The committed amount v satisfies: 1 <= v < 2^40 ``` -The lower bound of 1 (not 0) prevents zero-amount outputs that could be used in certain attack vectors. - -**Architecture: separate proofs, not aggregated.** Each output carries its own independent Bulletproof. This design supports: +(`htr-ct-crypto/src/rangeproof.rs`.) -- **Multi-party transactions**: Each party generates proofs for their own outputs independently, without revealing amounts. -- **Atomic swaps**: No need to share blinding factors across parties. -- **UTXO pruning**: Spent output proofs can be discarded independently. +- **Lower bound `min_value = 1`** prevents zero-amount outputs (enforced both at proof creation and re-checked at verification). +- **Fixed bit width `RANGE_PROOF_BITS = 40`.** 40 bits covers up to `2^40 − 1 ≈ 1.1 × 10^12` base units (~10 trillion HTR cents), above the maximum supply. Pinning the bit width makes every proof the **same size** regardless of the committed value, eliminating a proof-size side-channel. +- **Proof size**: ~3213 bytes; bounded by `MAX_RANGE_PROOF_SIZE = 3328`. -Performance optimization is achieved through **batch verification** (`verify_multi`), which amortizes multi-exponentiation cost across proofs (estimated 30-50% CPU reduction for multi-output transactions), and parallel verification across transactions. +**Architecture: separate proofs, not aggregated.** Each output carries its own independent range proof. This supports multi-party transactions and atomic swaps (each party proves its own outputs without sharing blinding factors) and independent UTXO pruning. -**Proof size**: ~675 bytes typical, bounded by `MAX_RANGE_PROOF_SIZE = 1024` bytes. +> A `batch_verify_range_proofs` helper exists but is currently a sequential loop +> (`TODO` in code: investigate a true batched secp256k1-zkp API). Treat batch +> verification as a future optimization, not a current property. ### Asset Surjection Proofs -For `FullShieldedOutput` only. Proves the output's blinded asset commitment corresponds to one of the input asset commitments, without revealing which one. +For `FullShieldedOutput` only (`htr-ct-crypto/src/surjection.rs`). Proves the output's blinded asset commitment corresponds to one of the input domain generators, without revealing which one: ``` Given: - Input asset commitments: A_1, A_2, ..., A_n - Output asset commitment: A_out - -Compute differences: d_i = A_out - A_i for each input i - -For the matching input j (same token): - d_j = (H_token + r_out*G) - (H_token + r_j*G) = (r_out - r_j) * G - -> discrete log is KNOWN + Domain generators: A_1, A_2, ..., A_n (one per input + per MintHeader entry) + Output commitment: A_out -For non-matching inputs i != j (different token): - d_i = (H_out + r_out*G) - (H_i + r_i*G) = (H_out - H_i) + (r_out - r_i)*G - -> discrete log is UNKNOWN - -A ring signature on {d_1, ..., d_n} proves knowledge of the discrete log -for exactly one d_i, without revealing which one. +A ring proof over {A_out - A_i} proves knowledge of the discrete log for exactly +one difference (the matching token), without revealing which. ``` -**Proof size**: Grows linearly with the number of inputs in the surjection domain. For a typical transaction with 3 inputs: ~130 bytes. Maximum: `MAX_SURJECTION_PROOF_SIZE = 4096` bytes. +The domain is built from: every transparent input's NUMS asset tag, every shielded input's on-chain `asset_commitment` (or derived tag for an `AmountShieldedOutput` input), and one NUMS asset tag per `MintHeader` entry (so a freshly-minted token can be the asset of a `FullShieldedOutput`). **Proof size**: grows with the domain size. Maximum: `MAX_SURJECTION_PROOF_SIZE = 4096` bytes. ### Homomorphic Balance Verification -The balance equation covers all inputs and outputs uniformly: +The balance equation covers all inputs and outputs uniformly. For transactions with at least one shielded output: ``` sum(C_inputs) == sum(C_outputs) + sum(C_fee_entries) - -Where: - - Shielded inputs/outputs use their on-chain commitment directly - - Transparent inputs/outputs use trivial commitments: C = amount * H_token - - Fee entries from FeeHeader are treated as transparent outputs ``` -Expanding the equation and grouping by generator: +For full-unshield transactions (shielded inputs, no shielded outputs) an excess scalar is added: ``` -(sum(v_in) - sum(v_out) - sum(fees)) * H + (sum(r_in) - sum(r_out)) * G == O +sum(C_inputs) == sum(C_outputs) + sum(C_fee_entries) + excess * G ``` -Since `H` and `G` are linearly independent (no known discrete log relationship), both scalar coefficients must be zero: +Where: +- Shielded inputs/outputs use their on-chain commitment directly. +- Transparent inputs/outputs (and `FeeHeader` entries) use trivial commitments `C = amount * H_token`. +- `excess = sum(r_in) − sum(r_out)`, carried as a 32-byte scalar in `UnshieldBalanceHeader`. -1. `sum(v_in) = sum(v_out) + sum(fees)` — values balance. -2. `sum(r_in) = sum(r_out)` — blinding factors balance. +Expanding via NUMS generator independence, both scalar coefficients must vanish: values balance per token, and blinding factors balance. For transactions with shielded outputs, the wallet enforces the blinding balance by construction (the last shielded output absorbs the residual via `compute_balancing_blinding_factor`). The Rust entry point is `htr-ct-crypto/src/balance.rs::verify_balance`, taking `BalanceEntry::{Transparent, Shielded}` lists plus an optional `excess`. -The wallet enforces condition (2) by construction: it assigns random blinding factors to all outputs except the last, which receives the balancing residual. +The mint/melt augmentation (Rule M4) is detailed in [§4.5](#45-mintmelt-reference). ## 4.2 Data Structures +### OutputMode + +A 1-byte discriminator (`hathorlib/transaction/shielded_tx_output.py`): + +| Value | Name | Meaning | +|---|---|---| +| `0` | `TRANSPARENT` | Standard `TxOutput` (not stored in shielded headers) | +| `1` | `AMOUNT_ONLY` | Amount hidden, token visible (no surjection proof) | +| `2` | `FULLY_SHIELDED` | Amount and token hidden (surjection proof required) | + ### AmountShieldedOutput Hides the amount; token type remains visible. @@ -436,12 +619,10 @@ Hides the amount; token type remains visible. | Field | Type | Size | Description | |-------|------|------|-------------| | `commitment` | bytes | 33 B | Pedersen commitment `C = v*H_token + r*G` | -| `range_proof` | bytes | ~675 B (max 1024) | Bulletproof range proof | +| `range_proof` | bytes | ~3213 B (max 3328) | Borromean range proof | | `script` | bytes | variable (max 1024) | Locking script (P2PKH, etc.) | | `token_data` | int | 1 B | Token index (same semantics as `TxOutput.token_data`) | -| `ephemeral_pubkey` | bytes | 33 B | Compressed secp256k1 point for ECDH recovery | - -**Typical total size**: ~770 bytes per output (with P2PKH script). +| `ephemeral_pubkey` | bytes \| None | 33 B | Compressed secp256k1 point for ECDH recovery (all-zero on the wire = not present) | ### FullShieldedOutput @@ -449,26 +630,86 @@ Hides both amount and token type. | Field | Type | Size | Description | |-------|------|------|-------------| -| `commitment` | bytes | 33 B | Pedersen commitment `C = v*A + r*G` (uses blinded generator) | -| `range_proof` | bytes | ~675 B (max 1024) | Bulletproof range proof | +| `commitment` | bytes | 33 B | Pedersen commitment (uses the blinded asset generator) | +| `range_proof` | bytes | ~3213 B (max 3328) | Borromean range proof | | `script` | bytes | variable (max 1024) | Locking script | | `asset_commitment` | bytes | 33 B | Blinded asset tag `A = H_token + r_asset*G` | -| `surjection_proof` | bytes | ~130 B (max 4096) | Asset surjection proof | -| `ephemeral_pubkey` | bytes | 33 B | Compressed secp256k1 point for ECDH recovery | +| `surjection_proof` | bytes | variable (max 4096) | Asset surjection proof | +| `ephemeral_pubkey` | bytes \| None | 33 B | Compressed secp256k1 point for ECDH recovery (all-zero = not present) | + +**Typical total size**: ~3.3 KB per shielded output, dominated by the range proof. -**Typical total size**: ~930 bytes per output (with P2PKH script, 3-input surjection domain). +### Size Constants + +| Constant | Value | Location | +|---|---|---| +| `COMMITMENT_SIZE` | 33 | `shielded_tx_output.py` | +| `ASSET_COMMITMENT_SIZE` | 33 | `shielded_tx_output.py` | +| `EPHEMERAL_PUBKEY_SIZE` | 33 | `shielded_tx_output.py` | +| `MAX_RANGE_PROOF_SIZE` | 3328 | `shielded_tx_output.py` | +| `MAX_SURJECTION_PROOF_SIZE` | 4096 | `shielded_tx_output.py` | +| `MAX_SHIELDED_OUTPUTS` | 32 | `shielded_tx_output.py` | +| `MAX_SHIELDED_OUTPUT_SCRIPT_SIZE` | 1024 | `shielded_tx_output.py` | +| `EXCESS_BLINDING_FACTOR_SIZE` | 32 | `unshield_balance_header.py` | +| `MAX_MINT_MELT_ENTRIES` | 16 | `mint_melt_header.py` | +| `AMOUNT_SIZE` (mint/melt) | 8 (u64 BE) | `mint_melt_header.py` | + +### Vertex Header IDs + +All headers are ordered ascending by ID within a transaction (`hathor/transaction/headers/types.py`): + +| ID | Header | +|---|---| +| `0x10` | `NANO_HEADER` | +| `0x11` | `FEE_HEADER` | +| `0x12` | `SHIELDED_OUTPUTS_HEADER` | +| `0x13` | `UNSHIELD_BALANCE_HEADER` | +| `0x14` | `MINT_HEADER` | +| `0x15` | `MELT_HEADER` | + +`get_maximum_number_of_headers()` returns **5** when `ENABLE_SHIELDED_TRANSACTIONS` is active (otherwise 3), allowing `FeeHeader + (ShieldedOutputs | UnshieldBalance) + MintHeader + MeltHeader` with one slot of margin for `NanoHeader`. ### ShieldedOutputsHeader -Shielded outputs are carried in a transaction header, not in the standard `tx.outputs` list. +Carries the shielded outputs; not part of the standard `tx.outputs` list. | Field | Size | Description | |-------|------|-------------| -| Header ID | 1 B | `0x12` (`VertexHeaderId.SHIELDED_OUTPUTS_HEADER`) | -| `num_outputs` | 1 B | Number of shielded outputs (max 32) | +| Header ID | 1 B | `0x12` | +| `num_outputs` | 1 B | Number of shielded outputs, `1 ≤ n ≤ 32` | | Outputs | variable | Concatenated serialized outputs | -**Maximum shielded outputs per transaction**: `MAX_SHIELDED_OUTPUTS = 32`. +There is **no** binding signature or excess-point trailer on this header. + +### UnshieldBalanceHeader + +Carries the balance residual for full-unshield transactions. + +| Field | Size | Description | +|-------|------|-------------| +| Header ID | 1 B | `0x13` | +| `excess_blinding_factor` | 32 B | Scalar `excess = sum(r_in) − sum(r_out)` | + +Total: 33 bytes. It is a **scalar**, not a point and not a Schnorr signature. The whole serialization is bound into the sighash, so mutating the scalar invalidates all signatures. Mutually exclusive with `ShieldedOutputsHeader`. + +### MintHeader / MeltHeader + +Publicly declare per-token supply changes (`hathorlib/headers/mint_melt_header.py`). + +| Field | Size | Description | +|-------|------|-------------| +| Header ID | 1 B | `0x14` (mint) / `0x15` (melt) | +| `num_entries` | 1 B | `1 ≤ n ≤ 16` | +| Entries | variable | Concatenated entries | + +Each entry (`MintMeltEntry`): + +| Field | Size | Description | +|-------|------|-------------| +| `token_index` | 1 B | `1 ≤ token_index ≤ 16` (validated against `len(tx.tokens)` during verification; HTR/index 0 forbidden) | +| `amount` | 8 B BE | Public amount, `1 ≤ amount < 2^64` | + +Deserialization rejects duplicate `token_index` within a header, out-of-range indices, and zero amounts. ### Wire Format @@ -481,200 +722,236 @@ FullShieldedOutput serialization: mode(1B=0x02) | commitment(33B) | rp_len(2B BE) | range_proof(var) | script_len(2B BE) | script(var) | asset_commitment(33B) | sp_len(2B BE) | surjection_proof(var) | ephemeral_pubkey(33B) + +ShieldedOutputsHeader: + 0x12 | num_outputs(1B) | | | ... + +UnshieldBalanceHeader: + 0x13 | excess_blinding_factor(32B) + +MintHeader / MeltHeader: + 0x14|0x15 | num_entries(1B) | (token_index(1B) | amount(8B BE)) ... + +Single-entry MintHeader for 100,000 of token at index 1: + 0x14 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x01 0x86 0xA0 ``` -The `mode` byte discriminates output types during deserialization. Length fields use big-endian unsigned 16-bit integers (`!H` struct format). +The `mode` byte discriminates output types during deserialization. Length fields are big-endian unsigned 16-bit integers (`!H`). `ephemeral_pubkey` is always written as 33 bytes; all-zeros encodes "not present". ### Sighash Coverage -The transaction sighash includes: `mode`, `commitment`, `script`, `token_data` (amount-shielded) or `asset_commitment` (full-shielded), and `ephemeral_pubkey`. It **excludes** `range_proof` and `surjection_proof` — these are verified independently and do not affect the spending signature. +| Structure | Included in sighash | Excluded | +|---|---|---| +| `AmountShieldedOutput` | mode, commitment, token_data, script, ephemeral_pubkey | range_proof | +| `FullShieldedOutput` | mode, commitment, asset_commitment, script, ephemeral_pubkey | range_proof, surjection_proof | +| `ShieldedOutputsHeader` | header_id, num_outputs, per-output sighash bytes | — | +| `UnshieldBalanceHeader` | full serialization | — | +| `MintHeader` / `MeltHeader` | full serialization | — | + +Range and surjection proofs are excluded because they are verified independently and do not affect the spending signature; `ephemeral_pubkey` is always included to prevent a malleability attack that strips it. ## 4.3 Transaction Rules -Seven rules govern shielded transactions: +The following rules govern shielded transactions. Each names the enforcing function in `hathor/verification/transaction_verifier.py` and the exception raised. -### Rule 1: Minimum Structure +### Rule 1: Commitment / structure validity -At least one input (or Nano Contract withdraw) and at least one output (transparent or shielded, or Nano Contract deposit) required. Standard transaction structure rules apply. +`verify_commitments_valid` — every commitment, asset commitment, and (present) ephemeral pubkey is a valid 33-byte secp256k1 point, and the output count is within `MAX_SHIELDED_OUTPUTS`. Raises `InvalidShieldedOutputError`. Standard transaction structure rules (≥1 input, ≥1 output) continue to apply. ### Rule 2: Fee Is Always Transparent -The transaction fee is always a plaintext HTR amount, declared in a `FeeHeader`. The fee enters the balance equation as a trivial commitment: `C_fee = fee * H_HTR`. - -### Rule 3: Blinding Factors Must Balance +`verify_shielded_fee` — a shielded transaction MUST carry a `FeeHeader`. Fees enter the balance equation as trivial commitments. Raises `InvalidShieldedOutputError`. -``` -sum(r_input_shielded) = sum(r_output_shielded) -``` +### Rule 3: Range Proofs -Transparent inputs and outputs contribute `r = 0`. The wallet enforces this by choosing the last shielded output's blinding factor as the balancing residual. For `FullShieldedOutput`, asset blinding factors must also balance: `sum(s_inputs) = sum(s_outputs)`. +`verify_range_proofs` — every shielded output MUST include a valid range proof for `[1, 2^40)` against its commitment and generator (unblinded for `AmountShieldedOutput`, blinded for `FullShieldedOutput`). Raises `InvalidRangeProofError`. ### Rule 4: Trivial Commitment Protection -If **all** inputs are transparent, at least 2 shielded outputs are required. - -**Rationale**: With all transparent inputs, the total input amount is public. A single shielded output with no transparent outputs would have its blinding factor forced to zero (to satisfy Rule 3), making the commitment trivially deducible as `C = (total_input - fee) * H`. +`verify_trivial_commitment_protection` — if a transaction has **any** shielded outputs, it MUST have **at least 2**. Raises `TrivialCommitmentError`. -**Exception**: This rule is relaxed if any input is shielded (the input's non-zero blinding factor provides the necessary entropy). It also does not apply if there is a transparent output alongside the single shielded output. +> This is stricter than earlier drafts, which relaxed the rule when an input was +> shielded. The implementation enforces ≥2 unconditionally (a conservative, +> storage-free check) whenever shielded outputs are present. -### Rule 5: Range Proofs +### Rule 5: Homomorphic Balance -Every shielded output MUST include a valid Bulletproof range proof proving the committed amount is in `[1, 2^64)`. The minimum value of 1 (not 0) prevents zero-amount outputs. +`verify_shielded_balance` — the balance equation of [§4.1](#41-cryptographic-primitives) holds (augmented per Rule M4 when mint/melt headers are present). Also enforces the mutual-exclusion invariants on `UnshieldBalanceHeader`. Raises `ShieldedBalanceMismatchError`. ### Rule 6: Surjection Proofs -Every `FullShieldedOutput` MUST include a valid asset surjection proof proving its asset commitment corresponds to one of the input asset commitments. `AmountShieldedOutput` does not require a surjection proof (its token is visible via `token_data`). Transparent inputs contribute their unblinded NUMS asset tag to the surjection proof domain. +`verify_surjection_proofs` — every `FullShieldedOutput` MUST include a valid surjection proof whose domain is `(transparent inputs ∪ shielded inputs ∪ MintHeader tokens)`. `AmountShieldedOutput` needs none (its token is visible via `token_data`). Raises `InvalidSurjectionProofError`. ### Rule 7: Authority Outputs Remain Transparent -Mint and melt authority outputs MUST always be transparent `TxOutput`s. Attempting to set authority bits on a shielded output is invalid (`ShieldedAuthorityError`). Authority tokens control token supply and must remain auditable. +`verify_authority_restriction` — no shielded output may carry authority bits. `AmountShieldedOutput` with `token_data & TOKEN_AUTHORITY_MASK` set is rejected; `FullShieldedOutput` has no `token_data` field and cannot encode authority. Raises `ShieldedAuthorityError`. -### Rule 8: Mint/Melt Transactions Cannot Have Shielded Outputs +### Rule M1: Mint/Melt requires a shielded transaction -A transaction that contains any mint or melt operation (i.e., spends a mint or melt authority input) MUST NOT include any shielded outputs (`ShieldedMintMeltForbiddenError`). All value outputs in a mint/melt transaction must be transparent. +`verify_mint_melt_requires_shielded` — `MintHeader`/`MeltHeader` are valid only when the transaction carries a `ShieldedOutputsHeader` or an `UnshieldBalanceHeader`. Raises `ShieldedMintMeltForbiddenError`. -**Rationale**: Keeping mint/melt transactions fully transparent ensures that token supply remains publicly auditable. Explorers and users can always verify the total circulating supply of any custom token by summing its mint and melt operations. Users who want amount privacy can move minted tokens into shielded outputs in a subsequent transaction. +### Rule M2: Authority input required per entry -## 4.4 Verification Pipeline +`verify_mint_melt_authority_inputs` — each `MintHeader` entry requires a matching transparent mint authority input; each `MeltHeader` entry requires a matching melt authority input. (A `TokenCreationTransaction` is exempt for `token_index = 1`, which it implicitly authorizes.) Raises `ForbiddenMint` / `ForbiddenMelt`. + +### Rule M3: One direction per token -Verification is split into two phases, matching Hathor's existing architecture. +`verify_mint_melt_headers_well_formed` — a `token_index` appearing in `MintHeader` MUST NOT also appear in `MeltHeader`. Also bounds each `token_index` against `len(tx.tokens)`. Raises `InvalidMintMeltHeaderError`. -### Phase 1: Without Storage (Basic Verification) +### Rule M4: Augmented homomorphic balance -Called during `verify_without_storage`. No UTXO lookups needed. +`verify_shielded_balance` + `_fold_mint_melt_entry`. The balance equation becomes: ``` -verify_shielded_outputs() - |-- verify_commitments_valid() - | Checks: all commitments are 33-byte valid secp256k1 points - | Checks: asset_commitments (FullShielded) are valid points - | Checks: ephemeral_pubkeys are valid secp256k1 points - | - |-- verify_authority_restriction() [Rule 7] - | Checks: no shielded output has authority bits set - | - |-- verify_range_proofs() [Rule 5] - | Checks: each shielded output's Bulletproof verifies against - | its commitment and generator (unblinded for Amount, - | blinded for Full) - | - |-- verify_trivial_commitment_protection() [Rule 4, conservative] - | Checks: at least 2 shielded outputs (relaxed with storage) - | - |-- verify_shielded_fee() - Checks: FeeHeader exists - Checks: total_declared_fee >= shielded_fee (lower bound) +sum(C_in) + sum_T(mint_T · H_T) + withdraw · H_HTR + == sum(C_out) + sum_T(melt_T · H_T) + deposit · H_HTR + fee · H_HTR + excess · G ``` -### Phase 2: With Storage (Full Verification) +- Each `MintHeader` entry `(T, amount)` adds `amount · H_T` to the **input** side (unblinded). +- Each `MeltHeader` entry `(T, amount)` adds `amount · H_T` to the **output** side (unblinded). +- For `DEPOSIT`-version tokens: mint adds a `deposit = 0.01 × amount` HTR term on the output side (paid by the user); melt adds a `withdraw = 0.01 × amount` HTR term on the input side (returned to the user). Computed via `get_deposit_token_deposit_amount` / `get_deposit_token_withdraw_amount`. +- For `FEE`-version tokens: each entry (mint *or* melt) adds one `FEE_PER_OUTPUT` HTR term on the output side. These per-entry charges are folded into the balance equation, **not** declared in `FeeHeader`. -Called during `verify` / `_verify_shielded_header`. Requires UTXO lookups to resolve input types. +Raises `ShieldedBalanceMismatchError`. -``` -_verify_shielded_header() - |-- verify_surjection_proofs() [Rule 6] - | Builds surjection domain from input asset commitments: - | - Transparent inputs: derive_asset_tag(token_uid) - | - Shielded inputs: use on-chain asset_commitment - | Verifies each FullShieldedOutput's proof against the domain - | - |-- verify_shielded_balance() - | Collects all input commitments (shielded: direct, transparent: trivial) - | Collects all output commitments (shielded: direct, transparent: trivial) - | Appends fee entries as transparent outputs - | Verifies: sum(inputs) == sum(outputs) - | - |-- _verify_trivial_commitment_with_storage() [Rule 4, relaxed] - If any input is shielded: allow 1 shielded output - Otherwise: require >= 2 shielded outputs +### Rule M5: No undeclared mint/melt -verify_token_rules(shielded_fee=X) [Fee exact match] - Existing fee verification, augmented with shielded_fee addend - Checks: standard_fee + shielded_fee == fees_from_fee_header (exact) -``` +`verify_no_undeclared_mint_melt` — on a shielded transaction, any non-`NATIVE` token showing a transparent surplus (mint) or deficit (melt) in the token accounting MUST be covered by a corresponding `MintHeader`/`MeltHeader` entry. Without a public scalar, the prover could mint from nothing. Raises `ShieldedMintMeltForbiddenError`. -### Token UID Normalization +### Rule M6: Mint/Melt vs. NanoHeader -The `_normalize_token_uid()` function handles the mismatch between Hathor's internal 1-byte HTR token UID (`b'\x00'`) and the crypto library's 32-byte requirement. HTR is padded with 31 zero bytes; custom tokens (already 32 bytes) pass through unchanged. +`verify_mint_melt_nano_compatibility` — a `NanoHeader` may coexist with mint/melt headers, but a single token MUST NOT have its supply changed through both a NanoHeader action and a `MintHeader`/`MeltHeader` entry (this would double-count in the balance equation). Raises `InvalidMintMeltHeaderError`. -## 4.5 Fee Mechanism +### TokenCreationTransaction rules -### Fee Calculation +`verify_minted_tokens` (token creation verifier): a shielded TCT MUST declare initial supply via exactly one `MintHeader` entry for `token_index = 1`, and MUST NOT melt that token. Non-shielded TCTs keep the existing `token_info.amount > 0` check. Raises `InvalidToken`. `MeltHeader` is not permitted on a token creation transaction. -```python -shielded_fee = (n_amount_shielded * FEE_PER_AMOUNT_SHIELDED_OUTPUT) - + (n_full_shielded * FEE_PER_FULL_SHIELDED_OUTPUT) -``` +## 4.4 Verification Pipeline + +Verification follows Hathor's two-phase architecture. -Settings in `HathorSettings`: +### Phase 1: Without Storage -| Setting | Default | Description | -|---------|---------|-------------| -| `FEE_PER_AMOUNT_SHIELDED_OUTPUT` | 1 | HTR base units per `AmountShieldedOutput` | -| `FEE_PER_FULL_SHIELDED_OUTPUT` | 2 | HTR base units per `FullShieldedOutput` | +`verify_shielded_outputs` (no UTXO lookups): -These settings are gated by `ENABLE_SHIELDED_TRANSACTIONS`. +``` +verify_shielded_outputs(tx) + |-- verify_commitments_valid [Rule 1] + |-- verify_authority_restriction [Rule 7] + |-- verify_range_proofs [Rule 3] + |-- verify_trivial_commitment_protection[Rule 4] + |-- verify_shielded_fee [Rule 2, lower bound] + +verify_mint_melt_basic(tx) (only if a mint/melt header is present) + |-- verify_mint_melt_headers_well_formed[Rule M3 + bounds] + |-- verify_mint_melt_requires_shielded [Rule M1] + |-- verify_mint_melt_nano_compatibility [Rule M6] +``` -### FeeHeader Integration +### Phase 2: With Storage -Fees are declared in the existing `FeeHeader` mechanism. The `FeeHeader` entries are treated as transparent outputs in the homomorphic balance equation: +Requires UTXO lookups to resolve input types: ``` -sum(C_in) == sum(C_out) + sum(C_fee_entry) +verify_shielded_outputs_with_storage(tx) + |-- verify_surjection_proofs [Rule 6] + +(from _verify_tx, the shielded counterpart to transparent balance:) + |-- verify_no_undeclared_mint_melt [Rule M5] + |-- verify_mint_melt_authority_inputs [Rule M2] + |-- verify_token_rules(shielded_fee=X) [exact fee match] + |-- verify_shielded_balance [Rule 5 / Rule M4] ``` -Each `C_fee_entry = fee_amount * H_token` for the corresponding token. This means the balance verification function does not need a separate `fee` parameter — fees are simply part of the output side. +Shielded inputs are detected by index: a `TxInput` whose `index >= len(spent_tx.outputs)` references a shielded output at `index − len(spent_tx.outputs)` in the spent transaction's `ShieldedOutputsHeader`. + +## 4.5 Mint/Melt Reference +[45-mintmelt-reference]: #45-mintmelt-reference + +The mint/melt design lives entirely in the headers and the augmented balance equation; there is **no separate feature flag** — it is gated by `SHIELDED_TRANSACTIONS` along with the rest of shielded outputs. + +### Disclosure model + +Each `MintHeader`/`MeltHeader` entry discloses a **token reference** (`token_index`) and an **amount**, both in plaintext. + +- **Token reference: always transparent.** The mint/melt authority output is itself transparent (Rule 7), so the token UID is already exposed; and the verifier must read the token's version (`NATIVE`/`DEPOSIT`/`FEE`) to apply the right deposit-or-fee logic. +- **Amount: transparent.** Total token supply is publicly computable per token by summing entries across the chain. Plaintext amounts are required for `DEPOSIT`-version tokens because the HTR deposit is `0.01 × amount` and must be verifiable. (Shielded amounts — a Pedersen commitment + range proof in the header — are a deferred business decision; see [Future possibilities](#future-possibilities).) + +### Surjection-domain extension + +For each `(T, amount)` in `MintHeader`, the unblinded NUMS asset tag `H_T = derive_asset_tag(T)` is appended to the surjection-proof domain so a `FullShieldedOutput` of a freshly-minted token can prove its asset is in `(transparent inputs ∪ shielded inputs ∪ minted tokens)`. `MeltHeader` does NOT extend the domain (melt produces no new outputs of the melted token). + +### Header order + +`MintHeader` (`0x14`) precedes `MeltHeader` (`0x15`) by the ascending-ID rule. Both follow `ShieldedOutputsHeader`/`UnshieldBalanceHeader`. + +## 4.6 Fee Mechanism + +### Settings + +| Setting | Default | Location | Description | +|---------|---------|----------|-------------| +| `FEE_PER_AMOUNT_SHIELDED_OUTPUT` | 1 | `hathor/conf/settings.py` | HTR base units per `AmountShieldedOutput` | +| `FEE_PER_FULL_SHIELDED_OUTPUT` | 2 | `hathor/conf/settings.py` | HTR base units per `FullShieldedOutput` | +| `FEE_PER_OUTPUT` | 1 | `hathorlib` settings | HTR per chargeable transparent output and per `FEE`-token mint/melt entry | + +### Calculation + +```python +shielded_fee = (n_amount_shielded * FEE_PER_AMOUNT_SHIELDED_OUTPUT) + + (n_full_shielded * FEE_PER_FULL_SHIELDED_OUTPUT) +``` ### Two-Phase Fee Verification -1. **Without storage (lower bound)**: `total_declared_fee >= shielded_fee`. Cannot compute exact expected fee without storage (standard token fees depend on `chargeable_outputs` which requires UTXO lookups). -2. **With storage (exact match)**: `standard_fee + shielded_fee == fees_from_fee_header`. Both over-payment and under-payment are rejected. +1. **Without storage (lower bound)**: `total_declared_fee >= shielded_fee` (`verify_shielded_fee`). The exact standard-token fee depends on chargeable outputs, which need UTXO lookups. +2. **With storage (exact match)**: `verify_token_rules(..., shielded_fee=X)` requires `standard_fee + shielded_fee == fees_from_fee_header`, and `verify_shielded_balance` reconciles the fee entries inside the balance equation. Both over- and under-payment are rejected. + +Shielded outputs are not counted in `chargeable_outputs` for standard token fee math; to prevent fee avoidance (shielding an output to dodge `FEE_PER_OUTPUT`), the shielded fee rates are set at least as large as the standard per-output fee. -### Shielded Fees Subsume Token Fees +## 4.7 Network-Wide Supply Audit +[47-network-wide-supply-audit]: #47-network-wide-supply-audit -Shielded outputs are not counted in `chargeable_outputs` for standard FEE-versioned token fee calculation. To prevent fee avoidance (shielding a token output to dodge `FEE_PER_OUTPUT`), the shielded fee rates are configured to be at least as large as the standard token output fee. +A monitor with chain access can verify, for each token, that no value was created or destroyed across the entire shielded history — **with no protocol change**: -## 4.6 ECDH Recovery Mechanism +``` +Σ C_utxo == Σ_token (total_supply_token · H_token) +``` + +This holds because the per-tx balance check is a curve-point equality (`Σ C_in == Σ C_out + fee·H_HTR`, augmented by the public mint/melt scalars), which by NUMS generator independence forces `Σ r_in = Σ r_out` for every shielded tx. Telescoping over the chain gives `Σ r_utxo = 0`, collapsing the residual `G`-component to zero. `total_supply_token` is maintained from the public mint/melt declarations (and genesis allocation for HTR). -### Overview +A regression test (`hathor_tests/tx/test_shielded_audit_equation.py`) builds small chains using `htr_lib.shielded` directly and checks the equation; the full derivation is in the design memo at `_designs/03-amount-privacy/100-NETWORK-SUPPLY-AUDIT.md`. Inflation-bug monitoring can therefore ship against the current chain immediately. -Every shielded output contains an `ephemeral_pubkey` field (33 bytes). This enables the recipient to recover the committed value without any out-of-band communication. +## 4.8 ECDH Recovery Mechanism ### Sender Flow -1. Generate ephemeral keypair: `(e, E = e*G)` on secp256k1. -2. Obtain recipient's scan public key `P = B_scan` (from the shielded address). -3. Compute shared secret: `s = SHA256(e * P)`. -4. Derive deterministic nonce: `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. -5. Create range proof using `nonce` as the nonce key (not random). -6. For `FullShieldedOutput`: embed `token_uid(32B) || asset_blinding_factor(32B)` in the range proof message. -7. Store `E` (33 bytes, compressed) in the shielded output's `ephemeral_pubkey` field. +1. Generate ephemeral keypair `(e, E = e*G)` on secp256k1 (`generate_ephemeral_keypair`). +2. Obtain the recipient's scan public key `P = B_scan`. +3. Compute shared secret `s = ECDH(e, P)` (secp256k1 `SharedSecret`, i.e. SHA256 over the shared point). +4. Derive deterministic nonce `nonce = SHA256("Hathor_CT_nonce_v1" || s)` (re-hashed with a counter in the ~2⁻¹²⁸ case where the result is not a valid scalar). +5. Create the range proof using `nonce` as the proof's nonce key (not random), enabling rewind. +6. For `FullShieldedOutput`: embed `token_uid(32B) || asset_blinding_factor(32B)` (64-byte message) in the range proof. +7. Store `E` (33 bytes, compressed) in the output's `ephemeral_pubkey` field. ### Recipient Flow -1. Parse output script; check if script contains `hash(spend_pubkey)` matching a wallet key. -2. Extract ephemeral pubkey `E` from the shielded output. -3. Compute shared secret: `s = SHA256(scan_privkey * E)` (same result since `scan_privkey*E = scan_privkey*e*G = e*scan_privkey*G = e*B_scan`). -4. Derive nonce: `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. -5. Call `rewind_range_proof(proof, commitment, nonce, generator)`: - - `generator` = `derive_asset_tag(token_uid)` for `AmountShieldedOutput` - - `generator` = `output.asset_commitment` for `FullShieldedOutput` -6. Returns: `(value, blinding_factor, message)`. -7. For `FullShieldedOutput`: extract `token_uid` and `asset_blinding_factor` from `message`, reconstruct expected asset commitment, and cross-check against on-chain value. +1. Match the output script against a wallet key. +2. Extract `E`; compute `s = ECDH(scan_privkey, E)` (equal to the sender's `s`). +3. Derive `nonce = SHA256("Hathor_CT_nonce_v1" || s)`. +4. Call `rewind_range_proof(proof, commitment, nonce, generator)` with `generator = derive_asset_tag(token_uid)` for `AmountShieldedOutput` or `output.asset_commitment` for `FullShieldedOutput`. +5. Receive `(value, blinding_factor, message)`. +6. For `FullShieldedOutput`: extract `token_uid` and `asset_blinding_factor` from `message`, recompute the expected asset commitment, and cross-check it against the on-chain `asset_commitment` (rejects a fraudulent token UID). ### Security Properties -- **Sighash binding**: The ephemeral pubkey is included in the transaction sighash, preventing MITM replacement. -- **Nonce uniqueness**: Each output uses a fresh ephemeral keypair. -- **Failed rewind**: Returns an error (not garbage) when the nonce is wrong — no false positives. -- **Forward secrecy**: Ephemeral keys are single-use. -- **Domain separation**: `"Hathor_CT_nonce_v1"` prefix isolates this derivation from other uses of the ECDH shared secret. -- **Token UID cross-check**: For `FullShieldedOutput`, the recovered `token_uid` is verified against the `asset_commitment` to prevent a malicious sender from embedding a fraudulent token UID. -- **No value logging**: Recovered amounts are never logged, even at DEBUG level. +- **Sighash binding**: `ephemeral_pubkey` is in the sighash, preventing MITM replacement. +- **Failed rewind**: returns an error (not garbage) on the wrong nonce — no false positives. +- **Forward secrecy / nonce uniqueness**: ephemeral keys are single-use. +- **Domain separation**: `"Hathor_CT_nonce_v1"` isolates this derivation; `"Hathor_AssetTag_v1"` isolates asset-tag derivation. +- **Token UID cross-check**: for `FullShieldedOutput`, the recovered UID is bound to the `asset_commitment`. -## 4.7 Feature Activation - -### Feature Flag +## 4.9 Feature Activation ```python # hathor/feature_activation/feature.py @@ -682,203 +959,162 @@ class Feature(StrEnum): SHIELDED_TRANSACTIONS = 'SHIELDED_TRANSACTIONS' ``` -### Settings - ```python # hathor/conf/settings.py ENABLE_SHIELDED_TRANSACTIONS: FeatureSetting = FeatureSetting.DISABLED ``` -`FeatureSetting` is an enum with values: `DISABLED`, `ENABLED`, `FEATURE_ACTIVATION`. - -### Crypto Library Availability +`FeatureSetting` is an enum with values `DISABLED`, `ENABLED`, `FEATURE_ACTIVATION`. A single flag gates **all** shielded functionality, including mint/melt headers — there is no separate `SHIELDED_MINT_MELT` flag. The vertex parser only parses shielded headers when the flag is active, and `get_maximum_number_of_headers()` is raised from 3 to 5 under the same gate. -At startup, if `ENABLE_SHIELDED_TRANSACTIONS != DISABLED`, the system validates that the native `hathor_ct_crypto` library is available via `validate_shielded_crypto_available()`. This prevents silent failures where all shielded output operations would fail at runtime. +The native `htr_lib` crypto extension must be available for shielded operations to function; it is imported by the `hathorlib` wrappers at runtime. -Build command: -```bash -poetry run maturin develop --manifest-path hathor-ct-crypto/Cargo.toml --features python -``` - -## 4.8 Interaction with Other Features +## 4.10 Interaction with Other Features ### Silent Payments (Phase A) -The `ephemeral_pubkey` in shielded outputs serves double duty when Silent Payments are active: - -- **Without SP**: The ephemeral key derives blinding factors only. The recipient address is visible in the script. -- **With SP**: The same ECDH shared secret derives both the one-time recipient address and the blinding factors. A single ECDH operation provides recipient privacy + blinding factor communication. - -Combined: a shielded output hides the recipient (one-time address), the amount (Pedersen commitment), and optionally the token type (blinded asset tag). +The `ephemeral_pubkey` serves double duty: without SP it derives blinding factors only (the recipient address is visible in the script); with SP the same ECDH shared secret derives both a one-time recipient address and the blinding factors. A single ECDH operation then provides recipient privacy + blinding-factor communication. ### Ring Signatures / Nullifiers (Phase C) -Shielded outputs simplify decoy selection for ring signatures: - -- **Without shielded outputs**: Decoys must match on amount (otherwise amount mismatch reveals the real input). This limits the anonymity set. -- **With shielded outputs**: All outputs are opaque commitments — any shielded output is a valid decoy regardless of hidden amount or token. Larger anonymity sets with simpler selection logic. - -Surjection proofs naturally compose with ring signatures: the surjection domain includes all ring members (real + decoys), hiding which input is real. +All shielded outputs are opaque commitments, so any shielded output is a valid decoy regardless of hidden amount or token — larger anonymity sets with simpler selection. Surjection proofs compose naturally: the domain can include ring members (real + decoys). ### Nano Contracts -The `nc_caller` field in Nano Contract transactions identifies the calling address. Shielded outputs do not affect `nc_caller` — it remains a standard address. However, if Nano Contracts consume or produce shielded outputs in the future, the balance equation and proof generation would need to account for the contract's logic. This is out of scope for this RFC. +A `NanoHeader` may coexist with mint/melt headers, subject to Rule M6 (no same-token supply change via both channels). Deeper interaction — contracts consuming or producing shielded outputs — is out of scope for this RFC. # Drawbacks [drawbacks]: #drawbacks -1. **Transaction size increase.** A shielded output is ~770-930 bytes vs ~40 bytes for a transparent output (~20-25x larger). This increases storage, bandwidth, and propagation time. However, Hathor's logarithmic weight formula means the fee increase is moderate (not proportional to the size increase). +1. **Transaction size increase.** A shielded output is ~3.3 KB (dominated by the ~3213-byte Borromean range proof) vs ~40 bytes for a transparent output — roughly two orders of magnitude larger. This increases storage, bandwidth, and propagation time. The range proof is the single largest contributor; a Bulletproof-based proof (~675 B) would shrink this substantially but is not available in the current `secp256k1-zkp` build. + +2. **Verification cost.** Each range proof and each surjection proof is verified independently. Batch verification is not yet truly batched (the helper loops sequentially), so multi-output transactions pay close to linear cost. + +3. **Wallet complexity.** Wallets must implement ECDH key exchange, range-proof rewind, blinding-factor management and balancing, surjection-proof generation, and fee computation — a substantial increase over transparent-only transactions. -2. **Verification cost.** Bulletproof range proof verification takes ~1ms per proof. For a transaction with 4 shielded outputs, this adds ~4ms of CPU time per transaction. Batch verification and parallelization can amortize this, but it remains significantly more expensive than transparent output verification. +4. **Explorer capability reduction.** Explorers lose amount/balance display for shielded outputs (though per-token supply remains auditable from the mint/melt headers). -3. **Wallet complexity.** Wallets must implement ECDH key exchange, range proof rewind, blinding factor management, surjection proof generation, and fee computation. This is a substantial increase in wallet complexity compared to transparent-only transactions. +5. **Recipient public key requirement.** The sender must know the recipient's full compressed public key (not just the address hash) to establish the ECDH shared secret. This requires a prior on-chain spend, a payment protocol, or a new address format. -4. **Explorer capability reduction.** Block explorers lose the ability to display amounts and compute balances for shielded outputs. This fundamentally changes the user experience of public blockchain explorers, which are a key tool for transparency and debugging. +6. **Full-unshield scalar leak.** `UnshieldBalanceHeader` exposes `Σ r_in` as a raw scalar. A point-form replacement with a binding signature would close this but is not implemented (see Future possibilities). -5. **Recipient public key requirement.** The sender must know the recipient's full compressed public key (not just the address hash) to establish the ECDH shared secret. This requires either a prior on-chain spend, a payment protocol, or a new address format. +7. **40-bit value ceiling.** Range proofs prove `[1, 2^40)`. This is above the maximum HTR supply but is a smaller domain than the full `u64` amount space; any token whose supply could exceed `2^40 − 1` base units cannot be fully represented in a shielded output. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives ## Why header-based (not a new TxVersion)? -A new `TxVersion` would require changes throughout the codebase — every `match vertex.version:` statement, serialization logic, feature gating, and verification path. The header-based approach reuses Hathor's existing header infrastructure (already used by `NanoHeader` and `FeeHeader`), attaching shielded outputs to standard transactions (`REGULAR_TRANSACTION` and `TOKEN_CREATION_TRANSACTION`) with zero structural changes. This means existing transaction processing, mempool logic, and DAG management continue to work unmodified. +A new `TxVersion` would require changes throughout the codebase — every `match vertex.version:`, serialization path, feature gate, and verification path. The header-based approach reuses Hathor's existing header infrastructure (already used by `NanoHeader` and `FeeHeader`), attaching shielded data to standard transactions with zero structural changes, so existing mempool logic and DAG management continue to work unmodified. ## Why two output types (not one)? -`AmountShieldedOutput` (amount hidden, token visible) is substantially smaller and cheaper to verify than `FullShieldedOutput` (both hidden). Many use cases only need amount privacy — e.g., hiding salaries paid in HTR, where the fact that the token is HTR is not sensitive. Offering both tiers lets users pay only for the privacy they need. +`AmountShieldedOutput` (amount hidden, token visible) skips the surjection proof and so is smaller and cheaper to verify than `FullShieldedOutput`. Many use cases only need amount privacy (e.g. salaries paid in HTR), so offering both tiers lets users pay only for the privacy they need. -## Why separate Bulletproofs (not aggregated)? +## Why separate range proofs (not aggregated)? -Aggregated Bulletproofs would require the prover to know all values and blinding factors for every output in the transaction. This is fundamentally incompatible with atomic swaps and multi-party transactions where each party independently constructs their outputs. The MPC workaround from Bünz et al. §4.3 is not implemented in `secp256k1-zkp` v0.11. Separate proofs also avoid permanently linking all outputs as created by the same party, and allow UTXO pruning. Performance is recovered through batch verification. +Aggregated proofs would require the prover to know all values and blinding factors for every output, which is incompatible with atomic swaps and multi-party transactions where each party independently constructs their outputs. Separate proofs also avoid permanently linking all outputs as same-party and allow independent UTXO pruning. + +## Why Borromean range proofs (not Bulletproofs)? + +The implementation uses `secp256k1-zkp`'s `RangeProof` (a Borromean-style proof). It is the proof type exposed by the library version in use, battle-tested in production (Liquid/Elements), and supports the ECDH-rewind recovery mechanism directly. The cost is size: ~3213 bytes vs a Bulletproof's ~675 bytes. Bit width is pinned to 40 so all proofs are constant-size, removing a value-size side-channel. Migrating to Bulletproofs is a future optimization gated on library support. ## Why secp256k1-zkp (not custom crypto)? -`secp256k1-zkp` is the industry-standard library for Confidential Transactions, maintained by Blockstream and used in production by Liquid/Elements. It provides battle-tested implementations of Pedersen commitments, Bulletproofs, and surjection proofs on the same secp256k1 curve Hathor already uses. Writing custom cryptographic primitives would be a security liability. +`secp256k1-zkp` is the industry-standard library for Confidential Transactions, maintained by Blockstream and used by Liquid/Elements. It provides battle-tested Pedersen commitments, range proofs, and surjection proofs on the same secp256k1 curve Hathor already uses. Writing custom cryptographic primitives would be a security liability. ## Why ECDH rewind (not encrypted messages)? -Range proof rewinding is an established technique (used by Monero, Grin, Elements) that embeds recovery data inside the proof itself, adding zero bytes to the transaction. The alternative — encrypting amount/blinding data in a separate field — would increase transaction size and add a new encryption scheme to audit. Rewinding also provides a natural authentication mechanism: only the correct ECDH shared secret produces valid rewind results. +Range-proof rewinding is an established technique (Monero, Grin, Elements) that embeds recovery data inside the proof itself, adding zero bytes to the transaction. Encrypting amount/blinding data in a separate field would increase size and add a new encryption scheme to audit. Rewinding also authenticates: only the correct ECDH shared secret produces valid rewind results. -## Alternative: ZK-SNARKs (Zcash model) +## Why declare mint/melt amounts publicly? + +Pedersen commitments cannot, on their own, distinguish "minted from nothing" from "received from an input". A public scalar declaring the mint binds it into the balance equation, preserving the no-inflation guarantee and keeping supply auditable. A purely private alternative (a ZK "I am minting Y ≤ my authority limit" proof) is an order of magnitude more complex and loses auditability. + +## Why a scalar excess (not a binding signature) today? -Zcash's Sapling/Orchard circuits use Groth16/Halo 2 proofs to hide amounts, token types, and the sender simultaneously. While more powerful (combining Phases B and C), this approach requires: -- Trusted setup (Groth16) or substantially larger proofs (Halo 2 ~1.8KB vs Bulletproof ~675B) -- A different curve (BLS12-381 or Pallas/Vesta), incompatible with Hathor's secp256k1 -- Complex circuit design and auditing -- Significantly longer proof generation time (~25-274ms vs ~2ms for Bulletproofs) +The implemented full-unshield path publishes the residual as a scalar in `UnshieldBalanceHeader`. The scalar form is functional and prevents inflation (the verifier checks balance directly against it). A point-form excess plus a Schnorr binding signature (Sapling-style) would additionally hide `Σ r_in` and enable receiver-chosen blindings and non-interactive multi-party construction — but those capabilities are not yet required, so the simpler scalar form ships first. See Future possibilities. -Hathor's phased approach achieves the same end-state with individually simpler, more auditable components. +## Alternative: ZK-SNARKs (Zcash model) + +Zcash's Sapling/Orchard circuits hide amounts, token types, and the sender in one proof. This requires a trusted setup (Groth16) or larger proofs (Halo 2 ~1.8 KB), a different curve (BLS12-381 or Pallas/Vesta) incompatible with secp256k1, complex circuit auditing, and far slower proving. Hathor's phased approach reaches a similar end-state with individually simpler, more auditable components. ## Alternative: Mimblewimble -Mimblewimble (used by Grin, Beam, Litecoin MWEB) provides amount hiding with transaction cut-through (pruning intermediate transactions). However: -- Mimblewimble requires interactive transaction construction (sender and receiver must communicate to build the transaction) -- It fundamentally changes the UTXO model and transaction structure -- It cannot support scripting or multi-asset transactions in their current form -- Hathor's DAG structure is incompatible with Mimblewimble's linear chain assumptions +Mimblewimble (Grin, Beam, Litecoin MWEB) provides amount hiding with cut-through, but requires interactive transaction construction, fundamentally changes the UTXO model, cannot support scripting or multi-asset transactions in its current form, and is incompatible with Hathor's DAG structure. ## Impact of not doing this -Without shielded outputs, all Hathor transactions remain fully transparent. Users who need amount or token privacy would have no on-chain option, limiting Hathor's utility for business payments, DeFi, and personal finance. Competitors with privacy features (Monero, Zcash, Litecoin MWEB, Liquid) would have a structural advantage for privacy-sensitive use cases. +Without shielded outputs, all Hathor transactions remain fully transparent, with no on-chain option for amount or token privacy — limiting Hathor's utility for business payments, DeFi, and personal finance, while competitors with privacy features hold a structural advantage. # Prior art [prior-art]: #prior-art ## Liquid / Elements Confidential Assets -The primary inspiration for this design. Liquid (Blockstream's Bitcoin sidechain) implements Confidential Transactions with Pedersen commitments, Bulletproofs, and asset surjection proofs on secp256k1. Hathor's implementation uses the same `secp256k1-zkp` library and the same cryptographic constructions. - -**Lessons learned:** -- Separate Bulletproofs per output are the practical choice (Liquid uses this). -- Asset surjection proofs are essential for multi-asset chains. -- ECDH-based range proof rewind works well for recovery. -- The two-output-type approach (amount-only vs. full) is a Hathor innovation not present in Liquid. - -## Monero (RingCT + Bulletproofs) - -Monero uses Pedersen commitments and Bulletproofs for amount hiding, combined with ring signatures for sender privacy. All transactions are mandatory CT. +The primary inspiration. Liquid implements Confidential Transactions with Pedersen commitments, range proofs, and asset surjection proofs on secp256k1. Hathor uses the same `secp256k1-zkp` library and constructions. Liquid also supports confidential asset issuance with a public asset_id and amount — directly analogous to `MintHeader`. The two-output-type split (amount-only vs. full) is a Hathor addition. -**Differences from Hathor:** -- Monero uses Ed25519, not secp256k1. -- Monero's CT is mandatory; Hathor's is opt-in per output. -- Monero combines amount hiding with sender privacy (ring signatures) in a single protocol; Hathor separates these into independent phases. -- Monero does not support custom tokens. +## Monero (RingCT) -**Lessons learned:** -- Bulletproof range proofs are production-proven at scale (Monero processes millions of CT transactions). -- ECDH-based recovery with deterministic nonces is the standard approach. -- Mandatory CT provides larger anonymity sets but increases chain size. +Monero uses Pedersen commitments and range proofs for amount hiding plus ring signatures for sender privacy; all transactions are mandatory CT. Differences: Ed25519 (not secp256k1), mandatory (not opt-in), combined amount+sender privacy, no custom tokens. Lessons: range-proof CT is production-proven at scale; ECDH recovery with deterministic nonces is the standard approach. ## Zcash (Sapling / Orchard) -Zcash uses ZK-SNARKs (Groth16 in Sapling, Halo 2 in Orchard) to provide full transaction privacy: hidden amounts, hidden token types, and hidden senders, all in a single proof. +Zcash uses ZK-SNARKs for full transaction privacy in a single proof, on BLS12-381 / Pallas-Vesta. Sapling's `valueBalance` field and binding signature are the closest analogs to a per-asset public delta on an otherwise-shielded transaction (the inspiration for the deferred point-form excess). Differences: different curves, more expensive proving, separate shielded pool. Lesson: opt-in privacy reduces the anonymity set, so shielded usage should be encouraged. -**Differences from Hathor:** -- Zcash uses different curves (BLS12-381, Pallas/Vesta). -- Zcash's proofs are more expensive to generate (~25ms Groth16, ~274ms Halo 2 vs ~2ms Bulletproof). -- Zcash requires circuit compilation and trusted setup (Groth16) or larger proofs (Halo 2). -- Zcash's shielded pool is separate from its transparent pool (different address types); Hathor mixes them freely. +## Grin / Beam (Mimblewimble) and Litecoin MWEB -**Lessons learned:** -- Opt-in privacy (Zcash's shielded pools) means most transactions remain transparent, reducing the effective anonymity set. Hathor should encourage shielded usage to grow the anonymity set. -- ZIP 317 fee structure (per-action fees) is a good model for incentive alignment. +Mimblewimble provides amount hiding with cut-through; MWEB adds opt-in CT via extension blocks with ECDH stealth addresses. Not adopted because of interactive construction, incompatibility with scripting/multi-asset, and incompatibility with Hathor's DAG. Mimblewimble's per-tx "kernel excess" (a point + Schnorr signature) is structurally the binding signature discussed under Future possibilities. -## Grin / Beam (Mimblewimble) - -Mimblewimble protocols use Pedersen commitments with transaction cut-through for amount hiding and chain compaction. +# Unresolved questions +[unresolved-questions]: #unresolved-questions -**Not adopted because:** Interactive transaction construction, incompatibility with scripting and multi-asset support, and incompatibility with Hathor's DAG structure. +1. **Shielded address format.** Option A (compact) vs Option B (full keys) vs Option C (spend-only). The tradeoff is address length vs. Silent Payments compatibility and light-client scanning. Should be resolved before wallet implementations begin. (Code currently assumes the full public key — Option B.) -## Litecoin MWEB +2. **Final fee amounts.** The placeholders (`FEE_PER_AMOUNT_SHIELDED_OUTPUT = 1`, `FEE_PER_FULL_SHIELDED_OUTPUT = 2`) are not final and should be set from measured verification-cost differentials and network economics. -Litecoin's MimbleWimble Extension Blocks (activated May 2022) add opt-in confidential transactions via a sidechain-like extension block. +3. **Batch verification.** The current `batch_verify_range_proofs` is a sequential loop. A true batched multi-exponentiation API would reduce CPU for multi-output transactions; timeline depends on library support. -**Relevant parallels:** -- Opt-in privacy (like Hathor). -- Shield/unshield operations at the boundary between transparent and MWEB pools. -- ECDH-based stealth addresses for recipient privacy. +4. **Light client scanning.** Full nodes verify all proofs, but light clients need an efficient protocol for detecting their own shielded payments without downloading every transaction (SP-style filters, server-side scanning, or compact summaries). -**Differences:** MWEB is a separate block structure with different consensus rules; Hathor integrates shielded outputs directly into the existing transaction format via headers. +5. **Shielded Nano Contract interactions.** Beyond the Rule M6 same-token guard, can a contract consume shielded inputs or produce shielded outputs? Deferred to a future RFC. -# Unresolved questions -[unresolved-questions]: #unresolved-questions +6. **`get_maximum_number_of_headers()` value.** 5 is the current value; whether to leave more margin for future headers is a consensus parameter decision. -1. **Shielded address format.** Option A (compact, 53-byte payload) vs Option B (full keys, 66-byte payload). The key tradeoff is address length vs. Silent Payments compatibility. This should be resolved before wallet implementations begin. +7. **40-bit range bound.** Whether `RANGE_PROOF_BITS = 40` is the right ceiling for all current and future tokens, or whether a per-token bit width is warranted. -2. **Final fee amounts.** The placeholder values (`FEE_PER_AMOUNT_SHIELDED_OUTPUT = 1`, `FEE_PER_FULL_SHIELDED_OUTPUT = 2`) are not final. Production values should be determined based on actual verification cost differentials, network economics, and desired incentive structure. +# Future possibilities +[future-possibilities]: #future-possibilities -3. **Batch verification optimization timeline.** The implementation currently verifies range proofs individually. Batch verification (`verify_multi`) would reduce CPU cost by an estimated 30-50% for multi-output transactions. The secp256k1-zkp API supports this, but the integration timeline is not yet determined. +## Binding signature / point-form excess (designed, not implemented) -4. **Light client scanning support.** Full nodes must verify all proofs, but light clients need an efficient protocol for detecting their own shielded payments without downloading every transaction. Possible approaches include SP-style scanning filters, trusted server-side scanning, or compact proof summaries. +Two related upgrades share one primitive — a per-tx Schnorr "binding signature" over a Pedersen value-balance commitment, with the kernel-excess point `E_tx = e_tx · G` as the public key: -5. **Shielded Nano Contract interactions.** How should Nano Contracts interact with shielded outputs? Can a contract consume shielded inputs or produce shielded outputs? What are the implications for contract state visibility? This is deferred to a future RFC. +- **Point-form `UnshieldBalanceHeader`.** Replace the 32-byte scalar `excess_blinding_factor` with `E_tx = excess · G` (33 B) plus a Schnorr signature (`R` 33 B, `z` 32 B). This keeps `Σ r_in` behind a discrete-log barrier (closing the full-unshield scalar leak) while the signature preserves inflation prevention. Cost: ~+66 B per full-unshield transaction; affects no other shape. *Recommended as the first follow-up.* -6. **Fee adjustment mechanism.** Should per-output fees be fixed consensus constants or adjustable via feature activation? Fixed constants are simpler but cannot adapt to changing verification costs or network conditions. +- **Binding signature for every shielded transaction.** Extend `ShieldedOutputsHeader` with the same `(E_tx, R, z)` trailer, removing the "last output absorbs the residual" construction. This enables independent per-output blindings, receiver-chosen (Sapling-style) blindings, composable output addition (no range-proof recomputation when adding an output), and non-interactive multi-party construction. Cost: ~+98 B per shielded transaction plus a verifier change. Justified only when a concrete trigger lands: Sapling-style payment proofs, hardware-wallet cold-signer flows, shielded coin-join / atomic swaps, or measured output-addition latency. -# Future possibilities -[future-possibilities]: #future-possibilities +The construction is exactly Sapling's binding signature (also used by Mimblewimble and Liquid CT): deterministic Schnorr over secp256k1 with a `"HathorBindingSig/v1"` domain separator, batch-verifiable across a block. Neither variant is in the current code; the network-wide supply audit ([§4.7](#47-network-wide-supply-audit)) already works without it. -## Sender Privacy (Phase C) +## Bulletproof range proofs -Ring signatures or nullifier-based protocols would hide which input is being spent, completing the privacy trifecta (hidden recipient + hidden amount/token + hidden sender). Shielded outputs simplify Phase C by making all outputs valid decoys regardless of their hidden amount or token. +Migrating from Borromean (~3213 B) to Bulletproof (~675 B) range proofs would cut shielded-output size ~5× and enable true batch verification, gated on `secp256k1-zkp` exposing a compatible Bulletproof + rewind API. -## Aggregated Bulletproofs for Multi-Party +## Shielded mint/melt amounts -If a multi-party computation (MPC) protocol for aggregated Bulletproofs becomes available in `secp256k1-zkp`, transactions where all outputs are controlled by the same party (the common case for non-atomic-swap transactions) could use a single aggregated proof, reducing total proof size logarithmically. +Extend `MintHeader`/`MeltHeader` to optionally carry a Pedersen commitment plus a range proof (and, for `DEPOSIT`-version tokens, a proof binding the HTR deposit to 1% of the committed amount), hiding total supply at the cost of the public-supply-auditor property. A business decision deferred to a later RFC; retrofittable via a new feature flag without invalidating already-issued tokens. -## Shielded Fee Amounts +## Aggregated Bulletproofs for single-party transactions -The fee amount is currently transparent. A future extension could allow the fee itself to be committed — the sender would prove (via range proof) that the committed fee exceeds the minimum, without revealing the exact amount. This would hide the number of shielded outputs (which is currently inferrable from the fee). +When all outputs are controlled by one party (the common non-swap case), a single aggregated proof could reduce total proof size logarithmically, if an MPC-free aggregation path becomes available. -## Cross-Chain Atomic Swaps with CT +## View key delegation for compliance -Shielded outputs on both sides of a cross-chain atomic swap would hide the amounts exchanged. Since each party independently balances their own blinding factors (see Section 4.1), no blinding factor exchange is needed. The atomic swap protocol only needs to coordinate the hash-time-lock, not the privacy layer. +A scan-key-derived view key would let a designated party (auditor, regulator, exchange) decrypt shielded outputs addressed to them without spending authority — analogous to Monero view keys. -## View Key Delegation for Compliance +## Asset-balance binding signature -Users could derive a "view key" that allows a designated party (auditor, regulator, exchange) to decrypt all shielded outputs addressed to them, without granting spending authority. This is analogous to Monero's view keys and could be implemented by sharing the scan private key. +The (future) binding signature covers value balance; a second one over asset balance would enable chain-wide *asset* audit for `FullShieldedOutput` transactions, detecting cross-asset forgery the same way inflation is detected today. -## Tiered Privacy Fees +## Tiered privacy fees -As the privacy stack matures, fees could be tiered by privacy level: transparent < amount-shielded < full-shielded < ring-shielded. This naturally extends the per-output-type fee model established in this RFC. +As the privacy stack matures, fees could be tiered by privacy level (transparent < amount-shielded < full-shielded < ring-shielded), extending the per-output-type fee model established here.