Skip to main content

Stealth module

The stealth module ships on its own subpath, @privacy-pools-v2/sdk/stealth, separate from the main barrel; nothing in it is reachable from @privacy-pools-v2/sdk. It implements ERC-5564 stealth addresses for two schemes and the glue that binds an announcement to a pool withdrawal. Usage walkthroughs live on Stealth withdraw; this page is the export reference.

import { deriveStealthAddress, scanAnnouncement } from "@privacy-pools-v2/sdk/stealth";

Schemes and constants​

SCHEME_CANONICAL // 0x0001, EIP-5564 secp256k1 scheme 1
SCHEME_FLUIDKEY // 0xfffe, FluidKey recipe (placeholder id until registered)
CANONICAL_ANNOUNCER_ADDRESS // 0x55649e01b5df198d18d95b5cc5051630cfd45564
assertCanonicalAnnouncer(announcer: EvmAddress): void
VIEW_TAG_BYTES // 1
ERC5564_ANNOUNCE_ABI // the announce(uint256,address,bytes,bytes) ABI fragment

assertCanonicalAnnouncer throws StealthAddressAnnouncementError for any address other than the canonical CREATE2 singleton; every builder that emits an announce call runs it.

Sender side​

resolveRecipient(identifier: RecipientIdentifier): MetaAddress
generateEphemeralKeyPair(scheme: SchemeId): EphemeralKeyPair
deriveStealthAddress(metaAddress: MetaAddress, ephemeral: EphemeralKeyPair, schemeId: SchemeId): StealthDerivation
deriveStealthSharedSecret(...) // the ECDH shared-secret hash the scheme uses
computeViewTag(...) // first byte of the shared hash
encodeViewTagAsMetadata(viewTag): Hex // the 1-byte announcement metadata
buildAnnounceCall(announcement: Announcement, announcer: EvmAddress): EvmCall
composeWithdrawalBundle(withdrawCall: DeclaredWithdrawCall, announcement: Announcement, announcer: EvmAddress): StealthWithdrawalBundle
prepareStealthWithdrawal(request: StealthWithdrawalRequest): Promise<StealthWithdrawalBundle>
toStealthAnnouncementFields(derivation: StealthDerivation): StealthAnnouncementFields
  • resolveRecipient maps an identifier to a typed meta-address. { kind: "raw", spendingPubKey } (65-byte uncompressed) yields a FluidKey meta-address at 0xfffe; { kind: "canonical", spendingPubKey, viewingPubKey } (two 33-byte compressed keys, both curve-validated) yields a canonical meta-address at 0x0001.
  • generateEphemeralKeyPair draws from WebCrypto only (rejection-sampled, no fallback) and produces the ephemeral public key in the scheme's wire format: compressed for canonical, uncompressed for FluidKey.
  • deriveStealthAddress dispatches on metaAddress.scheme; the explicit schemeId argument is defensive and must match (StealthAddressSchemeMismatchError otherwise). It returns { stealthAddress, announcement } and deliberately drops the meta-address from the result.
  • composeWithdrawalBundle requires the withdraw call's declaredDestination to equal the announcement's stealthAddress, the client-side form of the binding PrivacyPoolRelay.relayAndAnnounce enforces on-chain.
  • prepareStealthWithdrawal is the orchestrator: resolve, derive, call your generateWithdrawCall(stealthAddress) callback, and compose the bundle against request.announcerAddress.
  • toStealthAnnouncementFields converts a derivation into the announcement object RelayerInteractor.relayWithAnnounce expects.

Recipient side​

scanAnnouncement(announcement: Announcement, credentials: ScanCredentials): EvmAddress | null
deriveStealthPrivateKey(announcement: Announcement, credentials: DerivePrivCredentials): Scalar32
recoverStealthSharedSecret(...)
verifyAnnouncementBindsToWithdrawal(announcement, withdrawalLog: WithdrawalLogEvent): boolean
scanDepositNoteData(data: Hex, credentials: ScanCredentials): DepositStealthMatch | null
encodeStealthDepositPayload(payload: StealthDepositPayload): Hex
decodeStealthDepositPayload(data: Hex): StealthDepositPayload
  • scanAnnouncement requires announcement.schemeId === credentials.scheme, recomputes the shared hash from the recipient's key material (viewing private key for canonical, spending private key for FluidKey), compares its first byte to the view tag, and only on a hit reconstructs the candidate and compares it to the announced address.
  • deriveStealthPrivateKey produces the spending key for a matched address and self-validates it against the announced address (StealthAddressScanIntegrityError on mismatch).
  • verifyAnnouncementBindsToWithdrawal is the recipient-side mirror of the on-chain binding: it checks an announcement against a same-transaction withdrawal event.
  • The deposit-payload trio carries an ephemeral key, view tag, and ECDH-encrypted note payload inside a deposit's opaque noteData.data instead of a public announcement. Unlike scanAnnouncement there is no separately claimed on-chain address to verify against, so the derived candidate is the result. Used by the cross-chain reshield dust rescue.

Types​

type SchemeId = typeof SCHEME_CANONICAL | typeof SCHEME_FLUIDKEY;
type MetaAddress = CanonicalMetaAddress | FluidKeyMetaAddress; // discriminated on `scheme`
type Announcement = { schemeId; stealthAddress; ephemeralPubKey; viewTag; ... };
type StealthDerivation = { stealthAddress: EvmAddress; announcement: Announcement };
type RecipientIdentifier =
| { kind: "raw"; spendingPubKey: Hex }
| { kind: "canonical"; spendingPubKey: Hex; viewingPubKey: Hex };
type StealthWithdrawalRequest = {
recipient: RecipientIdentifier;
generateWithdrawCall: (stealthAddress: EvmAddress) => Promise<EvmCall> | EvmCall;
announcerAddress: EvmAddress;
schemeId?: SchemeId;
};
type StealthWithdrawalBundle = { withdraw: EvmCall; announce: EvmCall };
type DeclaredWithdrawCall = { to: EvmAddress; data: Hex; declaredDestination: EvmAddress };
type EvmCall = { to: EvmAddress; data: Hex };
type ScanCredentials / DerivePrivCredentials // per-scheme unions of the recipient's key material
type StealthDepositPayload / DepositStealthMatch / WithdrawalLogEvent

Branded hex types (CompressedPubKey, UncompressedPubKey, Scalar32, EvmAddress) validate width, prefix, and curve prefix at the codec boundary. assertNever is exported for exhaustive switches over the scheme union.

Errors​

All extend StealthAddressError: StealthAddressDeriveError, StealthAddressUnsupportedSchemeError, StealthAddressSchemeMismatchError, StealthAddressAnnouncementError, StealthAddressScanIntegrityError. They are runtime values on this subpath, so instanceof works here, unlike most errors on the main barrel.

Relation to the main SDK​

RelayerInteractor.relayWithAnnounce(relayer, params) on the main barrel is the transport for the atomic relayed path; its params are WithdrawalRelayParams & { announcement: StealthAnnouncementFields }. PoolSessionReshieldCrosschain accepts a dustRescue: { recipientIdentifier } opt-in that reuses this module's derivation for the destination-chain dust deposit. Stealth does not compose with batch withdrawal or with a yield unwrap.

Source: v2-monorepo/packages/sdk/src/stealth/