design: Shielded outputs - #104
Conversation
| scan_pubkey(33) || hash(spend_pubkey)(20) | ||
| ``` | ||
|
|
||
| The spend pubkey is represented as a 20-byte hash (like a standard P2PKH address). Shorter addresses, but the full spend pubkey is not recoverable from the address alone — it must be obtained from the blockchain (from a prior spend) or out-of-band. |
There was a problem hiding this comment.
but the full spend pubkey is not recoverable from the address alone — it must be obtained from the blockchain (from a prior spend) or out-of-band.
Who must obtain it? The receiver already has it and the sender does not need it, correct?
There was a problem hiding this comment.
The sender. But the previous text was misleading and was updated. Let me explain here: For shielded outputs, the compact address is enough and nothing has to be recovered by the sender. But it imposes a limitation for silent payments.
|
|
||
| ### 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. |
There was a problem hiding this comment.
or include a transparent output
In this case, won't we still know the amount in the shielded output? In other words, won't we require still 2 shielded outputs?
There was a problem hiding this comment.
Also, this rule will be enforced on the wallet, correct? From the protocol side, should be possible to do it even if it "leaks" information.
There was a problem hiding this comment.
Indeed. I added this rule to open the discussion whether the protocol should require at least two shielded outputs or not.
When there are two or more shielded outputs and all inputs are transparent, observers can determine the total value of the outputs, but not how that value is distributed among them.
If there's at least one shielded input, observers can only infer that sum(outputs) > sum(transparent_inputs).
There was a problem hiding this comment.
Got it. I believe we should not enforce it at the protocol level. We should probably make it standard in the wallet library, though.
f033ceb to
4bb0b7e
Compare
4bb0b7e to
57c7958
Compare
| ### Sending Shielded Payments | ||
|
|
||
| 1. **Choose privacy tier** per output (`AmountShieldedOutput` or `FullShieldedOutput`). | ||
| 2. **Generate ephemeral keypair** `(e, E = e*G)` for each output. |
There was a problem hiding this comment.
How do you generate it? Do you take some info from the output to make it unique?
There was a problem hiding this comment.
No, you generate a random number e and multiply by G.
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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.
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
LFRezende
left a comment
There was a problem hiding this comment.
Overall a really fine document, with only minor confusions and mistakes and/or just minor stuff that needs clarification. I think that after this first batch of reviews should already be approvable.
| - **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. |
There was a problem hiding this comment.
Not wrong, but I think you're hinting far too early the concept of shielded minting/melting, and not necessarily explaining why we need a shielded issuancer to cover for transparent minting/melting.
Maybe something on the lines of "while transparency for minting/melting operations is fundamental to probe the network's balance of token supplies, the transparency of these operations reveals the transaction that issued the supply change, exposing the identity of the issuer."
|
|
||
| Hathor offers **two privacy tiers**, selectable per output: | ||
|
|
||
| | Tier | Hides Amount | Hides Token | Proof Overhead | Use Case | | ||
| |------|:---:|:---:|---|---| | ||
| | `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 **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. | ||
|
|
There was a problem hiding this comment.
It is increasingly clear we will not go to mainnet with Borromean, both for cost optimizations and to parallelize verifications and proofs. So we need to change this section to include Bulletproofs.
| Tier | Hides Amount | Hides Token | Proof Overhead | Use Case |
|------|:---:|:---:|---|---|
| `AmountShieldedOutput` | Yes | No | Range proof (~ $675 + 64\cdot log_2 (n)$ bytes) | Hide salary amounts while token type is public |
| `FullShieldedOutput` | Yes | Yes | Range proof (same as above) + surjection proof (variable bytes) | Full privacy for multi-token transactions |
Additionally, the range of the proof is not
$[1, 2^40]$ But:
$[1, 2^(64 + \ceil{\frac{D}{log_10 2}})]$ Where D is the decimals we intend to add in the future as precision.
For simplicity, we can say for now [1, 2^k], where k is the number of range proof bits we intend to use, where typically k == 64.
Final paragraph rewritten:
| Tier | Hides Amount | Hides Token | Proof Overhead | Use Case |
|------|:---:|:---:|---|---|
| `AmountShieldedOutput` | Yes | No | Range proof (~ $675 + 64\cdot log_2 (n)$ bytes) | Hide salary amounts while token type is public |
| `FullShieldedOutput` | Yes | Yes | Range proof (same as above) + surjection proof (variable bytes) | Full privacy for multi-token transactions |
Both tiers use **Pedersen commitments** for amounts and **Bulletproof range proofs** to guarantee amounts are non-negative (specifically in `[1, 2^k)`), where k is the number of range proof bits we intend to use, typically `k == 64`. `FullShieldedOutput` additionally uses **blinded asset tags** and **asset surjection proofs** to hide and validate the token type.| ## 2. How It Works | ||
|
|
||
| ### Shielding: Moving Funds into Privacy |
There was a problem hiding this comment.
Opinion: The shielding section basically assumes profficiency with pedersen commitments, blinding factors and such.
Adding a "preliminaries" subsection here to introduce the basic concepts and hooks we'll work with, such as pedersen commitments, hash-to-point and such, could do massive work to easen the blow to newcomers.
No need to go too technical, just introducing and explaining what it does is enough.
| 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) |
There was a problem hiding this comment.
Scratch that. He does know the amount if he can sum. The whole point is unshielding. If we wished him not to know, we'd have two shielded inputs.
|
|
||
| > **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). | ||
|
|
There was a problem hiding this comment.
I think we already have this implemented? Or if not, I think we create pedersen commitments with zero value and just the excess blinding factor.
| tag = SHA256("Hathor_AssetTag_v1" || token_uid_32B) | ||
| H_token = Generator::new_unblinded(tag) # 33-byte point | ||
| ``` | ||
|
|
There was a problem hiding this comment.
I may need to review the docs on Generator::new_blinded() to be sure, but the tag usually has a counter, like this:
tag = SHA256("Hathor_AssetTag_v1" || token_uid_32B || k)
k = 0, 1, 2, ...H_token is a hash-to-point procedure, so the hash MAY lie on the curve or not (~50% chance for secp256k1).
In case it doesn't, we need to increase k and try again. The probability of it not lieing on a curve is 2**(-attemps) so eventually it will fall.
I pointed out in a previous repo review that the lib we used did not do this, but they seemed to use a different procedure. Nevertheless, it is worth flagging it out.
| r = blinding factor (32-byte scalar) | ||
| H = generator point specific to the token type | ||
| G = secp256k1 base generator | ||
| ``` |
There was a problem hiding this comment.
This holds only for ``AmountHidden` shielded outputs.
For FullShielded shielded outputs, C = v*A + r_value*G, where A = H + r_asset*G is the blinded asset tag. In amount only, r_asset = 0.
We should have a small section disclaiming it, and I advise for us to handle mentally all Pedersen Commitments as C == v*A + r*G, to avoid mistakes in the future.
| ### What Still Works | ||
|
|
||
| - Transaction structure (input/output count, shielded vs. transparent). | ||
| - Transparent outputs (amounts, tokens, scripts — unchanged). |
There was a problem hiding this comment.
This line depends on the choice we go with for the addresses. Some scripts may change (we may abandon P2PKH).
| total_fee = shielded_fee + standard_fee_if_any | ||
| ``` | ||
|
|
||
| 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. |
There was a problem hiding this comment.
Indeed it is fine to leave a 1% HTR deposit rate, as mint/melt amounts are public.
|
|
||
| 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 and rewinding the range proofs. |
Rendered