|
| 1 | +--- |
| 2 | +title: "Stellar Cryptography" |
| 3 | +description: "Design rationale, view tag derivation, and RFC-compatible cryptography for Stellar stealth payments." |
| 4 | +--- |
| 5 | + |
| 6 | +Wraith Protocol implements a non-interactive stealth payment scheme on Stellar. This page documents the cryptography decisions behind the implementation and exactly where each concept is realized in the SDK. |
| 7 | + |
| 8 | +## Why ed25519? |
| 9 | + |
| 10 | +Unlike EVM environments, which rely on `secp256k1`, the Stellar network uses the **ed25519** curve for all account addressing and signatures. |
| 11 | +To ensure that stealth accounts are valid Stellar accounts that can sign transactions, the protocol's stealth derivations must perform point addition on the ed25519 curve. |
| 12 | + |
| 13 | +- **Curve definition**: `scalar.ts:1` (via [@noble/curves/ed25519](https://github.com/paulmillr/noble-curves)) |
| 14 | + |
| 15 | +## X25519 ECDH and Edwards-to-Montgomery Conversion |
| 16 | + |
| 17 | +Standard ed25519 points (in Edwards form) are optimized for signing, not for Diffie-Hellman key exchange. To securely establish a shared secret between sender and receiver without interaction, we must use **X25519** ECDH. |
| 18 | +This requires converting the public and private ed25519 keys from Edwards coordinates to Montgomery coordinates, as specified in [RFC 7748](https://datatracker.ietf.org/doc/html/rfc7748). |
| 19 | + |
| 20 | +- **Edwards-to-Montgomery conversion**: `stealth.ts:91-92` |
| 21 | +- **X25519 shared secret**: `stealth.ts:20` and `stealth.ts:93` |
| 22 | + |
| 23 | +## Domain Separation Prefixes |
| 24 | + |
| 25 | +We use domain-separation prefixes in SHA-256 hashes to prevent cryptographic collisions between different key derivation phases. |
| 26 | + |
| 27 | +- `wraith:spending:`: Separates the derivation of the spending seed (`keys.ts:25`). |
| 28 | +- `wraith:viewing:`: Separates the derivation of the viewing seed (`keys.ts:26`). |
| 29 | +- `wraith:scalar:`: Prevents the hash scalar from colliding with the base shared secret before it's reduced modulo L (`scalar.ts:202`, `scalar.ts:220`). |
| 30 | +- `wraith:stellar:view-tag:v2:`: Domains the derivation for the 1-byte view tag (`stealth.ts:8`). |
| 31 | +- `wraith:tag:`: The legacy v1 view tag prefix (`stealth.ts:9`). |
| 32 | + |
| 33 | +## View Tag Derivation |
| 34 | + |
| 35 | +To avoid performing an expensive X25519 ECDH operation for every incoming transaction, the sender derives a 1-byte **view tag** and publishes it alongside their ephemeral public key. |
| 36 | + |
| 37 | +**Derivation:** |
| 38 | +``` |
| 39 | +view_tag = SHA-256("wraith:stellar:view-tag:v2:" || R_ephemeral || V_recipient)[0] |
| 40 | +``` |
| 41 | + |
| 42 | +- **Implementation**: `stealth.ts:99` |
| 43 | +- **Performance impact**: This creates a cheap public prefilter before the X25519 shared secret computation (`scan.ts:12`). |
| 44 | +- **False-positive rate**: A 1-byte tag produces a false-positive rate of `1/256` (~0.39%). For non-matching announcements, the protocol skips the expensive elliptic curve operations 99.61% of the time. |
| 45 | + |
| 46 | +```mermaid |
| 47 | +sequenceDiagram |
| 48 | + participant Network |
| 49 | + participant Scanner |
| 50 | + Network->>Scanner: Fetch Announcements (R, view_tag) |
| 51 | + Note over Scanner: Compare cheap view_tag first |
| 52 | + alt Match view_tag |
| 53 | + Scanner->>Scanner: X25519(v, R) -> shared_secret |
| 54 | + Scanner->>Scanner: Derive expected stealth address |
| 55 | + alt Match Address |
| 56 | + Scanner->>Network: Recovered match! |
| 57 | + end |
| 58 | + else Mismatch view_tag |
| 59 | + Note over Scanner: Skip (99.61% of non-matches) |
| 60 | + end |
| 61 | +``` |
| 62 | + |
| 63 | +## Private Scalar vs. Seeds and RFC 8032 |
| 64 | + |
| 65 | +Standard ed25519 signing libraries expect a 32-byte seed as the private key, which they hash (via SHA-512) to produce both the private scalar and a deterministic nonce. |
| 66 | + |
| 67 | +In our non-interactive stealth scheme, the stealth private key is a *derived scalar*, not a raw seed: |
| 68 | +``` |
| 69 | +stealth_scalar = (spending_scalar + hash_scalar) mod L |
| 70 | +``` |
| 71 | + |
| 72 | +Because we only hold the resulting scalar, we cannot use off-the-shelf seed-based signing APIs. Instead, the SDK exposes a custom `signWithScalar` function to deterministically sign transactions using a raw scalar directly, while maintaining strict [RFC 8032](https://datatracker.ietf.org/doc/html/rfc8032) compatibility for ed25519 signatures. |
| 73 | + |
| 74 | +- **`signWithScalar` implementation**: `scalar.ts:251` |
| 75 | + |
| 76 | +## Meta-Address Encoding |
| 77 | + |
| 78 | +To accept stealth payments, users publish a single "meta-address" that encapsulates both their spending and viewing public keys. |
| 79 | + |
| 80 | +- **Prefix**: `st:xlm:` (`constants.ts:43`). |
| 81 | +- **Encoding**: Consists of the prefix concatenated with the hex-encoded 32-byte spending public key and the 32-byte viewing public key (`meta-address.ts:10`). |
| 82 | +- **Stellar StrKey compatibility**: To turn the final derived public stealth key into a standard Stellar address format (`G...`), we utilize Stellar's `StrKey` encoding logic (`scalar.ts:171`). |
| 83 | + |
| 84 | +## Key Derivation Overview |
| 85 | + |
| 86 | +```mermaid |
| 87 | +flowchart TD |
| 88 | + S(Sender) -->|Generates| r(Ephemeral Private Key 'r') |
| 89 | + r --> R(Ephemeral Public Key 'R') |
| 90 | + S --> |Recipient's| V(Viewing Public Key 'V') |
| 91 | + S --> |Recipient's| K(Spending Public Key 'K') |
| 92 | + r & V --> X25519(X25519 ECDH) |
| 93 | + X25519 --> SS(Shared Secret) |
| 94 | + R & V --> VT(View Tag) |
| 95 | + SS --> |Hash mod L| HS(Hash Scalar) |
| 96 | + HS & K --> |Point Addition| SP(Stealth Public Key) |
| 97 | + SP --> |StrKey Encoding| SA(Stellar Address 'G...') |
| 98 | +``` |
0 commit comments