Protocol 03 · DKSAP

Stealth Address

A one-time address per payment, derivable only by the intended recipient. The recipient publishes a single meta-address and scans with a view key. Senders derive fresh destinations by ECDH.

meta-address

Each recipient publishes a meta-address — a pair of public keys — once. All subsequent payments derive unique on-chain addresses from this pair.

spend keyauthorizes outflows
The long-lived key that signs spends from stealth addresses. Kept in cold storage. Its public half is half of the meta-address.
view keyauthorizes scanning
A read-only key that detects incoming payments. Can be shared with auditors/watch-only wallets without exposing funds. Second half of the meta-address.
meta_address = (S, V) where S = s·G, V = v·G
sspend secret key (scalar)vview secret key (scalar)GRistretto255 basepoint

dual-key stealth address protocol (DKSAP)

KIRITE implements the standard DKSAP scheme. Sender derives a shared secret via ECDH against the view key, hashes it into a scalar, and adds that scalar's point to the spend key.

01
sender generates ephemeral
e ∈ [0, ℓ), E = e·G. Fresh randomness per payment.
02
sender computes shared secret
ss = H(e·V) = H(v·E). Both parties arrive at the same scalar via ECDH.
03
derive stealth pubkey
P = ss·G + S. This is the one-time destination. Publicly unlinkable to the meta-address without v.
04
sender publishes ephemeral
E is emitted on-chain (in the StealthRegistry). Without it, the recipient cannot rediscover the payment.
05
recipient scans
For each registry entry, recipient computes ss = H(v·E), candidate P = ss·G + S. Match ⇒ payment is mine.
06
recipient spends
Stealth private key is p = ss + s. Recipient signs with p. The spend key s never appears on-chain directly.

derivation — concrete

e ← random scalar
E = e · G
ss = SHA512(e · V)   // clamped to scalar mod ℓ
P = ss · G + S
p = ss + s   (mod ℓ)
Pstealth public key (on-chain destination)pstealth private key (recipient only)Eephemeral public key (posted to registry)

view-only audit

The v key detects payments but cannot spend them. A user can hand v to a tax tool or watch-only wallet without exposing any ability to move funds. The spend keys stays in cold storage.

scanning cost

Each registry entry costs one scalar multiplication + one hash + one point addition to check. Modern hardware scans ~50k entries per second on a single core. The SDK caches the latest scanned slot per view key.

// client-side scan (simplified)
for (const entry of registry) {
  const ss = hashToScalar(pointMul(viewSecret, entry.ephemeral));
  const candidate = pointAdd(basepointMul(ss), spendPubkey);
  if (equalPoints(candidate, entry.stealth)) {
    // payment is ours
    const stealthPriv = scalarAdd(ss, spendSecret);
    matches.push({ entry, stealthPriv });
  }
}

registry structure

fieldsizepurpose
owner32recipient who published meta-address
spend_pubkey32S (Ristretto compressed)
view_pubkey32V (Ristretto compressed)
ephemeral_pubkey32E per payment
stealth_pubkey32P derived destination
claimed1set after recipient sweeps

integration with shield pool

A standard private-pay flow composes shield pool withdrawal with a stealth recipient address:

  1. Sender derives P from recipient's meta-address.
  2. Sender calls withdraw on the shield pool withrecipient = P.
  3. The pool pays P; E is emitted to the registry.
  4. Recipient scans, finds the payment, derives p, sweeps funds.

gotchas

  • registry bloat. Every payment writes an entry. Recipients rotate meta-addresses periodically to bound scan costs.
  • no forward secrecy of the view key. If vis compromised, all past and future payments become linkable. Spend capability, however, stays with s.
  • address reuse defeats privacy. Sweeping stealth funds back to a known address immediately links the payment. Send through the shield pool first.