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.
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.
e ∈ [0, ℓ), E = e·G. Fresh randomness per payment.ss = H(e·V) = H(v·E). Both parties arrive at the same scalar via ECDH.P = ss·G + S. This is the one-time destination. Publicly unlinkable to the meta-address without v.E is emitted on-chain (in the StealthRegistry). Without it, the recipient cannot rediscover the payment.ss = H(v·E), candidate P = ss·G + S. Match ⇒ payment is mine.p = ss + s. Recipient signs with p. The spend key s never appears on-chain directly.derivation — concrete
E = e · G
ss = SHA512(e · V) // clamped to scalar mod ℓ
P = ss · G + S
p = ss + s (mod ℓ)
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
| field | size | purpose |
|---|---|---|
owner | 32 | recipient who published meta-address |
spend_pubkey | 32 | S (Ristretto compressed) |
view_pubkey | 32 | V (Ristretto compressed) |
ephemeral_pubkey | 32 | E per payment |
stealth_pubkey | 32 | P derived destination |
claimed | 1 | set after recipient sweeps |
integration with shield pool
A standard private-pay flow composes shield pool withdrawal with a stealth recipient address:
- Sender derives
Pfrom recipient's meta-address. - Sender calls
withdrawon the shield pool withrecipient = P. - The pool pays
P;Eis emitted to the registry. - 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 withs. - address reuse defeats privacy. Sweeping stealth funds back to a known address immediately links the payment. Send through the shield pool first.