Stealth Payments
Stealth payments let you send tokens to someone without anyone else knowing who received them. The recipient publishes a meta-address once, and senders derive a fresh one-time stealth address for each payment using ECDH key derivation.
The Problem
On a public blockchain, when Alice sends tokens to Bob's address 0xBob, everyone can see that Bob received funds. Even if the amount is encrypted, the recipient's identity is public.
Stealth payments solve this by generating a fresh, unique address for each payment that only Bob can identify and claim.
How It Works
Cryptographic Scheme
Meta-Address Registration
Bob's meta-address:
spending_pubkey = sk_spend · G
viewing_pubkey = sk_view · G
scheme_id = 1 (Obelysk ECDH)
Stealth Address Derivation (Sender)
1. Generate ephemeral key pair: (r, R = r·G)
2. Shared secret: S = r · viewing_pubkey = r · sk_view · G
3. View tag: first byte of Poseidon(S) (for fast scanning)
4. Stealth address: addr = Poseidon(S || spending_pubkey) mod n
Scanning (Recipient)
For each announcement (R, view_tag, stealth_addr):
1. S' = sk_view · R (same shared secret via ECDH)
2. If first_byte(Poseidon(S')) ≠ view_tag → skip (99.6% filter)
3. Compute addr' = Poseidon(S' || spending_pubkey) mod n
4. If addr' == stealth_addr → this payment is for me!
Claiming (Recipient)
stealth_sk = Poseidon(shared_secret || sk_spend) mod n
Schnorr proof of knowledge of stealth_sk → claim funds
View tags are the first byte of the ECDH shared secret. They filter out 255/256 = 99.6% of irrelevant announcements without performing full ECDH. This makes scanning fast even with millions of announcements on-chain.
On-Chain Announcement
Each stealth payment creates an announcement with:
| Field | Description |
|---|---|
ephemeral_pubkey | Alice's random R = r·G |
stealth_address | Derived one-time address |
encrypted_amount | ElGamal ciphertext of the amount |
view_tag | First byte of shared secret (scan filter) |
token | Token contract address |
timestamp | Block timestamp |
job_id | Optional job association (for payment tracking) |
SDK Usage
// Register meta-address (recipient, one-time setup)
await obelysk.stealth.register(spendPubKey, viewPubKey);
// Send stealth payment (sender)
const result = await obelysk.stealth.send({
to: '0xRECIPIENT_ADDRESS',
token: 'sage',
amount: '1000',
});
console.log('Stealth address:', result.stealthAddress);
// Scan for incoming payments (recipient)
const announcements = await obelysk.stealth.scan({
metaAddress: myMetaAddress,
fromBlock: 100000,
});
console.log('Found:', announcements.length, 'payments');
// Claim payments
for (const ann of announcements) {
await obelysk.stealth.claim({
announcement: ann,
recipient: '0xMY_WALLET',
});
}
Contract Details
- Address:
0x077ee4c38201b4e45b643f4af56ff6daf780260e9c8a281f3536fb711afcaea8 - Scheme: ECDH over STARK curve (EIP-5564 adapted for Starknet)
- Claim proof: Schnorr proof of stealth private key ownership
Next Steps
- Confidential Transfer — encrypted P2P with ElGamal
- ElGamal Encryption — the encryption behind amount hiding
- Privacy Pools — shielded deposits with Merkle proofs