advanced 60 min

Crypto pitfalls & performance

Common cryptographic mistakes to avoid and the performance characteristics of XRPL's cryptography.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Recognise common cryptographic pitfalls and how XRPL avoids them.
  • Compare secp256k1 vs ed25519 signing / verification cost.
  • Reason about crypto performance at network scale.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈60 min · Advanced · builds on Transaction signing & verification

Cryptography rarely fails loudly; it fails subtly, years later, in production. This module is a tour of the ten ways crypto code goes wrong, each with the mistake, the danger, and the fix, followed by the real performance numbers behind XRPL's algorithm choices. Treat it as your crypto code-review checklist.

The ten pitfalls at a glance

# Pitfall The risk The reflex
1 weak randomness keys recoverable from the seed time crypto_prng(), never rand()
2 secrets left in memory dumps, swap, cold boot secure_erase + RAII (SecretKey)
3 non-canonical signatures transaction ID malleability mustBeFullyCanonical = true
4 key reuse across contexts cross-protocol replay one key per purpose + hash prefixes
5 variable-time comparison secrets leak byte by byte CRYPTO_memcmp
6 short keys brute force in seconds 256 bits, always
7 homemade crypto subtle, total breaks OpenSSL / libsodium only
8 ignored error returns using an uninitialized key check, then fail loudly
9 hardcoded secrets git history never forgets env vars, config, HSM
10 missing validation undefined behavior on bad input validate before use

Pitfall 1: Weak Random Number Generation

std::srand(time(nullptr)) seeds from the clock. An attacker who knows roughly when a key was generated tries every second in the window (a day is only 86,400 seeds), regenerates each candidate key, and compares against the public key. Recovery takes seconds.

// BAD: predictable
std::srand(std::time(nullptr));
for (auto& byte : secretKey) byte = std::rand() % 256;

// GOOD: cryptographically secure, then wiped
std::uint8_t buf[32];
beast::rngfill(buf, sizeof(buf), crypto_prng());
SecretKey sk{Slice{buf, sizeof(buf)}};
secure_erase(buf, sizeof(buf));

Detection: any srand, rand, or std::mt19937 near key material is a finding. Good signs: crypto_prng(), RAND_bytes(), randomSecretKey().

Watch out. Predictable randomness means recoverable keys. Always seed key generation from a cryptographically secure source, never from rand() or a timestamp.


Pitfall 2: Memory Leakage

Secrets that outlive their use end up in core dumps, swap files, hibernation images, and debugger reads. A function that returns while secretKeyHex still sits in a std::string has already lost.

// BAD: both copies of the secret survive the call
std::string secretKeyHex = loadFromConfig();
auto signature = sign(pk, parseHex(secretKeyHex), tx);

// GOOD: RAII wipes the key, and the temporary is erased explicitly
SecretKey sk = loadSecretKey();          // destructor erases
auto signature = sign(pk, sk, tx);
secure_erase(const_cast<char*>(secretKeyHex.data()), secretKeyHex.size());

Review checklist: temporary buffers erased, std::string secrets cleaned, secrets in RAII wrappers, no early return that skips cleanup. The Secure memory handling module covers the machinery in depth.


Pitfall 3: Accepting Non-Canonical Signatures

An secp256k1 signature can be malleated: transformed into a second, equally valid signature for the same transaction, which yields a different transaction ID. Applications tracking the original ID never see their confirmation; dependent transactions break.

// BAD: malleability allowed
return verify(tx.publicKey, tx.data, tx.signature, false);

// GOOD: fully canonical only
return verify(tx.publicKey, tx.data, tx.signature, true);

Detection: grep for verify(..., false) and verifyDigest(..., false); each one needs a written justification or a fix.


Pitfall 4: Key Reuse Across Contexts

One master key signing transactions, handshakes, and validations means one captured signature can potentially replay in another context, and one compromise takes down everything.

// GOOD: one key per purpose
struct NodeKeys {
    SecretKey accountKey;      // account transactions
    SecretKey nodeKey;         // peer communication
    SecretKey validationKey;   // validator messages
};

rippled adds a second layer of defence: hash prefixes. sha512Half(HashPrefix::TransactionId, ...) and sha512Half(HashPrefix::Manifest, ...) can never collide, so even identical payloads produce different signed digests across contexts.


Pitfall 5: Timing Attacks on Comparisons

A byte-by-byte comparison returns faster when the first byte is wrong than when the first ten are right. Measured over enough attempts, that timing difference spells out the secret one byte at a time.

// BAD: early exit leaks the position of the first mismatch
for (size_t i = 0; i < a.size(); ++i)
    if (a[i] != b[i]) return false;

// GOOD: constant time whatever the inputs
return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0;

Detection: memcmp and == on secret material are red flags; CRYPTO_memcmp / OPENSSL_memcmp are the fix.

Watch out. Compare secrets in constant time. A comparison that returns early leaks the secret one byte at a time.


Pitfall 6: Insufficient Key Length

A 64-bit key is 2^32 brute-force operations; a modern GPU does billions per second, so it falls in seconds. XRPL's standard is 256 bits (2^128 operations, quantum-resistant for the foreseeable future):

std::uint8_t strongKey[32];          // 32 bytes = 256 bits
crypto_prng()(strongKey, sizeof(strongKey));

Rule of thumb: 128 bits is the floor, 256 bits is the standard, more buys nothing.


Pitfall 7: Rolling Your Own Crypto

XOR loops, homemade key schedules, unauthenticated modes: custom crypto fails against attacks its author never heard of (padding oracles, timing channels, weak scheduling). Standard algorithms have survived decades of expert scrutiny.

Never implement your own encryption, hashes, signatures, RNGs, or KDFs. Always wrap OpenSSL, libsodium, or another audited library, and use standard algorithms (AES-GCM, SHA-2, ed25519).


Pitfall 8: Ignoring Error Returns

If RAND_bytes fails and nobody checks, the "key" is whatever was on the stack: zeros, predictable, or someone else's key material.

// GOOD: crypto failures are fatal, not warnings
if (RAND_bytes(buf, 32) != 1)
    Throw<std::runtime_error>("RNG failure - cannot continue");

Check the return of every RAND_*, secp256k1_*, ed25519_*, EVP_*, and SSL_* call, and fail loudly. Never continue past a failed crypto operation.


Pitfall 9: Hardcoded Secrets

A secret in source code lives forever in git history, ships in the binary as a recoverable string, appears in code reviews, and cannot be rotated without a release.

// GOOD: load at runtime from a store you can rotate
auto const seedStr = std::getenv("XRPL_SEED");        // env var
auto const seedStr = readSecureConfig("seed");        // file, mode 600
auto const seedStr = loadFromHSM();                   // hardware module

Habits: never commit secrets, restrict file permissions, prefer a secrets manager, rotate regularly, audit access.


Pitfall 10: Insufficient Validation

Decoding an address that was never validated is undefined behavior waiting to happen; skipping validation is how security checks get bypassed.

// GOOD: validate, then decode, then check existence
if (!isValidAddress(addressStr))
    throw std::invalid_argument("Invalid address format");
AccountID account = decodeAddress(addressStr);
if (!ledger.hasAccount(account))
    throw std::runtime_error("Account not found");

Validate everything: public keys (format, size, curve point), signatures (canonical, size, encoding), addresses (checksum, prefix), seeds (format, entropy), amounts (sign, limits).


Debugging and Development Toolbox

In brief: the tools for inspecting crypto behaviour; the Development & debugging techniques module covers general debugging in depth.

Tool Command / entry point Use it for
Trace logging [rpc_startup] + log_level ... trace watching sign/verify decisions live
Standalone mode ./xrpld --standalone + ledger_accept a private ledger for experiments
Test accounts ./xrpld wallet_propose ed25519 fresh keys of either type
gdb break sign if publicKeyType(pk) == KeyType::ed25519 stepping into crypto calls; x/32xb &secretKey to dump bytes
Valgrind --leak-check=full, --track-origins=yes leaked or uninitialized key material
AddressSanitizer -fsanitize=address at compile buffer overflows around key buffers
Unit tests ./xrpld --unittest=xrpl.protocol.SecretKey the crypto suites in src/test/protocol

Two patterns worth keeping at hand. First, a signature verification post-mortem is always the same checklist in order: valid public key type? expected signature size (64 bytes for ed25519)? canonical encoding (secp256k1)? Only then run verify and, on failure, suspect wrong key, wrong message, corrupted signature, or algorithm mismatch.

Second, checking canonicality directly:

auto canon = ecdsaCanonicality(signature);
if (canon == ECDSACanonicality::fullyCanonical)
    ;   // S <= order/2: safe against malleability
else
    ;   // canonical but malleable, or invalid DER: normalize or reject

Performance & Optimization

In brief: the real cost of signing and verifying, and how ed25519 and secp256k1 compare at network scale.

Signature algorithm performance

Signature scheme benchmarks: ed25519 halves key generation and derivation times and is roughly four times faster at signing and five times faster at verification than secp256k1, with smaller signatures and batch verification support.

ed25519 wins for three reasons: a curve designed in 2011 with performance in mind (secp256k1 dates from 2000), simpler point arithmetic that fits CPU caches better, and no DER encoding overhead (raw 64-byte signatures).

Verification is the bottleneck

Every validator verifies every transaction's signature, so verification cost multiplies across the network:

1,000 tx/s x 50 validators = 50,000 verifications/second
secp256k1: 50,000 x 500 us = 25 s of CPU time per second   (impossible)
ed25519:   50,000 x 100 us =  5 s of CPU time per second
Use ed25519 for Keep secp256k1 for
new accounts (recommended default) existing accounts (keys cannot change)
high-throughput applications cross-chain compatibility
anything performance-sensitive legacy integrations

Hash function performance

Hash function throughput compared: SHA-512 and SHA-512-Half both reach about 650 MB per second, SHA-256 about 450, while RIPEMD-160 and RIPESHA sit near 200 MB per second.

SHA-512-Half exists because 64-bit CPUs run SHA-512's 64-bit operations faster than SHA-256's 32-bit ones: you get SHA-512 speed with SHA-256's 32-byte output. Hashing a 1 KB transaction costs about 1.5 us, noise next to a 100-500 us signature verification; hashing is never the bottleneck.

Caching strategies

Three caches do most of the work, and they share one rule: only cache verified results, and expire entries.

Cache Keyed by Saves
public keys AccountID repeated derivation and ledger lookups
verification results transaction hash re-verifying the same transaction (10 min TTL)
SHAMap node hashes tree node recomputing unchanged subtrees on every change

The shape is always the same thread-safe read-through pattern:

std::optional<PublicKey> cached = keyCache.get(account);   // shared_lock
if (cached) return *cached;
auto pk = deriveFromLedger(account);                       // slow path
keyCache.put(account, pk);                                 // unique_lock, bounded size
return pk;

Batch and parallel processing

ed25519 (and only ed25519) supports batch verification: ed25519_sign_open_batch checks N signatures in roughly 1.2x the time of one, instead of N times. The catch: if the batch fails, one signature is bad and you must re-verify individually to find it.

Signature checks are CPU-bound and independent, so they parallelise perfectly across cores (#pragma omp parallel for, or std::async per transaction). Caches shared between those threads need their locks.

Optimization guidelines

Do Don't
default new accounts to ed25519 sacrifice security for speed (skip canonicality, shrink keys)
cache keys, verifications, hashes cache anything before it verified
batch ed25519 verifications micro-optimise hashing (~1 us) while signatures cost 100-500 us
profile before optimising forget thread safety around shared caches

Real-world scale

XRPL mainnet, order of magnitude:
150 tx/ledger x 40 validators / 4 s  =  1,500 verifications/second

ed25519:   1,500 x 100 us = 15 percent of one CPU
secp256k1: 1,500 x 500 us = 75 percent of one CPU

Same hardware, five times the headroom: that is why the recommendation for every new account is ed25519.


Summary

This module was a tour of the ways cryptography goes subtly wrong, and how XRPL avoids each: weak randomness, leaked secrets, non-canonical signatures, key reuse, and timing attacks. You also compared the real cost of secp256k1 and ed25519 signing and verification at network scale. Treat it as your crypto code-review checklist.

To remember:

  • Weak randomness means recoverable keys: only the CSPRNG (include/xrpl/crypto/csprng.h), never rand()
  • Wipe secrets with secure_erase + RAII (the Secure memory handling module's habit)
  • Enforce fully-canonical signatures (the Signing & verification module's rule)
  • One key, one context: hash prefixes / domain separation prevent reuse attacks
  • Compare secrets in constant time; early-exit comparisons leak bytes through timing
  • Never roll your own crypto: wrap audited libraries
  • Performance: ed25519 verifies faster, and verification dominates at network scale (every node verifies every transaction)
  • Watch out: pitfall checklists rot; re-run this list at every crypto-touching code review, not once

Next up. Cryptography closed. New phase: your node has identity and secrets, but zero friends. Next: the overlay network, rippled's own layer above TCP.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026