The four pillars of blockchain security and the lifecycle of a cryptographic key in XRPL.
What you'll learn
≈90 min · Intermediate · builds on Case study — the CheckCreate transactor
Watch this short video by XRPL Commons first, then dive into the details below.
Everything valuable in XRPL rests on cryptography, so let's start with what it actually promises. In this module you'll learn the four pillars (confidentiality, integrity, authenticity and non-repudiation) the hard math behind them, and how a single key travels from raw entropy to an account address. It's the ground floor for the whole cryptography phase.
In brief: the four guarantees cryptography provides: confidentiality, integrity, authenticity, and non-repudiation.
When you interact with the XRP Ledger, cryptography provides four essential security properties. Understanding these properties, what they mean, why they matter, and how they're achieved, is foundational to everything else in this module.
Confidentiality means that sensitive information remains hidden from those who shouldn't see it.
In XRPL Context
When two rippled nodes establish a connection, their communication is encrypted so that eavesdroppers on the network can't read their messages. The SSL/TLS layer provides this guarantee, ensuring that even though the internet is fundamentally public, peer-to-peer conversations remain private.
How It Works
// From src/libxrpl/basics/make_SSLContext.cpp
// SSL context is configured with strong cipher suites
auto ctx = boost::asio::ssl::context(boost::asio::ssl::context::tlsv12);
ctx.set_options(
boost::asio::ssl::context::default_workaround |
boost::asio::ssl::context::no_sslv2 |
boost::asio::ssl::context::no_sslv3 |
boost::asio::ssl::context::single_dh_use);
Rippled uses TLS 1.2 or higher with carefully selected cipher suites that provide forward secrecy, even if a node's long-term key is compromised later, past communications remain protected.
Why It Matters
Without confidentiality:
Integrity ensures that data hasn't been tampered with. A single flipped bit could change "send 1 XRP" to "send 100 XRP", or worse, redirect funds to a different account entirely.
In XRPL Context
When you receive a transaction, you need to know that every byte is exactly as the sender intended. Hash functions provide this guarantee by creating unique fingerprints of data that change completely if even a single bit is modified.
How It Works
// Transaction ID is computed from transaction data
uint256 transactionID = sha512Half(serializedTransaction);
// Any change to the transaction changes the ID completely
// Original: "Payment of 1 XRP" → ID: 0x7F3B9...
// Modified: "Payment of 2 XRP" → ID: 0xA21C4...
// Even 1 bit different: completely different hash
The SHA-512-Half hash function used throughout XRPL ensures that:
Why It Matters
Without integrity:
Real-World Example
// Original transaction
{
"Account": "rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C",
"Destination": "rLHzPsX6oXkzU9w7fvQqJvGjzVtL5oJ47R",
"Amount": "1000000" // 1 XRP
}
// Hash: 0x7F3B9E4A...
// Attacker tries to change amount
{
"Account": "rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C",
"Destination": "rLHzPsX6oXkzU9w7fvQqJvGjzVtL5oJ47R",
"Amount": "100000000" // 100 XRP - just one character different!
}
// Hash: 0xA21C4F8D... - completely different!
The hash changes so dramatically that any modification is immediately detectable.
Authenticity proves the identity of the sender. When a transaction claims to come from a particular account, cryptographic signatures prove that the holder of that account's private key actually created it.
In XRPL Context
Every transaction on XRPL must be signed with the private key corresponding to the sending account. Without this signature, the transaction is rejected. The signature is mathematical proof that only someone with the secret key could have created it.
How It Works
// Signing a transaction
Buffer signature = sign(
publicKey, // Can be shared publicly
secretKey, // Must remain secret
txData // Transaction to sign
);
// Anyone can verify the signature
bool valid = verify(
publicKey, // Sender's public key
txData, // Transaction data
signature // Signature to verify
);
The mathematical relationship between public and secret keys ensures:
Why It Matters
Without authenticity:
Attack Scenario (Prevented)
// Attacker tries to steal funds
Transaction fakeTx = {
"Account": "rVictimAccount...", // Victim's address
"Destination": "rAttackerAccount...",
"Amount": "1000000000" // Attacker tries to drain account
};
// Attacker creates fake signature
Buffer fakeSignature = attacker.tryToForge();
// Verification fails!
bool valid = verify(victimPublicKey, fakeTx, fakeSignature);
// Returns false - attacker doesn't have victim's secret key
Without the victim's secret key, it's computationally infeasible to create a valid signature. The attacker would need to solve the discrete logarithm problem, which would take longer than the age of the universe with all of humanity's computing power.
Non-repudiation means that once you've signed something, you can't later deny you signed it. The mathematics of digital signatures make this guarantee absolute: if your private key created a signature, there's no ambiguity, no room for doubt.
In XRPL Context
This property is crucial for a financial system where disputes might arise and proof of authorization is essential. If you signed a payment, that signature is irrefutable proof that you authorized it.
How It Works
Digital signatures create an undeniable link between:
// Alice signs a payment
auto [alicePubKey, aliceSecKey] = generateKeyPair(KeyType::ed25519);
Buffer sig = sign(alicePubKey, aliceSecKey, payment);
// Later, Alice claims: "I never authorized that payment!"
// But the signature proves otherwise:
bool proofOfAuthorization = verify(alicePubKey, payment, sig);
// Returns true - irrefutable proof Alice signed this
Why It Matters
Without non-repudiation:
Real-World Scenario
Key idea. Signatures give you authenticity and non-repudiation; hashing gives you integrity. Keep those two pairings straight and the rest of the cryptography phase falls into place.
These four properties aren't independent, they work together to create a complete security system:
Let's see all four pillars in action:
// 1. AUTHENTICITY - Alice creates and signs a transaction
auto tx = Payment{
.account = alice.address,
.destination = bob.address,
.amount = XRP(100)
};
Buffer signature = sign(alice.publicKey, alice.secretKey, tx);
// Only Alice's secret key can create this valid signature
// 2. INTEGRITY - Transaction is serialized and hashed
auto serialized = serialize(tx, signature);
uint256 txID = sha512Half(serialized);
// Any tampering changes the hash completely
// 3. NON-REPUDIATION - Signature proves Alice authorized this
bool authorized = verify(alice.publicKey, tx, signature);
// Alice cannot later deny signing this transaction
// 4. CONFIDENTIALITY - Transaction is sent to peers via encrypted connection
sslStream.write(serialized); // Protected by TLS encryption
// Network observers can't read transaction details
In brief: the hard problems (discrete logarithm, collision resistance) those guarantees rest on.
The four pillars rest on hard mathematical problems:
Given: PublicKey = SecretKey × G (where G is a generator point)
Hard Problem: Find SecretKey given only PublicKey
Computing PublicKey from SecretKey: microseconds
Computing SecretKey from PublicKey: longer than age of universe
This asymmetry enables public-key cryptography. You can freely share your public key, and no one can derive your secret key from it.
Given: hash = SHA512Half(data)
Hard Problems:
1. Find different data' where SHA512Half(data') = hash (preimage)
2. Find data and data' where SHA512Half(data) = SHA512Half(data') (collision)
Computing hash from data: microseconds
Finding data from hash: computationally infeasible
This one-way property makes hashes perfect for integrity checking.
Given: Encrypted = AES_Encrypt(key, plaintext)
Hard Problem: Find plaintext without key
With key: decryption in microseconds
Without key: trying all 2^256 possible keys
TLS negotiates shared secret keys that both parties know but attackers don't.
In brief: who and what you have to trust, and what you do not.
These four pillars enable a trust model where:
You don't have to trust:
You only have to trust:
This is the fundamental shift blockchain enables: from institutional trust to mathematical trust.
Rippled doesn't rely on one cryptographic technique, it uses multiple layers:
Even if one layer has a vulnerability, others provide protection.
Understanding these four pillars helps you:
When reading code:
When writing code:
When debugging:
This appendix provides a comprehensive guide to navigating rippled's cryptographic code. You'll learn where to find specific implementations, how files are organized, and which functions are most important to understand.
The
ed25519-donnaandlibsecp256k1implementations are external dependencies fetched via Conan (they are not in thesrc/tree).
Location: src/libxrpl/protocol/SecretKey.cpp
Key functions:
// Random key generation
SecretKey randomSecretKey();
// Deterministic key generation
SecretKey generateSecretKey(KeyType type, Seed const& seed);
std::pair<PublicKey, SecretKey> generateKeyPair(KeyType type, Seed const& seed);
// Public key derivation
PublicKey derivePublicKey(KeyType type, SecretKey const& sk);
// Signing
Buffer sign(PublicKey const& pk, SecretKey const& sk, Slice const& m);
// Why does sign() take the PUBLIC key too? Key-type detection: the public
// key's first byte says whether this is secp256k1 or ed25519, so sign()
// knows which algorithm to run without a separate parameter.
Buffer signDigest(PublicKey const& pk, SecretKey const& sk, uint256 const& digest);
Navigate to:
randomSecretKey() implementationgenerateKeyPair() for secp256k1generateKeyPair() for ed25519derivePublicKey() implementationssign() functionLocation: src/libxrpl/protocol/PublicKey.cpp
Key functions:
// Verification
bool verify(PublicKey const& pk, Slice const& m, Slice const& sig, bool canonical);
bool verifyDigest(PublicKey const& pk, uint256 const& digest, Slice const& sig, bool canonical);
// Canonicality checking
std::optional<ECDSACanonicality> ecdsaCanonicality(Slice const& sig);
bool ed25519Canonical(Slice const& sig);
// Key type detection
std::optional<KeyType> publicKeyType(Slice const& slice);
Navigate to:
verify() functionverifyDigest() for secp256k1ecdsaCanonicality() implementationLocation: src/libxrpl/protocol/digest.cpp
Key implementations:
// SHA-512-Half hasher
class sha512_half_hasher { /* ... */ };
// RIPESHA hasher
class ripesha_hasher { /* ... */ };
// Helper functions
uint256 sha512Half(Args const&... args);
uint256 sha512Half_s(Slice const& data); // Secure variant
Navigate to:
sha512_half_hasher classripesha_hasher classLocation: src/libxrpl/crypto/csprng.cpp
Key implementation:
class csprng_engine {
// Constructor: Initialize entropy
// operator(): Generate random bytes
// mix_entropy(): Add additional entropy
};
csprng_engine& crypto_prng(); // Global singleton
Navigate to:
csprng_engine class definitionoperator() (random byte generation)mix_entropy() (additional entropy)Location: src/libxrpl/protocol/tokens.cpp
Key functions:
// Base58Check encoding
std::string encodeBase58Token(TokenType type, void const* token, std::size_t size);
// Base58Check decoding
std::string decodeBase58Token(std::string const& s, TokenType type);
// Helpers
std::string toBase58(AccountID const& id);
std::optional<AccountID> parseBase58(std::string const& s);
Key generation:
src/libxrpl/protocol/SecretKey.cpp
→ randomSecretKey() // Random generation
→ generateKeyPair() // Deterministic from seed
Signing:
src/libxrpl/protocol/SecretKey.cpp
→ sign() // Sign message
→ signDigest() // Sign pre-hashed message
Verification:
src/libxrpl/protocol/PublicKey.cpp
→ verify() // Verify signature
→ verifyDigest() // Verify digest signature
Hashing:
src/libxrpl/protocol/digest.cpp
→ sha512Half() // SHA-512-Half hash
→ sha512Half_s() // Secure variant
include/xrpl/protocol/digest.h
→ Hash function interfaces
Random numbers:
src/libxrpl/crypto/csprng.cpp
→ crypto_prng() // Get CSPRNG instance
→ csprng_engine::operator() // Generate random bytes
Address encoding:
src/libxrpl/protocol/tokens.cpp
→ encodeBase58Token() // Encode to Base58Check
→ decodeBase58Token() // Decode from Base58Check
Transaction signing:
src/libxrpl/protocol/STTx.cpp
→ STTx::sign() // Sign transaction
→ STTx::checkSign() // Verify transaction signature
Peer handshake:
src/xrpld/overlay/detail/Handshake.cpp
→ makeSharedValue() // Derive session value
→ buildHandshake() // Create handshake headers
→ verifyHandshake() // Verify peer handshake
src/libxrpl/protocol/SecretKey.cpp:sign()ed25519_sign()src/libxrpl/protocol/PublicKey.cpp:verify()verifyDigest()ecdsaCanonicality()secp256k1_ecdsa_verify()src/libxrpl/protocol/AccountID.cpp:calcAccountID()ripesha_hashersrc/libxrpl/protocol/digest.cppsrc/libxrpl/protocol/tokens.cpp:encodeBase58Token()// Headers show:
// - Function declarations
// - Class interfaces
// - Documentation comments
// - Public API
// Good for:
// - Understanding what's available
// - API reference
// - Quick lookup
// Implementation shows:
// - Actual algorithms
// - Error handling
// - Edge cases
// - Performance optimizations
// Good for:
// - Understanding how it works
// - Debugging
// - Learning
// - Contributing
# Find all signing functions
grep -r "Buffer sign" src/libxrpl/protocol/
# Find CSPRNG usage
grep -r "crypto_prng()" src/
# Find signature verification
grep -r "verify.*signature" src/
# Find Base58 encoding
grep -r "encodeBase58" src/
# Find hash function usage
grep -r "sha512Half" src/
# See who wrote/modified code and why
git blame src/libxrpl/protocol/SecretKey.cpp
# See commit that introduced a function
git log -S "randomSecretKey" --source --all
# Generate ctags for symbol navigation
ctags -R src/ include/
# Jump to definition in vim
# Position cursor on function name, press Ctrl-]
// Look for:
class SomeKey {
~SomeKey() {
secure_erase(/* ... */);
}
};
// Look for:
if (operation_failed()) {
Throw<std::runtime_error>("Operation failed");
}
// Look for:
switch (publicKeyType(pk)) {
case KeyType::secp256k1:
// ...
case KeyType::ed25519:
// ...
}
src/test for usage examplesIn brief: how one key travels from raw entropy to a usable account address.
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.
Everything begins with randomness. Not the pseudo-randomness of Math.random() or std::rand(), but true cryptographic randomness, numbers that are fundamentally unpredictable.
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.
// ❌ WRONG - Predictable and insecure
void generateWeakKey() {
std::srand(std::time(nullptr)); // Predictable seed!
std::uint8_t buf[32];
for (auto& byte : buf)
byte = std::rand() % 256; // NOT cryptographically secure
}
// ✅ CORRECT - Cryptographically secure
SecretKey generateStrongKey() {
std::uint8_t buf[32];
beast::rngfill(buf, sizeof(buf), crypto_prng()); // CSPRNG
SecretKey sk{Slice{buf, sizeof(buf)}};
secure_erase(buf, sizeof(buf)); // Clean up
return sk;
}
In rippled, randomness comes from the crypto_prng() function, which wraps OpenSSL's RAND_bytes:
// 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;
}
What happens here:
crypto_prng() fills it with cryptographically secure random bytes from OpenSSLSecretKey objectWhen 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.
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.
// 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);
This asymmetry is what makes public-key cryptography possible.
XRPL supports two cryptographic algorithms, each with its own derivation process:
secp256k1: Elliptic Curve Point Multiplication
// 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}};
}
How it works:
Why it's secure:
Public = Secret × G is fastSecret from Public requires solving the discrete logarithm problemed25519: Curve25519 Operations
case KeyType::ed25519: {
unsigned char buf[33];
buf[0] = 0xED; // Type prefix
ed25519_publickey(sk.data(), &buf[1]);
return PublicKey(Slice{buf, sizeof(buf)});
}
How it works:
Why it's secure:
The public key can be:
No matter who has it or what they do with it, they can't derive your secret key. Your private identity remains private.
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.
Problem with pure randomness:
Generate Key1 → Secret1, Public1
Generate Key2 → Secret2, Public2
Generate Key3 → Secret3, Public3
To backup: Must save Secret1, Secret2, Secret3, ...
Solution with seeds:
Remember one seed → Can regenerate all keys
Seed → Key1 (ordinal 0)
→ Key2 (ordinal 1)
→ Key3 (ordinal 2)
→ ...
A seed is a small piece of data, typically 16 bytes, that serves as the "master secret" for an entire family of keys:
// 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};
}
}
}
For ed25519: Simple and Direct
// 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};
}
Simple, deterministic, and secure. Same seed always produces same key.
For secp256k1: Handling Edge Cases
// 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");
}
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 class enables creating multiple independent keys from one 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
Each ordinal produces a cryptographically independent key pair. This enables powerful features like:
A public key isn't an address. To get the human-readable XRPL address (starting with 'r'), we need one more transformation:
// From src/libxrpl/protocol/AccountID.cpp
AccountID calcAccountID(PublicKey const& pk)
{
ripesha_hasher h;
h(pk.data(), pk.size());
return AccountID{static_cast<ripesha_hasher::result_type>(h)};
}
Why two hash functions?
Why hash at all?
Let's trace a key from birth to address:
// 1. BIRTH: Generate random secret key
SecretKey secretKey = randomSecretKey();
// Result: 32 random bytes
// Example: 0x1a2b3c4d...
// 2. GROWTH: Derive public key
PublicKey publicKey = derivePublicKey(KeyType::ed25519, secretKey);
// Result: 33 bytes (0xED prefix + 32 bytes)
// Example: 0xED9434799226374926EDA3B54B1B461B4ABF7237962EEB1144C10A7CA6A9D32C64
// 3. IDENTITY: Calculate account ID
AccountID accountID = calcAccountID(publicKey);
// Result: 20 bytes
// Example: 0x8B8A6C533F09CA0E5E00E7C32AA7EC323485ED3F
// 4. PRESENTATION: Encode as address
std::string address = toBase58(accountID);
// Result: Human-readable address
// Example: rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C
Each step is irreversible:
The SecretKey class demonstrates proper lifecycle management:
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;
};
RAII (Resource Acquisition Is Initialization):
Usage pattern:
void signTransaction(Transaction const& tx)
{
SecretKey sk = loadKeyFromSecureStorage();
auto signature = sign(pk, sk, tx);
// sk destructor automatically called here
// Key material is securely erased
}
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.
How does rippled know which algorithm a key uses? The first byte:
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;
}
}
This automatic detection means higher-level code doesn't need to track key types, the keys themselves carry the information.
secp256k1 public key: 0x02[32 bytes] or 0x03[32 bytes]
ed25519 public key: 0xED[32 bytes]
This module laid the cryptographic groundwork. You learned the four pillars, confidentiality, integrity, authenticity, and non-repudiation, the hard mathematical problems they rest on (the discrete logarithm problem for signatures, collision resistance for hashing), and how a single key travels from raw entropy to an account address. Signatures give you authenticity and non-repudiation; hashing gives you integrity.
To remember:
include/xrpl/protocol/SecretKey.h and PublicKey.hwallet_propose generates keys; treat its output as radioactive on shared machinesNext up. You know what the crypto must guarantee. Next, the tool that does most of the work: XRPL's hash functions, and why SHA-512-Half is everywhere.
Resources
Assignments
0 of 2 complete