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()
// From src/libxrpl/protocol/SecretKey.cppSecretKeyrandomSecretKey(){std::uint8_tbuf[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 bytes
Construct SecretKey: Wrap bytes in SecretKey object
Secure cleanup: Erase temporary buffer from memory
Return: SecretKey object (move semantics, no copy)
Why 32 Bytes?
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
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:
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
Ed25519: Simple Derivation
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
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)
The Generator Class
Deriving the Root Key
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
Compressed vs Uncompressed:
Why compress?
Saves 32 bytes per public key
Given X, only two possible Y values exist
Prefix bit tells us which one
For Ed25519
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:
RIPESHA: Double Hashing
The pipeline:
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:
Result:
We'll explore Base58Check encoding in detail in Chapter 7.
Complete Key Generation Examples
Example 1: Random Ed25519 Key
Example 2: Deterministic Secp256k1 Key
Example 3: Multiple Accounts from One Seed
Key Type Detection
Public Key Type Detection
Automatic Algorithm Selection
The secp256k1 Context
Security Considerations
Secret Key Storage
Key Validation
Seed Protection
Performance Characteristics
Key Generation Speed
Caching Considerations
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
Account IDs: Double hash (SHA-256 + RIPEMD-160) of public keys
Addresses: Base58Check encoding of account IDs
Two algorithms:
secp256k1 – complex, validated, widely used
ed25519 – simpler, faster, preferred
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.
// 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!
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};
}
// Seed structure
class Seed
{
private:
std::array<std::uint8_t, 16> buf_; // 128 bits
public:
// Construction, access, etc.
};
// 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};
}
// For secp256k1, need to handle curve order constraint
case KeyType::secp256k1: {
detail::Generator g(seed);
return g(0); // Generate the 0th key pair
}
// secp256k1 curve order
// Any secret key must be: 0 < key < order
static const uint256 CURVE_ORDER =
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141";
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;
}
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}};
}
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
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)});
}
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;
}
}
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);
}
}