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()

// 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:

  1. Allocate buffer: Create 32-byte buffer on stack

  2. Fill with randomness: Use crypto_prng() to fill with random bytes

  3. Construct SecretKey: Wrap bytes in SecretKey object

  4. Secure cleanup: Erase temporary buffer from memory

  5. 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?

  1. Defense in depth: If one hash is broken, the other provides protection

  2. Compactness: 20 bytes is shorter than 32 bytes

  3. 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:

  1. Randomness: Cryptographically secure random bytes from crypto_prng()

  2. Secret keys: 32 bytes of random or deterministically-derived data

  3. Public keys: Derived via one-way function (elliptic curve operations)

  4. Account IDs: Double hash (SHA-256 + RIPEMD-160) of public keys

  5. 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