# Stealth withdraw

> Pay a withdrawal to a one-time address the recipient can discover and spend, announced atomically so the announcement cannot name an address the withdrawal never funds.

A stealth withdrawal is a [withdrawal](/operations/withdraw) whose destination is a fresh address derived from the recipient's published meta-address, plus an ERC-5564 announcement that lets the recipient find it. On-chain the two land in one transaction through [`PrivacyPoolRelay.relayAndAnnounce`](/protocol/contracts/privacy-pool-relay), which refuses an announcement whose stealth address differs from the proof-bound payout recipient. Read [Stealth addresses](/concepts/stealth-addresses) first for the derivation and the two schemes.

## How it works

1.  **Resolve the recipient.** Turn what the recipient gave you into a `MetaAddress`: a `canonical` identifier (compressed spending and viewing public keys) selects the EIP-5564 scheme at `0x0001`; a `raw` identifier (one uncompressed spending key) selects the FluidKey scheme at `0xfffe`.
2.  **Derive.** Generate an ephemeral key pair and derive the one-time `stealthAddress` and its `Announcement` (scheme id, address, ephemeral public key, view-tag metadata).
3.  **Prove.** Build the withdrawal exactly as usual, with `recipientAddress = stealthAddress` and PrivacyPoolRelay as processor.
4.  **Relay and announce.** Hand the relayer the withdrawal relay params plus the announcement fields. It submits `relayAndAnnounce`, the contract checks `announcement.stealthAddress == payout.recipient`, relays, then announces to the canonical `Announcer` as the last action.

## How to withdraw to a stealth address

The stealth module is a separate SDK subpath. The relayed, atomic path uses `RelayerInteractor.relayWithAnnounce`:

```ts

    CANONICAL_ANNOUNCER_ADDRESS,
    deriveStealthAddress,
    generateEphemeralKeyPair,
    resolveRecipient,
    toStealthAnnouncementFields,
} from "@privacy-pools-v2/sdk/stealth";

// 1–2. Resolve and derive.
const metaAddress = resolveRecipient({
    kind: "canonical",
    spendingPubKey: recipientSpendingPubKey,   // 33-byte compressed secp256k1
    viewingPubKey: recipientViewingPubKey,     // 33-byte compressed secp256k1
});
const ephemeral = generateEphemeralKeyPair(metaAddress.scheme);
const derivation = deriveStealthAddress(metaAddress, ephemeral, metaAddress.scheme);

// 3. Prove a normal withdrawal to the one-time address.
const prepared = await session.prepareWithdraw({
    inputCommitments: [noteToSpend.commitment],
    amount: withdrawAmount,
    tokenId: NATIVE_ETH,
    recipientAddress: derivation.stealthAddress,
});

// 4. Relay atomically with the announcement. `withdrawalRelayParams` is the
//    relay params you would pass to relayWithdrawal for prepared.relayerOptions[0].
const txHash = await relayerInteractor.relayWithAnnounce(relayerInfo, {
    ...withdrawalRelayParams,
    announcement: toStealthAnnouncementFields(derivation),
});
```

If you are submitting yourself, `prepareStealthWithdrawal` composes the pair for you: give it the recipient identifier, a callback that returns your withdrawal call for a given stealth address, and `CANONICAL_ANNOUNCER_ADDRESS`, and it returns a `StealthWithdrawalBundle` with `withdraw` and `announce` as two `EvmCall`s. Submit both; without `relayAndAnnounce` the binding between them is only what you enforce, which is why the relayed path is preferred.

## Recipient side

The recipient scans announcements in their scheme with `scanAnnouncement(announcement, credentials)`, which returns the stealth address on a match and `null` otherwise, using the one-byte view tag to skip the vast majority cheaply. `deriveStealthPrivateKey` then produces the key that spends the funds. `verifyAnnouncementBindsToWithdrawal` confirms an announcement sits in the same transaction as a matching withdrawal before the recipient trusts it.

## Constraints

-   **Canonical announcer only.** The SDK refuses any announcer other than the CREATE2 singleton at `0x55649E01B5Df198D18D95b5cc5051630cfD45564` (`assertCanonicalAnnouncer`), because announcing elsewhere narrows the anonymity set to that other deployment's users.
-   **Only the address is bound.** The contract enforces `stealthAddress == recipient`. A wrong ephemeral key breaks discovery of a correctly funded payment, which is recoverable by re-announcing; it cannot misdirect funds.
-   **Scheme mismatch is silent.** A canonical scanner never finds a FluidKey announcement and vice versa. Resolve with the identifier kind the recipient actually published.
-   **Standard relay only.** `relayAndAnnounce` is a PrivacyPoolRelay function with no `PPRouter` branch and no batch form, so a stealth withdrawal cannot unwrap a [yield](/concepts/yield-shares) note or be a [batch](batch-withdraw) item.
-   **Announcements are public.** Anyone can see that this withdrawal was a stealth payment. For cooperative discovery inside a deposit, the module's `encodeStealthDepositPayload` / `scanDepositNoteData` carry the same material in the note's encrypted data instead; the cross-chain [reshield](reshield) dust rescue uses that.
-   **Availability.** `relayAndAnnounce` exists only on relay contracts deployed after it was added. The production relay on [mainnet, BNB Chain, and Citrea](/deployments) has it (its `ANNOUNCER()` is the canonical announcer); the Sepolia V9 relay in the [Sepolia page](/deployments/sepolia) predates it. Confirm the relayer publishes a relay that exposes it before offering the option.

## Behind the scenes

SDK call `resolveRecipient`, `generateEphemeralKeyPair`, `deriveStealthAddress`, `toStealthAnnouncementFields` from `@privacy-pools-v2/sdk/stealth`; `session.prepareWithdraw(...)`; `relayerInteractor.relayWithAnnounce(relayer, params)`.
Contract method [`PrivacyPoolRelay`](/protocol/contracts/privacy-pool-relay)`.relayAndAnnounce(proof, transactParams, noteData, announcement)`, which calls `PoolVault.transact`, distributes the payout, and then `Announcer.announce(schemeId, stealthAddress, ephemeralPubKey, metadata)`.
Relayer route `POST /v1/relay/evm/{chainId}/relay-and-announce`: an ordinary withdrawal relay request plus a nested `announcement`. The withdrawal fee quote is reused; the announcement transfers no value.
Circuit [`transact_NxM`](/protocol/circuits/transact); nothing stealth-specific is proven. The stealth address is just the bound recipient.

## What's next

-   [Withdraw](withdraw): the plain version of the same proof.
-   [Stealth addresses](/concepts/stealth-addresses): schemes, view tags, and why the announcement is bound on-chain.
-   [Stealth module reference](/sdk/stealth): every export of the subpath.
