# Stealth addresses

> Pay a withdrawal to a fresh, unlinkable address the recipient can find and spend without ever sharing an address: ERC-5564 meta-addresses, announcements, and the two supported schemes.

A normal [withdrawal](/operations/withdraw) pays a public address the sender chose. If a recipient hands out one address per counterparty or reuses one, the payments become linkable on-chain. Stealth addresses let a recipient publish a single static **meta-address** while every payment lands at a fresh, one-time `stealthAddress` that only the recipient can compute the private key for. The sender also posts an **announcement** so the recipient can discover the payment by scanning, with no interactive channel.

Privacy Pools v2 uses this for the last hop out of the pool: a stealth withdrawal is an ordinary withdrawal whose recipient is a derived one-time address, submitted atomically with its ERC-5564 announcement through [`PrivacyPoolRelay.relayAndAnnounce`](/protocol/contracts/privacy-pool-relay).

## Meta-address, ephemeral key, announcement

1.  **The recipient publishes a meta-address.** For the canonical scheme that is two compressed secp256k1 public keys, a spending key and a viewing key. For the FluidKey scheme it is a single uncompressed spending key.
2.  **The sender derives.** They generate a fresh ephemeral key pair, perform ECDH against the recipient's key, hash the shared secret, and derive the one-time `stealthAddress` from the recipient's spending key plus that hash. The first byte of the hash is the **view tag**.
3.  **The sender announces.** `announce(schemeId, stealthAddress, ephemeralPubKey, metadata)` on the canonical ERC-5564 `Announcer`, with the view tag as metadata.
4.  **The recipient scans.** For each announcement in their scheme, recompute the shared hash from their own key and the ephemeral key; compare its first byte to the view tag (a 1-in-256 prefilter that skips most announcements cheaply); on a match, reconstruct the candidate address and compare it to the announced `stealthAddress`. A hit yields the stealth private key, which spends the funds.

## Two schemes

The SDK ships both recipes side by side and dispatches on the meta-address's scheme tag.

| | `SCHEME_CANONICAL` | `SCHEME_FLUIDKEY` |
|---|---|---|
| `schemeId` | `0x0001` (the EIP-5564 registered id for secp256k1) | `0xfffe` (an unallocated placeholder until the FluidKey variant is registered) |
| ECDH against | recipient **viewing** pubkey | recipient **spending** pubkey |
| Point operation | addition, `S = B + hash·G` | multiplication, `S = B·hash` |
| Ephemeral pubkey on the wire | 33-byte compressed | 65-byte uncompressed |
| Who can scan | the viewing key alone | the spending key (it must be online to scan) |

The two are not interchangeable: a canonical scanner applied to FluidKey bytes silently finds nothing. An earlier phase shipped the FluidKey recipe under `0x0001`; the multi-scheme module moved it to `0xfffe` and restored the true canonical recipe at `0x0001`, without changing any FluidKey derivation bytes.

## The canonical announcer

All announcements go to the one ERC-5564 `Announcer` singleton, deployed at the same address on every supporting chain: `0x55649E01B5Df198D18D95b5cc5051630cfD45564`. The SDK refuses any other announcer address, because announcing through a different deployment narrows the recipient's anonymity set to "users of that other announcer". The monorepo ships a minimal `ERC5564Announcer` only for local and anvil deployments where the singleton is absent; live deployments resolve the canonical address.

## Why the announcement is bound on-chain

An announcement is advisory by nature: nothing in ERC-5564 ties it to a payment. If a relayer could announce one stealth address while paying another, the recipient would scan a hit and find nothing there. `PrivacyPoolRelay.relayAndAnnounce` closes that gap: it decodes the payout recipient from the proof-bound `PayoutRouting` and requires `announcement.stealthAddress` to equal it (`PrivacyPoolRelay_AnnouncementRecipientMismatch`), then relays and announces in one transaction. Only the address is enforced. A wrong ephemeral key would only break discovery of a correctly funded payment, which is recoverable by re-announcing. On the recipient side the SDK offers the mirror check, verifying that an announcement sits in the same transaction as a matching withdrawal.

## Announcements are public

Posting to the announcer marks the withdrawal as a stealth payment for anyone watching the chain. That is the right trade when sender and recipient have no other channel. When discovery can be cooperative, the SDK has a quieter carrier: the ephemeral public key and view tag can be packed, with the encrypted note payload, into a deposit's opaque `noteData.data` and scanned from there instead of from a public announcement. The cross-chain [reshield](/operations/reshield) dust-rescue path uses this, which is why an earlier design that announced rescue deposits publicly was withdrawn.

## What this is not

Stealth addresses are separate from [viewing keys](/concepts/keys). A viewing key decrypts notes inside the pool; a stealth meta-address controls where a withdrawal lands outside it. Stealth withdrawal is a standard-relay feature: it does not compose with [batch withdrawal](/operations/batch-withdraw) or with a [yield](/concepts/yield-shares) unwrap, both of which bind a different processor.

Source: `v2-monorepo/packages/sdk/src/stealth/` and `packages/contracts/src/contracts/PrivacyPoolRelay.sol`
