Let's follow the lifecycle of a cryptographic key in rippled, from its creation as random noise to its role as the foundation of an account's identity. This journey touches every aspect of rippled's cryptographic system and shows how the pieces fit together.
Understanding this lifecycle is crucial because keys are the foundation of everything in XRPL. Every account, every transaction, every validator message—all depend on the proper generation, handling, and use of cryptographic keys.
The Journey Begins: Birth Through Randomness
Everything begins with randomness. Not the pseudo-randomness of Math.random() or std::rand(), but true cryptographic randomness—numbers that are fundamentally unpredictable.
Why Randomness Matters
If an attacker can predict your random numbers, they can predict your keys. If they can predict your keys, they own your account. The stakes couldn't be higher.
In rippled, randomness comes from the crypto_prng() function, which wraps OpenSSL's RAND_bytes:
What happens here:
Allocate buffer: A 32-byte buffer is created on the stack
Fill with randomness: crypto_prng() fills it with cryptographically secure random bytes from OpenSSL
Create SecretKey: The buffer is wrapped in a SecretKey object
Secure cleanup: The temporary buffer is securely erased to prevent key material from lingering in memory
Where Randomness Comes From
When you call crypto_prng(), OpenSSL pulls entropy from multiple sources:
This multi-source approach ensures that even if one entropy source is weak, others provide backup security.
Growth: From Secret to Public
With a secret key in hand, we need to derive its public key—the identity we can share with the world. This derivation is one of the beautiful ideas in modern cryptography: a mathematical function that's easy to compute in one direction but effectively impossible to reverse.
The One-Way Function
This asymmetry is what makes public-key cryptography possible.
Two Algorithms, Two Approaches
XRPL supports two cryptographic algorithms, each with its own derivation process:
secp256k1: Elliptic Curve Point Multiplication
How it works:
Elliptic curve has a special "generator" point G
Public key = Secret key × G (point multiplication on the curve)
Result is a point with X and Y coordinates
Compressed format stores X coordinate + one bit for Y (33 bytes total)
Prefix byte: 0x02 or 0x03 (indicates Y parity)
X coordinate: 32 bytes
Why it's secure:
Computing Public = Secret × G is fast
Computing Secret from Public requires solving the discrete logarithm problem
No known efficient algorithm exists for this problem
ed25519: Curve25519 Operations
How it works:
Uses Ed25519 curve operations (optimized variant of Curve25519)
Derives public key through curve arithmetic
Adds 0xED prefix byte to identify key type
Total 33 bytes (1 prefix + 32 public key)
Why it's secure:
Based on different curve with different security proofs
Specifically designed for signing (not encryption)
More resistant to implementation errors
The Beauty of One-Way Functions
The public key can be:
Posted on websites
Included in transactions
Sent to strangers
Stored in public databases
No matter who has it or what they do with it, they can't derive your secret key. Your private identity remains private.
Alternative Path: Deterministic Generation
Sometimes we don't want pure randomness. Sometimes we want to be able to recreate the exact same key pair from a remembered or stored value. This is where seed-based deterministic key generation comes in.
Why Deterministic Keys?
Problem with pure randomness:
Solution with seeds:
How Seeds Work
A seed is a small piece of data—typically 16 bytes—that serves as the "master secret" for an entire family of keys:
For ed25519: Simple and Direct
Simple, deterministic, and secure. Same seed always produces same key.
For secp256k1: Handling Edge Cases
Why the loop?
Not all 32-byte values are valid secret keys for secp256k1. The value must be less than the curve's "order" (a large prime number). If the hash result is too large, increment a counter and try again.
The odds of needing more than one attempt are vanishingly small (roughly 1 in 2^128), but the code handles it correctly.
The Generator Pattern
The Generator class enables creating multiple independent keys from one seed:
Each ordinal produces a cryptographically independent key pair. This enables powerful features like:
Hierarchical wallets: One seed, many accounts
Key rotation: Generate new keys without remembering multiple seeds
Backup simplicity: One seed backs up everything
From Key to Identity: Account IDs
A public key isn't an address. To get the human-readable XRPL address (starting with 'r'), we need one more transformation:
The RIPESHA Double Hash
Why two hash functions?
Compactness: 20 bytes instead of 33 bytes
Defense in depth: If SHA-256 is broken, RIPEMD-160 provides protection; if RIPEMD-160 is broken, SHA-256 does
Compatibility: Same scheme used by other blockchain systems
Why hash at all?
Shorter addresses are easier to use
Provides a level of indirection (can't derive public key from address)
Quantum-resistant: even if quantum computers break elliptic curve crypto, they can't derive the public key from the address alone
The Complete Lifecycle
Let's trace a key from birth to address:
Each step is irreversible:
Can't derive secret from public
Can't derive public from account ID
Can't derive account ID from address (but can decode)
Lifecycle Management: RAII and Secure Cleanup
The SecretKey class demonstrates proper lifecycle management:
Even if sign() throws an exception, the destructor still runs and the key is erased. This is defensive programming—making it impossible to forget cleanup.
Key Type Detection
How does rippled know which algorithm a key uses? The first byte:
This automatic detection means higher-level code doesn't need to track key types—the keys themselves carry the information.
┌─────────────────────────────────────────┐
│ OpenSSL Entropy Pool │
└─────────────────────────────────────────┘
↑ ↑ ↑
│ │ │
┌──────┴────┐ ┌───┴────┐ ┌────┴──────┐
│ Hardware │ │ OS │ │ Timing │
│ RNG │ │Entropy │ │ Jitter │
└───────────┘ └────────┘ └───────────┘
Hardware RNG:
- CPU instructions (RDRAND, RDSEED on x86)
- True random number generators
OS Entropy:
- /dev/urandom (Unix/Linux)
- CryptGenRandom (Windows)
- System events, disk I/O, network timing
Timing Jitter:
- High-resolution timers
- Thread scheduling randomness
- Cache timing variations
// Easy direction: secret → public (microseconds)
PublicKey publicKey = derivePublicKey(KeyType::ed25519, secretKey);
// Impossible direction: public → secret (longer than age of universe)
// There is NO function: secretKey = deriveSecretKey(publicKey);
// From src/libxrpl/protocol/SecretKey.cpp
case KeyType::secp256k1: {
secp256k1_pubkey pubkey_imp;
// Multiply the generator point G by the secret key
secp256k1_ec_pubkey_create(
secp256k1Context(),
&pubkey_imp,
reinterpret_cast<unsigned char const*>(sk.data()));
// Serialize to compressed format (33 bytes)
unsigned char pubkey[33];
std::size_t len = sizeof(pubkey);
secp256k1_ec_pubkey_serialize(
secp256k1Context(),
pubkey,
&len,
&pubkey_imp,
SECP256K1_EC_COMPRESSED); // Compressed format
return PublicKey{Slice{pubkey, len}};
}
case KeyType::ed25519: {
unsigned char buf[33];
buf[0] = 0xED; // Type prefix
ed25519_publickey(sk.data(), &buf[1]);
return PublicKey(Slice{buf, sizeof(buf)});
}
Generate Key1 → Secret1, Public1
Generate Key2 → Secret2, Public2
Generate Key3 → Secret3, Public3
To backup: Must save Secret1, Secret2, Secret3, ...
Remember one seed → Can regenerate all keys
Seed → Key1 (ordinal 0)
→ Key2 (ordinal 1)
→ Key3 (ordinal 2)
→ ...
// From src/libxrpl/protocol/SecretKey.cpp
std::pair<PublicKey, SecretKey>
generateKeyPair(KeyType type, Seed const& seed)
{
switch (type)
{
case KeyType::secp256k1: {
detail::Generator g(seed);
return g(0); // Generate the 0th key pair
}
case KeyType::ed25519: {
auto const sk = generateSecretKey(type, seed);
return {derivePublicKey(type, sk), sk};
}
}
}
// Hash the seed to get the secret key
SecretKey generateSecretKey(KeyType::ed25519, Seed const& seed)
{
auto const secret = sha512Half_s(makeSlice(seed)); // Secure hash
return SecretKey{secret};
}
// Must ensure result is valid secret key
SecretKey deriveDeterministicRootKey(Seed const& seed)
{
std::uint32_t ordinal = 0;
// Try up to 128 times to find valid key
for (int i = 0; i < 128; ++i)
{
// Create buffer with seed + ordinal
std::array<std::uint8_t, 20> buf;
std::copy(seed.data(), seed.data() + 16, buf.begin());
buf[16] = (ordinal >> 24) & 0xFF;
buf[17] = (ordinal >> 16) & 0xFF;
buf[18] = (ordinal >> 8) & 0xFF;
buf[19] = (ordinal >> 0) & 0xFF;
// Hash it
auto const secret = sha512Half(makeSlice(buf));
// Check if it's a valid secret key
if (isValidSecretKey(secret))
return SecretKey{secret};
// If not valid, try next ordinal
++ordinal;
}
// Should never happen (probability ~ 1 in 2^128)
Throw<std::runtime_error>("Failed to generate key from seed");
}
detail::Generator g(seed);
auto [pub0, sec0] = g(0); // First key pair
auto [pub1, sec1] = g(1); // Second key pair
auto [pub2, sec2] = g(2); // Third key pair
class SecretKey
{
private:
std::uint8_t buf_[32];
public:
SecretKey(Slice const& slice)
{
std::memcpy(buf_, slice.data(), sizeof(buf_));
}
~SecretKey()
{
// Automatically called when SecretKey goes out of scope
secure_erase(buf_, sizeof(buf_));
}
// Prevent copying to avoid multiple erasures
SecretKey(SecretKey const&) = delete;
SecretKey& operator=(SecretKey const&) = delete;
// Allow moving (transfers ownership)
SecretKey(SecretKey&&) noexcept = default;
SecretKey& operator=(SecretKey&&) noexcept = default;
};
void signTransaction(Transaction const& tx)
{
SecretKey sk = loadKeyFromSecureStorage();
auto signature = sign(pk, sk, tx);
// sk destructor automatically called here
// Key material is securely erased
}
std::optional<KeyType> publicKeyType(Slice const& slice)
{
if (slice.size() != 33)
return std::nullopt;
switch (slice[0])
{
case 0x02:
case 0x03:
return KeyType::secp256k1;
case 0xED:
return KeyType::ed25519;
default:
return std::nullopt;
}
}
secp256k1 public key: 0x02[32 bytes] or 0x03[32 bytes]
ed25519 public key: 0xED[32 bytes]