The split-key protocol
Rotation (Stellar) and mint creation (Solana) make a seller's copy of a key worthless. Some keys, such as a wallet's, must stay powerful forever. For those, split-key lets a searcher find a vanity address for a key it never learns.
Idea
Ed25519 public keys are points s·B on a curve. Points can be added, and so can their scalars. The client picks a secret scalar s_client and publishes only A_client = s_client·B. The searcher tries offsets i until A_client + i·B encodes to a matching address, then proves it found one, without revealing i. After payment it sends i. Only the client can compute the final key s_client + i; the searcher knows i but not s_client.
Notation
B is the edwards25519 base point and ℓ = 2^252 + 27742317777372353535851937790883648493 its order. Points are 32-byte RFC 8032 encodings. Scalars are 32 bytes, little-endian, canonical (< ℓ).
1. Client share
s_client= 64 random bytes read as a little-endian integer, reduced mod ℓ. It is not clamped, and 0 is rejected.A_client = s_client·B.- A fresh share for every job; implementations refuse to reuse one. The share is stored encrypted.
2. Search
- The searcher validates
A_client(section 6) and searchesi ∈ [1, ℓ)forQ = A_client + i·B. Optionally it also tests−Q. The result is(i, neg), withA_total = neg ? −Q : Q. - Every candidate from a GPU is re-derived and re-matched on the CPU before it is accepted.
iis never logged and is released only after payment.
3. Proof of knowledge, before payment
Let P = (neg ? −A_total : A_total) − A_client = i·B. The searcher sends a standard Ed25519 signature by the scalar i over a context message, so the client can check it with any Ed25519 library:
M = "splitkey/pok/v1" ‖ network ‖ job_id ‖ A_client ‖ A_total ‖ neg
‖ SHA-256(canonical pattern) ‖ expiry
r = SHA-512(nonce_key ‖ i ‖ M) mod ℓ deterministic, never reused
R = r·B; c = SHA-512(R ‖ P ‖ M) mod ℓ; z = (r + c·i) mod ℓ
proof = R ‖ z a 64-byte Ed25519 signatureThe client computes P itself and checks: Ed25519.verify(pubkey = P, msg = M, sig = proof), that A_total matches the pattern, and that P is not the identity. This proves the searcher knows i and reveals nothing about it. The nonce must never repeat: two proofs with the same r would reveal i. That is why r is derived from a secret nonce key.
4. Delivery and assembly, after payment
The searcher sends i. The client checks (neg ? −1 : 1)·(A_client + i·B) == A_total, then computes s_total = neg ? −(s_client + i) : (s_client + i) mod ℓ, checks s_total·B == A_total, uses it, and wipes both scalars. JavaScript and Go cannot guarantee that memory is wiped, and the security notes say so.
5. Signing with the assembled scalar
The result is an expanded Ed25519 key (a scalar, not a seed), so it signs like this:
prefix = SHA-512("splitkey/prefix/v1" ‖ s_total)[32..64]
r = SHA-512(prefix ‖ msg) mod ℓ; R = r·B
k = SHA-512(R ‖ A_total ‖ msg) mod ℓ; S = (r + k·s_total) mod ℓ
signature = R ‖ S verifies anywhereThe export format splitkey-expanded-v1 (scalar, prefix, address) is deliberately unlike a Stellar S… seed or a Solana 64-byte keypair. Wallets that import only seeds cannot import it, which is the main practical limit of split-key.
6. Point validation
Checking 8·A ≠ O is not enough: A = s·B + T with a torsion point T passes it but has no usable scalar. The searcher requires all of:
- a canonical encoding (decode, re-encode, compare bytes);
- a point on the curve;
- membership in the prime-order subgroup:
8⁻¹·(8·A) == Awith8⁻¹ mod ℓ. This also rejects small-order points and the identity.
7. Network notes
- Stellar. The address is
G+ base32 of0x30 ‖ key ‖ CRC16. The second character is always A–D, and there is no case-insensitive speedup (base32 is uppercase). An account atA_totalis activated in one transaction that the user's wallet funds, signed by the wallet and byA_total(section 5). - Solana. The address is the base58 of the key as a big-endian integer, 43 or 44 characters, with a very uneven first character. Case-insensitive patterns get
2^kvariants for thekletters that exist in both cases. For mints, plain mint keys already suffice; split-key matters for wallets.
Status and review
This draft follows an internal design review. The planned library is Go (on filippo.io/edwards25519) and TypeScript (on @noble/curves), both MIT, with shared test vectors. Proofs and signatures will verify with the standard crypto/ed25519 and @noble/curves verifiers. Comments are welcome at hello@keyplates.com.