Stealth Addresses
One-time addresses for private receipt
Overview#
Mini Veil uses a Dual-Key Stealth Address Protocol (DKSAP) based on Ed25519 and X25519. Each payment generates a unique, one-time stealth address that only the intended recipient can detect and control. No on-chain link exists between the sender's transaction and the recipient's wallet.
Stealth Address Lifecycle
Sender
Ephemeral Key
Random X25519 keypair (r, R)
Shared Secret
X25519(r, viewingPubKey)
Tweak
reduceScalar(sha256(secret))
Stealth Key
spendingPubKey + tweak*G
Recipient
Shared Secret
X25519(viewingPriv, R)
View Tag
sha256(secret)[0:2] quick filter
Decrypt Key
AES-256-GCM(secret, encKey)
Stealth Priv
(spendingScalar + tweak) % L
Key Structure#
Each user has two key pairs:
| Key Pair | Algorithm | Purpose | |---|---|---| | Spending key | Ed25519 | Signs claim transactions from stealth addresses | | Viewing key | X25519 | Enables shared secret derivation for scanning |
Both are derived deterministically from a master seed:
masterSeed = SHA-256(walletSignature("Mini Veil master keys"))
spendingPriv = SHA-256("mini-veil-spending" || masterSeed)
viewingPriv = SHA-256("mini-veil-viewing" || masterSeed)
spendingPub = Ed25519.getPublicKey(spendingPriv)
viewingPub = X25519.getPublicKey(viewingPriv)
Sender: Generating a Stealth Address#
1. Generate ephemeral X25519 keypair (r, R = r * G)
2. sharedSecret = X25519(r, viewingPubKey)
3. tweak = reduceScalar(SHA-256(sharedSecret)) // mod Ed25519 order L
4. stealthPubKey = spendingPubKey + (tweak * G) // Ed25519 point addition
5. viewTag = u16(SHA-256(sharedSecret)[0:2]) // LE bytes
6. encryptedKey = AES-256-GCM(SHA-256(sharedSecret), r)
The sender publishes (R, viewTag, encryptedKey) in a StealthAnnouncement event.
Recipient: Scanning#
For each announcement (R, viewTag, encryptedKey):
1. sharedSecret = X25519(viewingPrivKey, R)
2. Quick filter: compare viewTag against SHA-256(sharedSecret)[0:2]
3. If match: tweak = reduceScalar(SHA-256(sharedSecret))
4. Decrypt r via AES-256-GCM(SHA-256(sharedSecret), encryptedKey)
5. stealthPrivScalar = (spendingScalar + tweak) % L
6. stealthPubKey = stealthPrivScalar * G
7. Check on-chain balance of stealthPubKey
View Tag Optimization
The view tag (2 bytes) enables fast rejection of non-matching announcements. For each announcement, the recipient computes one X25519 shared secret and compares 2 bytes. If no match, the announcement can be skipped without further computation. With 2-byte view tags, ~99.8% of non-matching announcements are filtered immediately.
BN254 Field Constraint#
The stealth address must fit within the BN254 scalar field modulus:
BN254_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n
~62% of Ed25519 public keys exceed this modulus. The generateStealthAddress function retries with new ephemeral keys until the resulting stealth address is within the field:
retries = 0
do {
ephemeralKey = generateRandomX25519Key()
stealthAddress = deriveStealthAddress(spendingPubKey, viewingPubKey, ephemeralKey)
retries++
} while (stealthAddress > BN254_FIELD && retries < 40)
40 retries give approximately 3×10⁻⁹ failure probability.
Claiming#
The recipient uses the stealth private key scalar to build a transaction:
claim_stealth(amount: u64)
Accounts:
stealth_address(Signer, mut) — the stealth address signing as fee payerrecipient(UncheckedAccount, mut) — the destination walletsystem_program
The stealth private key scalar is used to create a ScalarSigner:
scalarSigner = createScalarSigner(stealthPrivKeyScalar)
transaction.feePayer = stealthPublicKey
transaction.sign(scalarSigner)
connection.sendRawTransaction(transaction.serialize())
The claim drains all but the rent-exempt minimum and sweep fee (10,000 lamports) from the stealth address.
Metadata Address#
Recipients expose their stealth address capability via a MetaAddress:
interface MetaAddress {
spendingPubKey: Uint8Array // Ed25519 public key (32 bytes)
viewingPubKey: Uint8Array // X25519 public key (32 bytes)
}
This is stored on-chain in the KeyRegistry PDA and fetched by the sender when generating the stealth address.
Event Structure#
StealthAnnouncement {
ephemeral_pubkey: [u8; 32],
view_tag: u16,
encrypted_ephemeral_key: [u8; 80],
sender: Pubkey,
timestamp: i64,
}
Discriminator: [197, 85, 83, 203, 142, 88, 5, 176] (SHA-256 of "event:StealthAnnouncement")