Key Generation Pipeline
Introduction
Now that we understand where randomness comes from, let's explore how rippled transforms that randomness into cryptographic keys. This chapter traces the complete key generation pipeline—from random bytes to secret keys, from secret keys to public keys, and from public keys to account addresses.
We'll examine both random key generation (for new accounts) and deterministic key generation (for wallet recovery), and understand why rippled supports two different cryptographic algorithms.
The Two Paths to Key Generation
Rippled supports two approaches to key generation:
Path 1: Random Generation
crypto_prng() → SecretKey → PublicKey → AccountID
(Used for: New accounts, one-time keys)
Path 2: Deterministic Generation
Seed → SecretKey → PublicKey → AccountID
(Used for: Wallet recovery, multiple accounts from one seed)
Random Key Generation
The Simple Case: randomSecretKey()
randomSecretKey()
// From src/libxrpl/protocol/SecretKey.cpp
SecretKey randomSecretKey()
{
std::uint8_t buf[32];
beast::rngfill(buf, sizeof(buf), crypto_prng());
SecretKey sk(Slice{buf, sizeof(buf)});
secure_erase(buf, sizeof(buf));
return sk;
}
Step-by-step breakdown:
Allocate buffer: Create 32-byte buffer on stack
Fill with randomness: Use
crypto_prng()
to fill with random bytesConstruct SecretKey: Wrap bytes in
SecretKey
objectSecure cleanup: Erase temporary buffer from memory
Return:
SecretKey
object (move semantics, no copy)
Why 32 Bytes?
// 32 bytes = 256 bits
std::uint8_t buf[32];
// This provides 2^256 possible keys
// That's approximately 10^77 combinations
// More than atoms in the observable universe!
Security level:
128-bit security requires 2^128 operations to break
256 bits provides 2^256 operations (overkill, but standard)
Quantum computers reduce security by half (2^256 → 2^128)
So 256 bits ensures long-term security even against quantum attacks
Generating a Complete Key Pair
std::pair<PublicKey, SecretKey> randomKeyPair(KeyType type)
{
// Generate random secret key
SecretKey sk = randomSecretKey();
// Derive public key from secret
PublicKey pk = derivePublicKey(type, sk);
return {pk, sk};
}
Deterministic Key Generation from Seeds
What is a Seed?
A seed is a compact representation (typically 16 bytes) from which many keys can be derived:
// Seed structure
class Seed
{
private:
std::array<std::uint8_t, 16> buf_; // 128 bits
public:
// Construction, access, etc.
};
Why seeds matter:
Backup: Remember one seed → recover all keys
Portability: Move keys between wallets
Hierarchy: Generate multiple accounts from one seed
Generating Keys from Seeds: The Interface
std::pair<PublicKey, SecretKey>
generateKeyPair(KeyType type, Seed const& seed)
{
switch (type)
{
case KeyType::secp256k1:
return generateSecp256k1KeyPair(seed);
case KeyType::ed25519:
return generateEd25519KeyPair(seed);
}
}
Ed25519: Simple Derivation
// For ed25519, derivation is straightforward
case KeyType::ed25519: {
// Hash the seed to get secret key
auto const sk = generateSecretKey(type, seed);
// Derive public key from secret
return {derivePublicKey(type, sk), sk};
}
SecretKey generateSecretKey(KeyType::ed25519, Seed const& seed)
{
// Simply hash the seed
auto const secret = sha512Half_s(makeSlice(seed));
return SecretKey{secret};
}
Why this works:
SHA-512-Half is a one-way function
Same seed always produces same secret key
Different seeds produce uncorrelated secret keys
No special validation needed (all 32-byte values are valid ed25519 keys)
Secp256k1: Complex Derivation
// For secp256k1, need to handle curve order constraint
case KeyType::secp256k1: {
detail::Generator g(seed);
return g(0); // Generate the 0th key pair
}
Why more complex?
Not all 32-byte values are valid secp256k1 secret keys. The value must be:
Greater than 0
Less than the curve order (a large prime number)
// secp256k1 curve order
// Any secret key must be: 0 < key < order
static const uint256 CURVE_ORDER =
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141";
The Generator Class
class Generator
{
private:
Seed seed_;
public:
explicit Generator(Seed const& seed) : seed_(seed) {}
// Generate the n-th key pair
std::pair<PublicKey, SecretKey> operator()(std::uint32_t ordinal)
{
// Derive root key from seed
SecretKey rootKey = deriveRootKey(seed_, ordinal);
// Derive public key
PublicKey publicKey = derivePublicKey(KeyType::secp256k1, rootKey);
return {publicKey, rootKey};
}
};
Deriving the Root Key
SecretKey deriveRootKey(Seed const& seed, std::uint32_t ordinal)
{
// Try up to 128 times to find valid key
for (int i = 0; i < 128; ++i)
{
// Create buffer: seed (16 bytes) + ordinal (4 bytes)
std::array<std::uint8_t, 20> buf;
// Copy seed
std::copy(seed.data(), seed.data() + 16, buf.begin());
// Append ordinal (big-endian)
buf[16] = (ordinal >> 24) & 0xFF;
buf[17] = (ordinal >> 16) & 0xFF;
buf[18] = (ordinal >> 8) & 0xFF;
buf[19] = (ordinal >> 0) & 0xFF;
// Hash it
auto const candidate = sha512Half(makeSlice(buf));
// Check if valid secp256k1 secret key
if (isValidSecretKey(candidate))
return SecretKey{candidate};
// Not valid, increment ordinal and try again
++ordinal;
}
// Should never reach here (probability ~ 1 in 2^128)
Throw<std::runtime_error>("Failed to derive key from seed");
}
bool isValidSecretKey(uint256 const& candidate)
{
// Must be in range: 0 < candidate < CURVE_ORDER
return candidate > 0 && candidate < CURVE_ORDER;
}
Why this loop?
The probability that a random 256-bit value is >= CURVE_ORDER is approximately 1 in 2^128. This is so unlikely that we almost never need a second try, but the code handles it correctly.
Incrementing ordinal: If the first hash isn't valid, we increment the ordinal and try again. This ensures:
Deterministic behavior (same seed always produces same result)
Eventually finds a valid key (extremely high probability on first try)
No bias in the resulting key distribution
Public Key Derivation
For Secp256k1
PublicKey derivePublicKey(KeyType::secp256k1, SecretKey const& sk)
{
secp256k1_pubkey pubkey_imp;
// Perform elliptic curve point multiplication: PublicKey = SecretKey × G
secp256k1_ec_pubkey_create(
secp256k1Context(),
&pubkey_imp,
reinterpret_cast<unsigned char const*>(sk.data()));
// Serialize to compressed format
unsigned char pubkey[33];
std::size_t len = sizeof(pubkey);
secp256k1_ec_pubkey_serialize(
secp256k1Context(),
pubkey,
&len,
&pubkey_imp,
SECP256K1_EC_COMPRESSED); // 33 bytes: prefix + X coordinate
return PublicKey{Slice{pubkey, len}};
}
Compressed vs Uncompressed:
Uncompressed: 0x04 | X (32 bytes) | Y (32 bytes) = 65 bytes
Compressed: 0x02/0x03 | X (32 bytes) = 33 bytes
Prefix byte indicates Y parity:
- 0x02: Y is even
- 0x03: Y is odd
Why compress?
Saves 32 bytes per public key
Given X, only two possible Y values exist
Prefix bit tells us which one
For Ed25519
PublicKey derivePublicKey(KeyType::ed25519, SecretKey const& sk)
{
unsigned char buf[33];
buf[0] = 0xED; // Type prefix marker
// Derive public key using Ed25519 algorithm
ed25519_publickey(sk.data(), &buf[1]);
return PublicKey(Slice{buf, sizeof(buf)});
}
Simpler than secp256k1:
No compression needed (Ed25519 public keys are naturally 32 bytes)
No serialization complexity
Just prepend type marker (0xED)
Account ID Generation
Once we have a public key, we derive the account ID:
AccountID calcAccountID(PublicKey const& pk)
{
ripesha_hasher h;
h(pk.data(), pk.size());
return AccountID{static_cast<ripesha_hasher::result_type>(h)};
}
RIPESHA: Double Hashing
class ripesha_hasher
{
private:
openssl_sha256_hasher sha_;
public:
void operator()(void const* data, std::size_t size)
{
// First: SHA-256
sha_(data, size);
}
operator result_type()
{
// Get SHA-256 result
auto const sha256_result =
static_cast<openssl_sha256_hasher::result_type>(sha_);
// Second: RIPEMD-160 of SHA-256
ripemd160_hasher ripe;
ripe(sha256_result.data(), sha256_result.size());
return static_cast<result_type>(ripe);
}
};
The pipeline:
Public Key (33 bytes)
↓
SHA-256
↓
32-byte digest
↓
RIPEMD-160
↓
20-byte Account ID
Why double hash?
Defense in depth: If one hash is broken, the other provides protection
Compactness: 20 bytes is shorter than 32 bytes
Quantum resistance: Even if quantum computers break elliptic curve crypto, they can't reverse the hash to get the public key
Address Encoding
The final step is encoding the account ID as a human-readable address:
std::string toBase58(AccountID const& accountID)
{
return encodeBase58Token(
TokenType::AccountID,
accountID.data(),
accountID.size());
}
Result:
Account ID (20 bytes): 0x8B8A6C533F09CA0E5E00E7C32AA7EC323485ED3F
Address: rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C
We'll explore Base58Check encoding in detail in Chapter 7.
Complete Key Generation Examples
Example 1: Random Ed25519 Key
// Generate random ed25519 key pair
auto [publicKey, secretKey] = randomKeyPair(KeyType::ed25519);
// Derive account ID
AccountID accountID = calcAccountID(publicKey);
// Encode as address
std::string address = toBase58(accountID);
std::cout << "Public Key: " << strHex(publicKey) << "\n";
std::cout << "Account ID: " << strHex(accountID) << "\n";
std::cout << "Address: " << address << "\n";
// Output:
// Public Key: ED9434799226374926EDA3B54B1B461B4ABF7237962EEB1144C10A7CA6A9D32C64
// Account ID: 8B8A6C533F09CA0E5E00E7C32AA7EC323485ED3F
// Address: rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C
Example 2: Deterministic Secp256k1 Key
// Create seed from passphrase (EXAMPLE ONLY - don't do this in production!)
Seed seed = generateSeedFromPassphrase("my secret passphrase");
// Generate deterministic key pair
auto [publicKey, secretKey] = generateKeyPair(KeyType::secp256k1, seed);
// Same seed always produces same keys
auto [publicKey2, secretKey2] = generateKeyPair(KeyType::secp256k1, seed);
assert(publicKey == publicKey2);
assert(secretKey == secretKey2);
// Derive account
AccountID accountID = calcAccountID(publicKey);
std::string address = toBase58(accountID);
std::cout << "Address: " << address << "\n";
Example 3: Multiple Accounts from One Seed
Seed seed = /* ... */;
// Create generator
detail::Generator gen(seed);
// Generate multiple accounts
auto [pub0, sec0] = gen(0); // First account
auto [pub1, sec1] = gen(1); // Second account
auto [pub2, sec2] = gen(2); // Third account
// Each has different address
AccountID acc0 = calcAccountID(pub0);
AccountID acc1 = calcAccountID(pub1);
AccountID acc2 = calcAccountID(pub2);
std::cout << "Account 0: " << toBase58(acc0) << "\n";
std::cout << "Account 1: " << toBase58(acc1) << "\n";
std::cout << "Account 2: " << toBase58(acc2) << "\n";
// All can be recovered from the same seed!
Key Type Detection
How does rippled know which algorithm a key uses?
Public Key Type Detection
std::optional<KeyType> publicKeyType(Slice const& slice)
{
if (slice.size() != 33)
return std::nullopt;
// Check first byte
switch (slice[0])
{
case 0x02:
case 0x03:
return KeyType::secp256k1;
case 0xED:
return KeyType::ed25519;
default:
return std::nullopt;
}
}
First byte encoding:
0x02: secp256k1 compressed public key (Y is even)
0x03: secp256k1 compressed public key (Y is odd)
0xED: ed25519 public key
Automatic Algorithm Selection
Buffer sign(PublicKey const& pk, SecretKey const& sk, Slice const& m)
{
// Automatically detect which algorithm to use
auto const type = publicKeyType(pk.slice());
switch (*type)
{
case KeyType::ed25519:
return signEd25519(pk, sk, m);
case KeyType::secp256k1:
return signSecp256k1(pk, sk, m);
}
}
Higher-level code doesn't need to track key types—the keys themselves carry the information.
The secp256k1 Context
secp256k1_context const* secp256k1Context()
{
// Thread-local context for performance
static thread_local std::unique_ptr<
secp256k1_context,
decltype(&secp256k1_context_destroy)>
context{
secp256k1_context_create(
SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY),
&secp256k1_context_destroy
};
return context.get();
}
Why a context?
secp256k1 library requires a context object
Context caches precomputed tables for performance
Thread-local: each thread gets its own context (thread safety)
Automatically destroyed when thread exits (RAII)
Security Considerations
Secret Key Storage
// ❌ WRONG - Key remains in memory
void badExample() {
SecretKey sk = randomSecretKey();
// Use key...
// sk remains in memory after function returns!
}
// ✅ CORRECT - RAII ensures cleanup
void goodExample() {
SecretKey sk = randomSecretKey();
// Use key...
// sk destructor automatically erases key material
}
Key Validation
// Always validate keys before use
bool validateKeys(PublicKey const& pk, SecretKey const& sk)
{
// Verify public key is correct derivation from secret key
auto derived = derivePublicKey(publicKeyType(pk).value(), sk);
return derived == pk;
}
Seed Protection
Seeds are even more sensitive than individual keys:
// One compromised seed = all derived keys compromised
class Seed {
~Seed() {
secure_erase(buf_.data(), buf_.size());
}
};
Performance Characteristics
Key Generation Speed
// Benchmark results (approximate, hardware-dependent):
Ed25519:
- Secret key generation: ~50 microseconds
- Public key derivation: ~50 microseconds
- Total: ~100 microseconds
Secp256k1:
- Secret key generation: ~50 microseconds
- Public key derivation: ~100 microseconds
- Total: ~150 microseconds
Ed25519 is faster for key generation.
Caching Considerations
// If generating many keys, consider batching
std::vector<std::pair<PublicKey, SecretKey>> generateKeys(int count)
{
std::vector<std::pair<PublicKey, SecretKey>> keys;
keys.reserve(count); // Pre-allocate
for (int i = 0; i < count; ++i) {
keys.push_back(randomKeyPair(KeyType::ed25519));
}
return keys;
}
Summary
Key generation in rippled involves:
Randomness: Cryptographically secure random bytes from
crypto_prng()
Secret keys: 32 bytes of random or deterministically-derived data
Public keys: Derived via one-way function (elliptic curve operations)
Account IDs: Double hash (SHA-256 + RIPEMD-160) of public keys
Addresses: Base58Check encoding of account IDs
Two algorithms:
secp256k1: More complex, requires validation, widely used
ed25519: Simpler, faster, recommended for new accounts
Two approaches:
Random: Maximum security, requires backup of each key
Deterministic: One seed recovers many keys, convenient for wallets
In the next chapter, we'll see how these keys are used to create and verify digital signatures—the mathematical proof of authorization.
Last updated