Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
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:
- Recipient publishes meta-address - Concatenation of spending and viewing public keys (66 bytes)
- Sender generates ephemeral key pair - One-time keys for this payment
- Sender computes shared secret - ECDH between ephemeral private key and recipient’s viewing public key
- Sender derives stealth address - Combines shared secret with recipient’s spending public key
- Sender announces on-chain - Publishes ephemeral public key and view tag
- Recipient scans announcements - Uses view tag for fast filtering (~6x faster), verifies matches
- Recipient computes stealth private key - Combines spending private key with shared secret to spend
API
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.
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
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.
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)
}
}
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