ERC-5564: Stealth Addresses
ERC-5564 standardises a recipient-side privacy primitive: the recipient publishes a long-term stealth meta-address (two public keys — spending + viewing); senders derive a fresh per-payment recipient address from that meta-address plus an ephemeral key. The stealth addresses are unlinkable to the meta-address by anyone who doesn't hold the viewing key — outside observers see a stream of one-shot recipient addresses. The recipient scans incoming announcements with their viewing key, identifies which ones belong to them, and spends with the matching spending key. This is the cryptographic foundation of Umbra Cash and related privacy tools.
Required Components
- Meta-address format (
st:eth:0x<spendingPubKey><viewingPubKey>) — a CAIP-style identifier the recipient publishes once. - Stealth address generation — sender computes
stealthPubKey = spendingPubKey + hash(sharedSecret) * GwheresharedSecret = ECDH(ephemeralPriv, viewingPubKey). - Announcement contract — a singleton that emits an
Announcementevent for every stealth payment so recipients know to scan. - View tag — a short prefix of
hash(sharedSecret)published with each announcement so recipients can quickly skip non-matching announcements without doing full ECC math.
interface IERC5564Announcer {
event Announcement(
uint256 indexed schemeId, // 1 = secp256k1
address indexed stealthAddress,
address indexed caller,
bytes ephemeralPubKey,
bytes metadata // includes view tag + amount
);
function announce(
uint256 schemeId,
address stealthAddress,
bytes calldata ephemeralPubKey,
bytes calldata metadata
) external;
}Neo Equivalent: secp256r1 ECDH + Announcement Contract
Neo's native cryptography exposes secp256r1 (the default curve for Neo addresses) with CryptoLib.VerifyWithECDsa and ECDH-friendly point arithmetic via ECPoint. The stealth-address scheme transposes to secp256r1 with the same algebra; the only meaningful difference is the curve and the address-derivation step (Helper.ToScriptHash(pubKey) instead of Ethereum's keccak256(pubKey)[12:]).
// Sender side (off-chain, then on-chain announcement):
// 1. Derive sharedSecret = ECDH(ephemeralPriv, viewingPubKey)
// 2. Compute stealthPub = spendingPubKey + hash(sharedSecret) * G
// 3. Convert stealthPub → stealthScriptHash via Helper.ToScriptHash
// 4. Send NEO/GAS/NEP-17 to stealthScriptHash
// 5. Call Announcer.Announce(schemeId, stealthScriptHash, ephemeralPub, metadata)
//
// Recipient side (off-chain):
// 1. Subscribe to Announcer's "Announcement" notifications
// 2. For each announcement, derive sharedSecret with viewingPriv + ephemeralPub
// 3. Check view tag → fast-skip non-matching announcements
// 4. For matching ones, derive stealthPriv = spendingPriv + hash(sharedSecret)
// 5. Use stealthPriv to spend the received funds| ERC-5564 Component | Neo Equivalent | Notes |
|---|---|---|
| Curve: secp256k1 | Curve: secp256r1 (Neo default) | Same algebra; different curve params |
metaAddress = spending‖viewing (33+33 = 66 bytes compressed) | Same compressed point format; encode as ByteString | |
address(stealthPubKey) via keccak256[12:] | Helper.ToScriptHash(stealthPubKey) → UInt160 | Different one-way hash, same role |
Announcement(...) event | Announcement(...) notification | NeoVM Notify semantics |
| View tag (1 byte) | View tag (1 byte) — same idea | Identical optimisation |
| Singleton announcer at known address | Singleton announcer contract; address published in NEP-X registration | Same architectural choice |
Why The Curve Difference Doesn't Break Adoption
ERC-5564 is parameterised by schemeId — the spec reserves schemeId = 1 for secp256k1 and explicitly leaves room for other curves at higher ids. A Neo deployment claims schemeId = 100 (or similar) for secp256r1. Any wallet that wants to integrate Neo stealth addresses extends its scheme table by one entry; no protocol-level forking required.
Migration Notes
Solidity contracts using stealth addresses are uncommon — most stealth scanning happens off-chain. The on-chain footprint is the announcer contract (a thin event emitter) plus optionally a registry for meta-addresses (so wallets can look up "what's this recipient's stealth meta-address?" by domain or ENS-like name).
For Neo, the announcer is a 30-line C# contract; the heavy lifting (key derivation, scanning) lives in the wallet SDK. A future NEP-X registry for meta-addresses would slot in alongside ERC-1820 or use NNS as the discovery layer.
Composability
- Pairs with ERC-1271 when the stealth address is contract-controlled (a contract holds the spending key).
- Pairs with ERC-6551 — TBAs can be stealth addresses bound to NFTs, hiding the link between an NFT collection and its beneficiary owners.
- Pairs with ERC-3009 for "anonymous one-shot transfer with off-chain authorization".
