Skip to content

EIP-5564 Implementation

This document details SIP Protocol’s implementation of EIP-5564 (Stealth Addresses) and ERC-6538 (Stealth Meta-Address Registry), including technical decisions, security considerations, and integration guides.

EIP-5564 defines a standardized approach to stealth addresses on Ethereum. SIP Protocol implements the secp256k1 scheme (scheme id 1) for full compatibility with the Ethereum ecosystem.

PropertySIP Implementation
Scheme1 (secp256k1)
Curvesecp256k1
Key formatCompressed (33 bytes)
Hash functionSHA-256
View tagFirst byte of shared secret hash
Address derivationKeccak-256 of uncompressed public key

A stealth meta-address encodes two public keys needed to generate stealth addresses:

sip:<chain>:<spendingKey>:<viewingKey>
ComponentSizeDescription
chainvariableChain identifier (e.g., ethereum, polygon)
spendingKey33 bytesCompressed secp256k1 public key for spending
viewingKey33 bytesCompressed secp256k1 public key for scanning

Compressed secp256k1 public keys are 33 bytes:

  • First byte: 0x02 (y is even) or 0x03 (y is odd)
  • Remaining 32 bytes: x-coordinate
// Valid compressed keys
'0x02' + 64 hex chars // 33 bytes total, y even
'0x03' + 64 hex chars // 33 bytes total, y odd
// Invalid formats
'0x04' + 128 hex chars // Uncompressed (65 bytes)
'0x' + 64 hex chars // Missing prefix (32 bytes)
import { generateStealthMetaAddress } from '@sip-protocol/sdk'
const { metaAddress, spendingPrivateKey, viewingPrivateKey } =
generateStealthMetaAddress('ethereum')
// metaAddress.spendingKey: 0x02abc...
// metaAddress.viewingKey: 0x03def...
import {
encodeStealthMetaAddress,
decodeStealthMetaAddress,
} from '@sip-protocol/sdk'
// Encode for sharing
const encoded = encodeStealthMetaAddress(metaAddress)
// "sip:ethereum:0x02abc...:0x03def..."
// Decode from string
const decoded = decodeStealthMetaAddress(encoded)

The protocol follows EIP-5564’s cryptographic scheme:

Given:

  • Recipient spending public key: K
  • Recipient viewing public key: V
  • Ephemeral private key: r (randomly generated)
  • Ephemeral public key: R = r * G

The stealth address is derived as (canonical EIP-5564 — shared secret from the VIEWING key, stealth public key offset from the SPENDING key):

1. Compute shared secret point: S = r * V
2. Hash the shared secret: h = SHA256(S)
3. Derive stealth public key: P = K + h * G
4. Derive Ethereum address: addr = keccak256(decompress(P))[12:32]
5. View tag: vt = h[0]
import { generateStealthAddress } from '@sip-protocol/sdk'
const { stealthAddress, sharedSecret } = generateStealthAddress(recipientMetaAddress)
// stealthAddress.address: 0x02xyz... (compressed public key)
// stealthAddress.ephemeralPublicKey: 0x03abc... (must be published)
// stealthAddress.viewTag: 42 (0-255, first byte of hash)

Converting the stealth public key to an Ethereum address:

import { publicKeyToEthAddress } from '@sip-protocol/sdk'
// Compressed public key → Ethereum address
const ethAddress = publicKeyToEthAddress(stealthAddress.address)
// "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"

Steps:

  1. Decompress the public key (33 bytes → 65 bytes)
  2. Remove the 04 prefix (65 → 64 bytes)
  3. Keccak-256 hash (64 → 32 bytes)
  4. Take last 20 bytes
  5. Apply EIP-55 checksum

The ephemeral key is generated per-payment and must be published for the recipient to claim funds:

// Sender generates ephemeral key
const { stealthAddress } = generateStealthAddress(recipientMetaAddress)
// Ephemeral public key must be published (announcer contract or off-chain)
console.log('Publish:', stealthAddress.ephemeralPublicKey)

Sender computes:

S = r * V (ephemeral private × viewing public)

Recipient computes:

S = v * R (viewing private × ephemeral public)

Both arrive at the same point due to ECDH properties. Because the shared secret derives from the VIEWING key, the recipient can detect payments with only the viewing private key plus the spending PUBLIC key — the spending private key is needed solely to derive the spendable stealth key.

The view tag enables efficient scanning by allowing quick rejection of non-matching payments.

  1. View tag = first byte of SHA256(shared_secret_point)
  2. Range: 0-255
  3. Expected false positive rate: ~1/256 (0.39%)

checkStealthAddress performs the view-tag check internally (it returns early on a tag mismatch before doing the full elliptic-curve derivation), so scanning is a single call per candidate:

Scanning is view-only — it needs the recipient’s viewing PRIVATE key and their spending PUBLIC key (the meta-address spendingKey), never the spending private key (canonical EIP-5564):

import { checkStealthAddress } from '@sip-protocol/sdk'
// For each potential payment
for (const payment of potentialPayments) {
// View tag is checked first inside checkStealthAddress; the
// expensive derivation only runs when the tag matches (~0.4% of the time)
const isMine = checkStealthAddress(
payment.stealthAddress,
myViewingPrivateKey,
mySpendingPublicKey
)
if (isMine) {
// This is our payment!
console.log('Found payment:', payment)
}
}

Without view tag: O(n × full_computation) With view tag: O(n × (cheap_check + 0.004 × full_computation))

For 1 million transactions, this reduces full computations from 1M to ~4K.

EIP-5564 defines an announcer contract for publishing ephemeral keys on-chain.

interface IERC5564Announcer {
event Announcement(
uint256 indexed schemeId,
address indexed stealthAddress,
address indexed caller,
bytes ephemeralPubKey,
bytes metadata
);
function announce(
uint256 schemeId,
address stealthAddress,
bytes memory ephemeralPubKey,
bytes memory metadata
) external;
}
// Planned API — illustrative, not yet exported by @sip-protocol/sdk
const announcer = createAnnouncerClient({
address: '0x...', // EIP-5564 announcer contract
provider: ethersProvider,
})
// After sending to stealth address
await announcer.announce({
schemeId: 1, // secp256k1
stealthAddress: ethAddress,
ephemeralPubKey: stealthAddress.ephemeralPublicKey,
metadata: '0x', // Optional: encrypted note
})
// Planned API — illustrative, not yet exported by @sip-protocol/sdk
const announcements = await announcer.getAnnouncements({
fromBlock: 19000000,
schemeId: 1,
})
// Each candidate is then checked with checkStealthAddress (view tag handled
// internally; view-only — viewing PRIVATE key + spending PUBLIC key)
const myPayments = announcements.filter(a =>
checkStealthAddress(a.stealthAddress, myViewingPrivateKey, mySpendingPublicKey)
)

ERC-6538 defines a registry for storing stealth meta-addresses on-chain.

interface IERC6538Registry {
event StealthMetaAddressSet(
address indexed registrant,
uint256 indexed schemeId,
bytes stealthMetaAddress
);
function registerKeys(
uint256 schemeId,
bytes memory stealthMetaAddress
) external;
function registerKeysOnBehalf(
address registrant,
uint256 schemeId,
bytes memory signature,
bytes memory stealthMetaAddress
) external;
function stealthMetaAddressOf(
address registrant,
uint256 schemeId
) external view returns (bytes memory);
}
// Planned API — illustrative, not yet exported by @sip-protocol/sdk
const registry = createRegistryClient({
address: '0x...', // ERC-6538 registry
signer: ethers.signer,
})
// Register your meta-address
await registry.registerKeys({
schemeId: 1, // secp256k1
stealthMetaAddress: encodedMetaAddress,
})
// Planned API — illustrative, not yet exported by @sip-protocol/sdk
const recipientMeta = await registry.stealthMetaAddressOf(
recipientEthAddress,
1 // schemeId
)
if (!recipientMeta) {
throw new Error('Recipient has no registered stealth meta-address')
}
KeySensitivityStorage Recommendation
Spending privateCriticalHardware wallet, encrypted vault
Viewing privateHighSecure storage, may share with auditors
Ephemeral privateTemporarySecurely wipe after use
ThreatMitigation
Spending key compromiseFull loss of funds - treat as seed phrase
Viewing key compromisePrivacy loss only - can regenerate
Ephemeral key reuseNever reuse - always generate fresh
View tag collision~0.4% false positives, full derivation required
  1. Key generation: Use cryptographically secure random number generator
  2. Key storage: Encrypt at rest, never log or transmit unencrypted
  3. Ephemeral keys: Generate fresh for each payment, securely wipe after
  4. View tags: Always check before full computation
  5. Registry: Verify on-chain meta-address matches expected format

SIP SDK uses secure memory practices:

import { secureWipe } from '@sip-protocol/sdk'
const privateKey = generatePrivateKey()
try {
// Use key
} finally {
secureWipe(privateKey) // Overwrite memory
}
FeatureSIP ProtocolUmbraFluidkey
Schemesecp256k1secp256k1secp256k1
View tagYes (1 byte)Yes (1 byte)Yes (1 byte)
Multi-chainYesEthereum onlyEthereum L2s
ERC-6538PlannedNoPartial
ComplianceViewing keysNoNo
Cross-chainNEAR IntentsNoNo
  1. Cross-chain: Works across 15+ chains via NEAR Intents
  2. Compliance: Viewing keys for selective disclosure
  3. Multi-curve: secp256k1 (EVM) and ed25519 (Solana/NEAR)
  4. SDK: Comprehensive TypeScript SDK with React hooks

SIP Protocol is fully compatible with EIP-5564 scheme 1, with these extensions:

ExtensionDescription
Ed25519 supportAdditional scheme for Solana/NEAR (not on-chain)
Cross-chainSettlement via NEAR Intents
Viewing keysAdditional layer for compliance
SIP encodingsip:chain:spending:viewing format

Before deploying to production:

  • Private keys never logged or transmitted
  • Ephemeral keys generated fresh per payment
  • Secure random number generator used
  • Keys wiped from memory after use
  • View tag checked before full derivation
  • Registry contract address verified
  • Announcer contract address verified
  • Chain IDs validated
  • Input validation on all parameters