Skip to content

Stealth Addresses

Stealth addresses provide recipient privacy by generating unique, one-time addresses for each transaction.

In current cross-chain swaps:

User has: shielded ZEC in z-address
User swaps: ZEC → SOL via intent
Refund goes to: t1ABC... (transparent, reused)
Chain analysis: "t1ABC received refunds 50 times"
→ Links to shielded activity

With SIP stealth addresses:

Each swap: generates fresh stealth address
Refund goes to: unique stealth_1, stealth_2, stealth_3...
Chain analysis: "No pattern - each address used once"
KeySymbolDescription
Spending private keypControls spending
Spending public keyP = p·GIn meta-address
Viewing private keyqScans for incoming txs
Viewing public keyQ = q·GIn meta-address
Ephemeral keyr, RPer-transaction
Stealth keya, AOne-time address
const { metaAddress, spendingPrivateKey, viewingPrivateKey } =
generateStealthMetaAddress('ethereum')
// Share meta-address publicly
console.log(metaAddress)
// sip:ethereum:0x02abc...def:0x03xyz...789
const { stealthAddress, ephemeralPublicKey } =
generateStealthAddress(recipientMetaAddress)
// Send funds to stealthAddress.address
// Publish ephemeralPublicKey alongside transaction
// Recipient scans for their transactions
const isMine = checkStealthAddress(
stealthAddress,
spendingPrivateKey,
viewingPrivateKey
)
if (isMine) {
// Derive private key to claim funds
const privateKey = deriveStealthPrivateKey(
stealthAddress,
ephemeralPublicKey,
spendingPrivateKey,
viewingPrivateKey
)
}
KeyGen():
1. p ← random_scalar() // Spending private
2. P ← p · G // Spending public
3. q ← random_scalar() // Viewing private
4. Q ← q · G // Viewing public
Return: ((P, Q), p, q)
GenerateStealth(P, Q):
1. r ← random_scalar() // Ephemeral private
2. R ← r · G // Ephemeral public
3. S ← r · P // Shared secret (ECDH)
4. h ← SHA256(S) // Hash shared secret
5. view_tag ← h[0] // First byte (optimization)
6. A ← Q + h · G // Stealth address
Return: (A, R, view_tag)
ScanForStealth(R, A, view_tag, p, q):
1. S' ← p · R // Recompute shared secret
2. h' ← SHA256(S')
// Quick reject using view tag
3. if h'[0] ≠ view_tag: return NOT_MINE
// Full verification
4. A' ← Q + h' · G
5. if A' ≠ A: return NOT_MINE
Return: MINE
DerivePrivateKey(R, p, q):
1. S ← p · R
2. h ← SHA256(S)
3. a ← q + h (mod n) // Stealth private key
Return: a
sip:<chain>:<spending_key>:<viewing_key>
Example:
sip:ethereum:0x02abc...def:0x03123...456
Chain IDDescription
ethereumEthereum mainnet
solanaSolana mainnet
zcashZcash (t-addresses)
nearNEAR Protocol
polygonPolygon PoS
PropertyGuarantee
UnlinkabilityDifferent addresses cannot be linked
Recipient PrivacyObserver cannot determine recipient
Sender DeniabilityAnyone could have generated the address
Forward SecrecyViewing key compromise doesn’t reveal past
InformationProtected?
Recipient identityYes
Link between txsYes
Recipient’s balanceYes
Viewing key holderYes
InformationProtected?Notes
Transaction amountNoUse commitments
Sender identityNoStealth is for recipient
Transaction timingNoBlock timestamp visible

The 8-bit view tag reduces scanning work by 256x:

  • Only 1/256 addresses need full verification
  • Leaks 8 bits of information (acceptable tradeoff)
  • Addresses sharing view tag are still unlinkable

Different chains derive addresses differently from stealth public key A:

ChainDerivation
Ethereumkeccak256(A)[12:32]
Zcash (t-addr)hash160(A) + version
Bitcoinhash160(A) + version
import {
generateStealthMetaAddress,
generateStealthAddress,
checkStealthAddress,
deriveStealthPrivateKey,
encodeStealthMetaAddress,
decodeStealthMetaAddress
} from '@sip-protocol/sdk'
// Recipient setup (one-time)
const meta = generateStealthMetaAddress('ethereum')
const encoded = encodeStealthMetaAddress(meta.metaAddress)
// Share `encoded` publicly
// Sender creates payment
const decoded = decodeStealthMetaAddress(encoded)
const { stealthAddress, ephemeralPublicKey } =
generateStealthAddress(decoded)
// Recipient scans
const isMine = checkStealthAddress(
stealthAddress,
meta.spendingPrivateKey,
meta.viewingPrivateKey
)
// Recipient claims
if (isMine) {
const pk = deriveStealthPrivateKey(
stealthAddress,
ephemeralPublicKey,
meta.spendingPrivateKey,
meta.viewingPrivateKey
)
// Use pk to spend from stealthAddress
}

SIP stealth addresses align with EIP-5564:

AspectEIP-5564SIP
Curvesecp256k1secp256k1
Key structure(P, Q)(P, Q)
Shared secretECDHECDH
View tag1 byte1 byte

Extension: SIP adds multi-chain support via sip: URI scheme.