advanced 45 min

Key generation & derivation

How rippled turns randomness (or a seed) into secp256k1 / ed25519 key pairs and account IDs.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Generate random and deterministic key pairs (secp256k1, ed25519).
  • Derive public keys and account IDs (RIPEMD160∘SHA256).
  • Understand seeds and multi-account derivation.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈45 min · Advanced · builds on Hash functions in XRPL

Where does an XRPL account actually come from? In this module you'll follow the path from randomness (or a seed) to a full key pair (for both secp256k1 and ed25519) and on to an account ID and address. You'll see how a seed makes the whole thing reproducible, and why the two signature schemes derive their keys so differently.

The Two Paths to Key Generation

Rippled supports two approaches to key generation:

Two paths to a key pair: path 1 is random (crypto_prng gives 32 secure bytes, used for new accounts and one-time keys), path 2 is deterministic (a seed hashed into 32 bytes, used for wallet recovery and many accounts from one seed); both meet at the SecretKey, which derives the PublicKey and then the AccountID

Random Key Generation

In brief: make a fresh key pair from cryptographically secure randomness.

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

In brief: regenerate the same key pair from a seed, every time.

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

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

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

Key idea. The seed is the real secret: it deterministically regenerates the key pair, which is why backing up the seed is the same as backing up the account.

Public Key Derivation

In brief: derive the public key from the private key (differently for secp256k1 and ed25519).

For Secp256k1

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

In brief: hash the public key (RIPEMD160 of SHA-256) to get the account ID.

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

The pipeline:

RIPESHA, the double hash: the 33-byte public key goes through SHA-256 to a 32-byte digest, then RIPEMD-160 reduces it to the 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 the Base58Check encoding module.

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";

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

// ❌ WRONG
void badExample() {
    SecretKey sk = randomSecretKey();
}

// ✅ CORRECT
void goodExample() {
    SecretKey sk = randomSecretKey();
}

Key Validation

bool validateKeys(PublicKey const& pk, SecretKey const& sk)
{
    auto derived = derivePublicKey(publicKeyType(pk).value(), sk);
    return derived == pk;
}

Seed Protection

class Seed {
    ~Seed() {
        secure_erase(buf_.data(), buf_.size());
    }
};

Performance Characteristics

Key Generation Speed

Ed25519:
- Secret key generation: ~50 µs
- Public key derivation:  ~50 µs
- Total: ~100 µs

Secp256k1:
- Secret key generation:  ~50 µs
- Public key derivation:  ~100 µs
- Total: ~150 µs

Caching Considerations

std::vector<std::pair<PublicKey, SecretKey>> generateKeys(int count)
{
    std::vector<std::pair<PublicKey, SecretKey>> keys;
    keys.reserve(count);

    for (int i = 0; i < count; ++i) {
        keys.push_back(randomKeyPair(KeyType::ed25519));
    }

    return keys;
}

Summary

This module followed an account from randomness to address. You generated key pairs both randomly and deterministically from a seed, for secp256k1 and ed25519, derived public keys and then account IDs (RIPEMD160 of SHA-256), and saw why the two schemes derive their keys differently. The seed is the real secret: it regenerates the whole key pair, which is why backing up the seed is backing up the account.

To remember:

  • Two schemes: secp256k1 (historical default) and ed25519 (public key starts with ED, 33 bytes)
  • Random path: randomSecretKey() pulls 32 bytes from the CSPRNG
  • Deterministic path: seed to key pair; ed25519 derives directly, secp256k1 goes through the Generator (root + derived keys)
  • Account ID = RIPEMD160(SHA256(pubkey)); the address is its Base58Check encoding
  • wallet_propose with "key_type" picks the algorithm
  • Same seed always regenerates the same account: backing up the seed IS backing up the account
  • Code: src/libxrpl/protocol/SecretKey.cpp, PublicKey.cpp
  • Watch out: key-type detection reads the public key prefix; treat a 0xED-prefixed key as secp256k1 and verification breaks

Next up. You can create secrets; can you keep them? Next: secure memory handling, or how rippled makes sure keys never outlive their use.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026