Skip to main content

Overview

StealthAddress implements EIP-5564 stealth addresses for privacy-preserving, non-interactive address generation. Senders can generate one-time addresses that only the intended recipient can detect and spend from, without requiring any prior communication.
import * as StealthAddress from '@tevm/voltaire/StealthAddress'
import * as Secp256k1 from '@tevm/voltaire/Secp256k1'

// Recipient: Generate key pairs and meta-address
const spendingPrivKey = crypto.getRandomValues(new Uint8Array(32))
const viewingPrivKey = crypto.getRandomValues(new Uint8Array(32))

const spendingPubKey = StealthAddress.compressPublicKey(
  Secp256k1.derivePublicKey(spendingPrivKey)
)
const viewingPubKey = StealthAddress.compressPublicKey(
  Secp256k1.derivePublicKey(viewingPrivKey)
)
const metaAddress = StealthAddress.generateMetaAddress(spendingPubKey, viewingPubKey)

// Sender: Generate stealth address from recipient's meta-address
const ephemeralPrivKey = crypto.getRandomValues(new Uint8Array(32))
const { stealthAddress, ephemeralPublicKey, viewTag } =
  StealthAddress.generateStealthAddress(metaAddress, ephemeralPrivKey)

// Sender publishes: stealthAddress, ephemeralPublicKey, viewTag on-chain

// Recipient: Scan announcements to detect payments
const result = StealthAddress.checkStealthAddress(
  viewingPrivKey,
  ephemeralPublicKey,
  viewTag,
  spendingPubKey,
  stealthAddress
)

if (result.isForRecipient) {
  const stealthPrivKey = StealthAddress.computeStealthPrivateKey(
    spendingPrivKey,
    result.stealthPrivateKey!
  )
  // Use stealthPrivKey to spend from stealthAddress
}

How It Works

EIP-5564 stealth addresses use ECDH (Elliptic Curve Diffie-Hellman) key exchange:
  1. Recipient publishes meta-address - Concatenation of spending and viewing public keys (66 bytes)
  2. Sender generates ephemeral key pair - One-time keys for this payment
  3. Sender computes shared secret - ECDH between ephemeral private key and recipient’s viewing public key
  4. Sender derives stealth address - Combines shared secret with recipient’s spending public key
  5. Sender announces on-chain - Publishes ephemeral public key and view tag
  6. Recipient scans announcements - Uses view tag for fast filtering (~6x faster), verifies matches
  7. Recipient computes stealth private key - Combines spending private key with shared secret to spend

API

generateMetaAddress

Creates a 66-byte stealth meta-address from spending and viewing public keys.
function generateMetaAddress(
  spendingPubKey: SpendingPublicKey,
  viewingPubKey: ViewingPublicKey
): StealthMetaAddress
const spendingPubKey = StealthAddress.compressPublicKey(
  Secp256k1.derivePublicKey(spendingPrivKey)
)
const viewingPubKey = StealthAddress.compressPublicKey(
  Secp256k1.derivePublicKey(viewingPrivKey)
)

const metaAddress = StealthAddress.generateMetaAddress(spendingPubKey, viewingPubKey)
console.log(metaAddress.length) // 66
Throws InvalidPublicKeyError if either public key is not 33 bytes.

parseMetaAddress

Parses a 66-byte meta-address into its component public keys.
function parseMetaAddress(metaAddress: StealthMetaAddress): {
  spendingPubKey: SpendingPublicKey
  viewingPubKey: ViewingPublicKey
}
const { spendingPubKey, viewingPubKey } = StealthAddress.parseMetaAddress(metaAddress)
console.log(spendingPubKey.length) // 33
console.log(viewingPubKey.length) // 33
Throws InvalidStealthMetaAddressError if meta-address is not 66 bytes.

generateStealthAddress

Generates a stealth address from a meta-address using an ephemeral private key.
function generateStealthAddress(
  metaAddress: StealthMetaAddress,
  ephemeralPrivateKey: Uint8Array
): GenerateStealthAddressResult
Returns:
  • stealthAddress - 20-byte Ethereum address
  • ephemeralPublicKey - 33-byte compressed public key (publish on-chain)
  • viewTag - 1-byte view tag for fast scanning (publish on-chain)
const ephemeralPrivKey = crypto.getRandomValues(new Uint8Array(32))

const result = StealthAddress.generateStealthAddress(metaAddress, ephemeralPrivKey)

console.log(result.stealthAddress)      // AddressType (20 bytes)
console.log(result.ephemeralPublicKey)  // 33 bytes
console.log(result.viewTag)             // 0-255
Throws StealthAddressGenerationError if:
  • Ephemeral private key is not 32 bytes
  • Meta-address is invalid
  • Cryptographic operation fails

checkStealthAddress

Checks if a stealth address announcement belongs to the recipient.
function checkStealthAddress(
  viewingPrivateKey: Uint8Array,
  ephemeralPublicKey: EphemeralPublicKey,
  viewTag: ViewTag,
  spendingPublicKey: SpendingPublicKey,
  stealthAddress: AddressType
): CheckStealthAddressResult
Returns:
  • isForRecipient - true if the stealth address belongs to this recipient
  • stealthPrivateKey - 32-byte shared secret hash (use with computeStealthPrivateKey)
const result = StealthAddress.checkStealthAddress(
  viewingPrivKey,
  announcement.ephemeralPublicKey,
  announcement.viewTag,
  spendingPubKey,
  announcement.stealthAddress
)

if (result.isForRecipient) {
  console.log('Found stealth payment!')
  // result.stealthPrivateKey contains the shared secret hash
}
The view tag enables fast rejection: ~255/256 non-matching addresses are rejected after a single byte comparison, before expensive EC operations.

computeStealthPrivateKey

Computes the full stealth private key for spending.
function computeStealthPrivateKey(
  spendingPrivateKey: Uint8Array,
  hashedSharedSecret: Uint8Array
): Uint8Array
Implements: stealthPrivateKey = (spendingPrivateKey + hashedSharedSecret) mod n
if (result.isForRecipient) {
  const stealthPrivKey = StealthAddress.computeStealthPrivateKey(
    spendingPrivKey,
    result.stealthPrivateKey!
  )
  // Use stealthPrivKey to sign transactions from stealthAddress
}

compressPublicKey

Compresses a 64-byte uncompressed public key to 33-byte compressed format.
function compressPublicKey(uncompressed: Uint8Array): Uint8Array
const uncompressed = Secp256k1.derivePublicKey(privateKey) // 64 bytes
const compressed = StealthAddress.compressPublicKey(uncompressed) // 33 bytes

// First byte is 0x02 (even y) or 0x03 (odd y)
console.log(compressed[0]) // 2 or 3

decompressPublicKey

Decompresses a 33-byte compressed public key to 64-byte uncompressed format.
function decompressPublicKey(compressed: Uint8Array): Uint8Array
const compressed = new Uint8Array(33)
compressed[0] = 0x02 // even y prefix
// ... set x-coordinate

const uncompressed = StealthAddress.decompressPublicKey(compressed)
console.log(uncompressed.length) // 64

computeViewTag

Extracts the view tag (first byte) from a hashed shared secret.
function computeViewTag(hashedSharedSecret: Uint8Array): ViewTag
import * as Keccak256 from '@tevm/voltaire/Keccak256'

const sharedSecret = new Uint8Array(32)
const hash = Keccak256.hash(sharedSecret)
const viewTag = StealthAddress.computeViewTag(hash)

console.log(viewTag >= 0 && viewTag <= 255) // true

parseAnnouncement

Parses announcement data into ephemeral public key and view tag.
function parseAnnouncement(announcement: Uint8Array): {
  ephemeralPublicKey: EphemeralPublicKey
  viewTag: ViewTag
}
const announcement = new Uint8Array(34) // 33 + 1
const { ephemeralPublicKey, viewTag } = StealthAddress.parseAnnouncement(announcement)

console.log(ephemeralPublicKey.length) // 33
console.log(typeof viewTag) // 'number'

Types

StealthMetaAddress

66-byte stealth meta-address (spending public key + viewing public key).
type StealthMetaAddress = Uint8Array & {
  readonly __tag: "StealthMetaAddress"
}

SpendingPublicKey

33-byte compressed secp256k1 public key for deriving stealth addresses.
type SpendingPublicKey = Uint8Array & {
  readonly __tag: "SpendingPublicKey"
}

ViewingPublicKey

33-byte compressed secp256k1 public key for scanning the blockchain.
type ViewingPublicKey = Uint8Array & {
  readonly __tag: "ViewingPublicKey"
}

EphemeralPublicKey

33-byte one-time public key generated by sender and announced on-chain.
type EphemeralPublicKey = Uint8Array & {
  readonly __tag: "EphemeralPublicKey"
}

ViewTag

Single byte (0-255) for fast rejection of non-matching stealth addresses.
type ViewTag = number

GenerateStealthAddressResult

interface GenerateStealthAddressResult {
  stealthAddress: AddressType
  ephemeralPublicKey: EphemeralPublicKey
  viewTag: ViewTag
}

CheckStealthAddressResult

interface CheckStealthAddressResult {
  isForRecipient: boolean
  stealthPrivateKey?: Uint8Array  // Hashed shared secret (offset)
}

StealthAnnouncement

interface StealthAnnouncement {
  ephemeralPublicKey: EphemeralPublicKey
  viewTag: ViewTag
  stealthAddress: AddressType
}

Constants

const STEALTH_META_ADDRESS_SIZE = 66     // spendingPubKey (33) + viewingPubKey (33)
const COMPRESSED_PUBLIC_KEY_SIZE = 33    // 0x02/0x03 prefix + x-coordinate
const UNCOMPRESSED_PUBLIC_KEY_SIZE = 64  // x (32) + y (32)
const PRIVATE_KEY_SIZE = 32
const VIEW_TAG_SIZE = 1
const SCHEME_ID = 1                      // ERC-5564 scheme for SECP256k1 with view tags

Errors

StealthAddressError

Base error for all stealth address operations.
import { StealthAddressError } from '@tevm/voltaire/StealthAddress'

try {
  StealthAddress.checkStealthAddress(...)
} catch (error) {
  if (error instanceof StealthAddressError) {
    console.log(error.code)    // 'CHECK_FAILED'
    console.log(error.message)
  }
}

InvalidStealthMetaAddressError

Thrown when meta-address format or length is invalid.
import { InvalidStealthMetaAddressError } from '@tevm/voltaire/StealthAddress'

InvalidPublicKeyError

Thrown when public key format or length is invalid.
import { InvalidPublicKeyError } from '@tevm/voltaire/StealthAddress'

StealthAddressGenerationError

Thrown when stealth address generation fails.
import { StealthAddressGenerationError } from '@tevm/voltaire/StealthAddress'

InvalidAnnouncementError

Thrown when announcement format is invalid.
import { InvalidAnnouncementError } from '@tevm/voltaire/StealthAddress'

Privacy Considerations

View Tag Tradeoff: The view tag reduces scanning overhead by ~6x but leaks 1 byte of the shared secret. This is considered acceptable for the performance benefit. Key Separation: Use separate spending and viewing keys. The viewing key can be delegated to a scanning service without risking funds. On-chain Privacy: Stealth addresses are unlinkable to the recipient’s public identity. Only the recipient (with viewing key) can identify which payments are theirs. Metadata Leakage: The sender’s identity may still be linkable through transaction graph analysis, timing, or gas funding patterns.

See Also