Performance & Optimization

Introduction

Cryptography is essential for security, but it comes with computational cost. In a high-throughput blockchain like XRPL, cryptographic operations—signing, verifying, hashing—happen thousands of times per second. Understanding performance characteristics and optimization opportunities is crucial for building efficient systems.

This chapter explores the performance implications of different cryptographic choices and strategies for optimizing without compromising security.

Signature Algorithm Performance

Benchmark Results

// Approximate timings on modern hardware (2023-era CPU)

Operation              secp256k1    ed25519     Winner
─────────────────────────────────────────────────────────
Key generation         ~100 μs      ~50 μs      ed25519
Public key derivation  ~100 μs      ~50 μs      ed25519
Signing                ~200 μs      ~50 μs      ed25519 (4x faster)
Verification           ~500 μs      ~100 μs     ed25519 (5x faster)
Batch verification     N/A          Available   ed25519
─────────────────────────────────────────────────────────
Public key size        33 bytes     33 bytes    Tie
Signature size         ~71 bytes    64 bytes    ed25519

Why Ed25519 is Faster

1. Simpler mathematics:

// secp256k1:
// - Complex curve operations
// - Modular arithmetic with large primes
// - DER encoding/decoding overhead

// ed25519:
// - Optimized curve (Curve25519)
// - Simpler point arithmetic
// - No encoding overhead (raw bytes)

2. Better caching:

// ed25519 operations fit better in CPU cache
// Fewer memory accesses
// More predictable branching

3. Modern design:

// Ed25519 designed in 2011 with performance in mind
// secp256k1 designed in 2000 before modern optimizations

Verification is the Bottleneck

// In XRPL consensus:
// Every validator verifies EVERY transaction signature
// 1000 tx/s × 50 validators = 50,000 verifications/second

// With secp256k1:
50,000 × 500 μs = 25,000,000 μs = 25 seconds of CPU time

// With ed25519:
50,000 × 100 μs = 5,000,000 μs = 5 seconds of CPU time

// Ed25519 saves 20 seconds of CPU time per second!
// Allows for higher throughput or more validators

When to Use Each Algorithm

Use ed25519:

  • New accounts (recommended)

  • High-throughput applications

  • When performance matters

  • Modern systems

Use secp256k1:

  • Compatibility requirements

  • Existing accounts (can't change)

  • Cross-chain interoperability

  • Legacy systems

Hash Function Performance

Benchmark Results

// Throughput on modern 64-bit CPU

Algorithm        Throughput       Notes
────────────────────────────────────────────────────
SHA-512         ~650 MB/s        64-bit optimized
SHA-512-Half    ~650 MB/s        Same (just truncated)
SHA-256         ~450 MB/s        32-bit operations
RIPEMD-160      ~200 MB/s        Older algorithm
RIPESHA         ~200 MB/s        Limited by RIPEMD-160

Why SHA-512-Half?

// On 64-bit processors:
SHA-512:  Uses 64-bit operations → fast
SHA-256:  Uses 32-bit operations → slower on 64-bit CPU

// SHA-512-Half gives us:
Performance of SHA-512 (~650 MB/s)
Output size of SHA-256 (32 bytes)

// Best of both worlds!

Hashing Performance Impact

// Transaction ID calculation:
Serialize transaction: ~1 KB
Hash with SHA-512-Half: ~1.5 μs

// Negligible compared to signature verification (100-500 μs)
// Not a bottleneck

Caching Strategies

Public Key Caching

// Problem: Deriving public key from signature is expensive
// Solution: Cache account ID → public key mappings

class PublicKeyCache
{
private:
    std::unordered_map<AccountID, PublicKey> cache_;
    std::shared_mutex mutex_;
    size_t maxSize_ = 10000;

public:
    std::optional<PublicKey> get(AccountID const& id)
    {
        std::shared_lock lock(mutex_);
        auto it = cache_.find(id);
        return it != cache_.end() ? std::optional{it->second} : std::nullopt;
    }

    void put(AccountID const& id, PublicKey const& pk)
    {
        std::unique_lock lock(mutex_);

        if (cache_.size() >= maxSize_)
            cache_.clear();  // Simple eviction

        cache_[id] = pk;
    }
};

// Usage:
PublicKey getAccountPublicKey(AccountID const& account)
{
    // Check cache first
    if (auto pk = keyCache.get(account))
        return *pk;

    // Not in cache - derive from ledger
    auto pk = deriveFromLedger(account);

    // Cache for next time
    keyCache.put(account, pk);

    return pk;
}

Benefits:

  • Avoids repeated derivation

  • Reduces ledger lookups

  • Especially beneficial for frequently-used accounts

Signature Verification Caching

// Problem: Same transaction verified multiple times
// Solution: Cache transaction hash → verification result

class VerificationCache
{
private:
    struct Entry {
        bool valid;
        std::chrono::steady_clock::time_point expiry;
    };

    std::unordered_map<uint256, Entry> cache_;
    std::shared_mutex mutex_;

public:
    std::optional<bool> check(uint256 const& txHash)
    {
        std::shared_lock lock(mutex_);

        auto it = cache_.find(txHash);
        if (it == cache_.end())
            return std::nullopt;

        // Check if expired
        if (std::chrono::steady_clock::now() > it->second.expiry) {
            return std::nullopt;  // Expired
        }

        return it->second.valid;
    }

    void store(uint256 const& txHash, bool valid)
    {
        std::unique_lock lock(mutex_);

        cache_[txHash] = Entry{
            valid,
            std::chrono::steady_clock::now() + std::chrono::minutes(10)
        };
    }
};

// Usage:
bool verifyTransaction(Transaction const& tx)
{
    auto txHash = tx.getHash();

    // Check cache
    if (auto cached = verifyCache.check(txHash))
        return *cached;

    // Not cached - verify
    bool valid = verify(tx.publicKey, tx.data, tx.signature, true);

    // Cache result
    verifyCache.store(txHash, valid);

    return valid;
}

Considerations:

  • Cache must expire (memory limits)

  • Expiry time vs hit rate trade-off

  • Thread safety required

  • Only cache verified transactions (not unverified)

Hash Caching in SHAMap

// Merkle tree nodes cache their hashes
class SHAMapNode
{
private:
    uint256 hash_;
    bool hashValid_ = false;

public:
    uint256 const& getHash()
    {
        if (!hashValid_) {
            hash_ = computeHash();
            hashValid_ = true;
        }
        return hash_;
    }

    void invalidateHash()
    {
        hashValid_ = false;
        // Parent nodes also invalidated (recursively)
    }
};

Benefits:

  • Avoids recomputing unchanged subtrees

  • Critical for Merkle tree performance

  • Cache invalidation on modification

Batch Operations

Batch Signature Verification (Ed25519 Only)

// Ed25519 supports batch verification
// Verify multiple signatures faster than individually

bool verifyBatch(
    std::vector<PublicKey> const& publicKeys,
    std::vector<Slice> const& messages,
    std::vector<Slice> const& signatures)
{
    // Batch verification algorithm:
    // Combines multiple verification equations
    // Single verification check for all signatures
    //
    // Time: ~1.2 × single verification
    // Instead of: N × single verification
    //
    // For N=100: 100× speedup!

    return ed25519_sign_open_batch(
        messages.data(),
        messages.size(),
        publicKeys.data(),
        signatures.data(),
        messages.size()) == 0;
}

Benefits:

  • Massive speedup for multiple verifications

  • Ideal for transaction processing

  • Only available for Ed25519

Limitations:

  • Batch fails if ANY signature is invalid

  • Must verify individually to find which failed

  • Requires all same algorithm (ed25519)

Batch Hashing

// For hashing multiple items
void hashMultiple(
    std::vector<Slice> const& items,
    std::vector<uint256>& hashes)
{
    hashes.reserve(items.size());

    // Option 1: Parallel hashing
    #pragma omp parallel for
    for (size_t i = 0; i < items.size(); ++i) {
        hashes[i] = sha512Half(items[i]);
    }

    // Option 2: Vectorized hashing (if available)
    // Some crypto libraries support SIMD hashing
    hashMultipleSIMD(items, hashes);
}

Parallel Processing

Multi-threaded Verification

// Verify signatures in parallel
std::vector<bool> verifyParallel(
    std::vector<Transaction> const& transactions)
{
    std::vector<bool> results(transactions.size());

    // Use thread pool
    #pragma omp parallel for
    for (size_t i = 0; i < transactions.size(); ++i) {
        results[i] = verifyTransaction(transactions[i]);
    }

    return results;
}

Considerations:

  • Cryptographic operations are CPU-bound

  • Parallelism limited by number of cores

  • Thread synchronization overhead

  • Good for batch processing

Async Processing

// Verify asynchronously
std::future<bool> verifyAsync(Transaction const& tx)
{
    return std::async(std::launch::async, [tx]() {
        return verifyTransaction(tx);
    });
}

// Usage:
std::vector<std::future<bool>> futures;
for (auto const& tx : transactions) {
    futures.push_back(verifyAsync(tx));
}

// Collect results
for (auto& future : futures) {
    bool valid = future.get();
    // ...
}

Memory Optimization

Signature Size

// Ed25519 signatures are smaller and fixed-size
Signature size:
    secp256k1: 70-72 bytes (variable, DER encoded)
    ed25519:   64 bytes (fixed, raw bytes)

// For 1,000,000 signatures:
secp256k1: ~71 MB
ed25519:   ~64 MB

// Savings: 7 MB (10%)
// Also: Fixed size easier to handle

Public Key Storage

// Compressed public keys
secp256k1: 33 bytes (compressed)
ed25519:   33 bytes

// Both use compression
// No optimization available

Performance Measurement

Profiling

// Measure cryptographic operations
auto measureSign = []() {
    auto [pk, sk] = randomKeyPair(KeyType::ed25519);
    std::vector<uint8_t> message(1000, 0xAA);

    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < 1000; ++i) {
        auto sig = sign(pk, sk, makeSlice(message));
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);

    std::cout << "Average sign time: " << duration.count() / 1000.0 << " μs\n";
};

Bottleneck Identification

// Use profiler to find hotspots
// Example output:

Function                     Time      % Total
───────────────────────────────────────────────
verifyTransaction            45.2%     Critical
  ├─ ed25519_sign_open      42.1%     ← Bottleneck
  └─ sha512Half              2.8%
processLedger                35.1%
  ├─ computeMerkleRoot      20.3%
  └─ serializeTransactions  14.8%

Optimization Guidelines

✅ DO:

  1. Use ed25519 for new accounts

    // 4-5× faster than secp256k1
    auto [pk, sk] = randomKeyPair(KeyType::ed25519);
  2. Cache frequently-used data

    // Public keys, verification results, hashes
    cache.get(key);
  3. Batch operations when possible

    // Especially for ed25519 batch verification
    verifyBatch(pks, messages, sigs);
  4. Profile before optimizing

    // Measure actual bottlenecks
    // Don't optimize blindly
  5. Use parallel processing for batches

    // Utilize multiple cores
    #pragma omp parallel for

❌ DON'T:

  1. Don't sacrifice security for speed

    // ❌ Skipping canonicality checks
    // ❌ Using weak algorithms
    // ❌ Reducing key sizes
  2. Don't cache unverified data

    // ❌ Caching before verification
    // ✅ Cache after verification succeeds
  3. Don't over-optimize negligible operations

    // Hashing is fast (~1 μs)
    // Focus on signatures (~100-500 μs)
  4. Don't forget thread safety

    // Caches need proper locking
    // Crypto libraries might not be thread-safe

Real-World Performance

XRPL Mainnet Statistics

// Approximate numbers from XRPL mainnet:

Transactions per ledger: ~50-200
Ledger close time: ~3-5 seconds
Validators: ~35-40

Signature verifications per second:
(150 tx/ledger × 40 validators) / 4 seconds = 1,500 verifications/second

With ed25519 (100 μs each):
1,500 × 0.0001s = 0.15 seconds of CPU time per second
= 15% CPU utilization

With secp256k1 (500 μs each):
1,500 × 0.0005s = 0.75 seconds of CPU time per second
= 75% CPU utilization

Ed25519 allows 5× higher throughput with same CPU!

Summary

Performance optimization in cryptography:

  1. Algorithm choice matters: ed25519 is 4-5× faster than secp256k1

  2. Verification is the bottleneck: Focus optimization here

  3. Caching helps: Public keys, verification results, hashes

  4. Batch operations: Especially for ed25519

  5. Parallel processing: Utilize multiple cores

  6. Profile first: Measure before optimizing

  7. Never sacrifice security: Performance < Security

Key takeaways:

  • Use ed25519 for new accounts (faster, simpler)

  • Cache wisely (but verify first)

  • Batch when possible (ed25519 batch verification)

  • Profile to find real bottlenecks

  • Optimize hot paths only

  • Security always comes first

Last updated