Skip to content

design: Shielded outputs - #104

Open
msbrogli wants to merge 6 commits into
masterfrom
feat/shielded-outputs
Open

design: Shielded outputs#104
msbrogli wants to merge 6 commits into
masterfrom
feat/shielded-outputs

Conversation

@msbrogli

@msbrogli msbrogli commented Mar 3, 2026

Copy link
Copy Markdown
Member

@msbrogli msbrogli self-assigned this Mar 3, 2026
@msbrogli msbrogli moved this from Todo to In Progress (Done) in Hathor Network Mar 3, 2026
Comment thread text/0000-shielded-outputs.md Outdated
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread text/0000-shielded-outputs.md Outdated

### 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I believe we should not enforce it at the protocol level. We should probably make it standard in the wallet library, though.

@msbrogli
msbrogli force-pushed the feat/shielded-outputs branch from f033ceb to 4bb0b7e Compare March 4, 2026 18:58
@msbrogli msbrogli moved this from In Progress (Done) to In Review (Done) in Hathor Network Mar 5, 2026
@msbrogli
msbrogli force-pushed the feat/shielded-outputs branch from 4bb0b7e to 57c7958 Compare March 13, 2026 15:56
@msbrogli msbrogli moved this from In Review (Done) to In Progress (Done) in Hathor Network Mar 13, 2026
### Sending Shielded Payments

1. **Choose privacy tier** per output (`AmountShieldedOutput` or `FullShieldedOutput`).
2. **Generate ephemeral keypair** `(e, E = e*G)` for each output.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you generate it? Do you take some info from the output to make it unique?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you generate a random number e and multiply by G.

@msbrogli msbrogli moved this from In Progress (Done) to In Review (Done) in Hathor Network Mar 18, 2026
msbrogli and others added 4 commits April 27, 2026 00:38
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>
@msbrogli msbrogli moved this from In Review (Done) to In Progress (WIP) in Hathor Network Jun 24, 2026
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>
@msbrogli msbrogli moved this from In Progress (WIP) to In Progress (Done) in Hathor Network Jun 30, 2026

@LFRezende LFRezende left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Comment on lines +58 to +67

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +92 to +94
## 2. How It Works

### Shielding: Moving Funds into Privacy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +135 to +140

> **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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comment. I like it.

@pedroferreira1 pedroferreira1 moved this from In Progress (Done) to In Review (WIP) in Hathor Network Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Review (WIP)

Development

Successfully merging this pull request may close these issues.

4 participants