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.

ECDH
Key Scheme
99.6%
Scan Filter
One-Time
Address Type
Schnorr
Claim Proof

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

1
Bob registers a meta-address
Bob publishes two public keys: a spending key and a viewing key. This is done once and used for all incoming payments.
2
Alice derives a stealth address
Alice generates a random ephemeral key pair. She computes a shared secret via ECDH with Bob's viewing key, then derives a unique stealth address from this secret.
3
Alice sends and announces
Alice sends tokens to the stealth address and publishes an announcement containing her ephemeral public key and a view tag (first byte of shared secret).
4
Bob scans and claims
Bob uses his viewing key to scan announcements. The view tag filters 99.6% of irrelevant ones. For matches, Bob computes the stealth private key and claims the funds with a Schnorr proof.

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 for Performance

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:

FieldDescription
ephemeral_pubkeyAlice's random R = r·G
stealth_addressDerived one-time address
encrypted_amountElGamal ciphertext of the amount
view_tagFirst byte of shared secret (scan filter)
tokenToken contract address
timestampBlock timestamp
job_idOptional 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