Hands-On Exercises with Solutions

Introduction

This appendix provides hands-on exercises to reinforce your understanding of XRPL cryptography. Each exercise includes a problem statement, hints, and a complete solution with explanations.

Exercise 1: Key Generation and Verification

Problem

Write a program that:

  1. Generates a random ed25519 key pair

  2. Derives the account ID and address

  3. Signs a test message

  4. Verifies the signature

  5. Prints all intermediate values

Hints

  • Use randomKeyPair(KeyType::ed25519)

  • Use calcAccountID() for the account ID

  • Use toBase58() for the address

  • Use sign() and verify()

Solution

#include <ripple/protocol/SecretKey.h>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/digest.h>
#include <ripple/protocol/tokens.h>
#include <iostream>

using namespace ripple;

void exercise1()
{
    std::cout << "=== Exercise 1: Key Generation and Verification ===\n\n";

    // 1. Generate random ed25519 key pair
    std::cout << "1. Generating ed25519 key pair...\n";
    auto [publicKey, secretKey] = randomKeyPair(KeyType::ed25519);

    std::cout << "   Public Key (hex): " << strHex(publicKey) << "\n";
    std::cout << "   Public Key size: " << publicKey.size() << " bytes\n\n";

    // 2. Derive account ID and address
    std::cout << "2. Deriving account ID and address...\n";
    AccountID accountID = calcAccountID(publicKey);

    std::cout << "   Account ID (hex): " << strHex(accountID) << "\n";
    std::cout << "   Account ID size: " << accountID.size() << " bytes\n";

    std::string address = toBase58(accountID);
    std::cout << "   Address: " << address << "\n\n";

    // 3. Sign a test message
    std::cout << "3. Signing test message...\n";
    std::string message = "Hello, XRPL!";
    Buffer signature = sign(
        publicKey,
        secretKey,
        makeSlice(message));

    std::cout << "   Message: " << message << "\n";
    std::cout << "   Signature (hex): " << strHex(signature) << "\n";
    std::cout << "   Signature size: " << signature.size() << " bytes\n\n";

    // 4. Verify the signature
    std::cout << "4. Verifying signature...\n";
    bool valid = verify(
        publicKey,
        makeSlice(message),
        makeSlice(signature),
        true);  // Require canonical

    std::cout << "   Verification result: "
              << (valid ? "✓ VALID" : "✗ INVALID") << "\n\n";

    // 5. Test with modified message
    std::cout << "5. Testing with modified message...\n";
    std::string modifiedMessage = "Hello, XRPL?";
    bool validModified = verify(
        publicKey,
        makeSlice(modifiedMessage),
        makeSlice(signature),
        true);

    std::cout << "   Modified message: " << modifiedMessage << "\n";
    std::cout << "   Verification result: "
              << (validModified ? "✓ VALID" : "✗ INVALID") << "\n";
    std::cout << "   (Expected: INVALID - signature shouldn't verify)\n";
}

Expected Output

=== Exercise 1: Key Generation and Verification ===

1. Generating ed25519 key pair...
   Public Key (hex): ED9434799226374926EDA3B54B1B461B4ABF7237962EEB1144C10A7CA6A9D32C64
   Public Key size: 33 bytes

2. Deriving account ID and address...
   Account ID (hex): 8B8A6C533F09CA0E5E00E7C32AA7EC323485ED3F
   Account ID size: 20 bytes
   Address: rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C

3. Signing test message...
   Message: Hello, XRPL!
   Signature (hex): 3F4B8A...
   Signature size: 64 bytes

4. Verifying signature...
   Verification result: ✓ VALID

5. Testing with modified message...
   Modified message: Hello, XRPL?
   Verification result: ✗ INVALID
   (Expected: INVALID - signature shouldn't verify)

Exercise 2: Deterministic Key Generation

Problem

Implement a function that generates multiple accounts from a single seed, demonstrating hierarchical deterministic key generation.

Hints

  • Use a fixed seed for reproducibility

  • Use Generator class for secp256k1

  • Generate accounts with ordinals 0, 1, 2

Solution

void exercise2()
{
    std::cout << "=== Exercise 2: Deterministic Key Generation ===\n\n";

    // Create a seed (in practice, this would be randomly generated)
    std::uint8_t seedBytes[16] = {
        0xDE, 0xDC, 0xE9, 0xCE, 0x67, 0xB4, 0x51, 0xD8,
        0x52, 0xFD, 0x4E, 0x84, 0x6F, 0xCD, 0xE3, 0x1C
    };
    Seed seed{makeSlice(seedBytes)};

    std::cout << "Seed (hex): " << strHex(seed) << "\n";
    std::cout << "Seed (Base58): " << toBase58(seed) << "\n\n";

    // Generate multiple accounts from the same seed
    std::cout << "Generating accounts from seed:\n\n";

    for (int ordinal = 0; ordinal < 3; ++ordinal)
    {
        std::cout << "Account " << ordinal << ":\n";

        // Generate key pair (using secp256k1 to demonstrate Generator)
        detail::Generator gen(seed);
        auto [publicKey, secretKey] = gen(ordinal);

        // Derive address
        AccountID accountID = calcAccountID(publicKey);
        std::string address = toBase58(accountID);

        std::cout << "  Public Key: " << strHex(publicKey) << "\n";
        std::cout << "  Address:    " << address << "\n\n";
    }

    // Verify determinism
    std::cout << "Verifying determinism (regenerating account 0):\n";
    detail::Generator gen2(seed);
    auto [pk2, sk2] = gen2(0);
    std::cout << "  Same public key? "
              << (strHex(pk2) == strHex(/* first public key */) ? "✓ YES" : "✗ NO")
              << "\n";
}

Exercise 3: Signature Malleability Test

Problem

Demonstrate why signature malleability is dangerous and how canonical signatures prevent it.

Hints

  • Generate a secp256k1 signature

  • Check its canonicality

  • Show that transaction ID depends on signature

Solution

void exercise3()
{
    std::cout << "=== Exercise 3: Signature Malleability ===\n\n";

    // Generate secp256k1 key pair
    auto [publicKey, secretKey] = randomKeyPair(KeyType::secp256k1);

    // Create a transaction-like message
    std::string txData = "Payment: Alice sends 100 XRP to Bob";

    // Sign it
    Buffer signature = sign(publicKey, secretKey, makeSlice(txData));

    std::cout << "Original Signature:\n";
    std::cout << "  Hex: " << strHex(signature) << "\n";
    std::cout << "  Size: " << signature.size() << " bytes\n";

    // Check canonicality
    auto canonicality = ecdsaCanonicality(makeSlice(signature));

    if (canonicality)
    {
        switch (*canonicality)
        {
            case ECDSACanonicality::fullyCanonical:
                std::cout << "  Canonicality: ✓ FULLY CANONICAL\n";
                std::cout << "  (S ≤ order/2 - prevents malleability)\n";
                break;
            case ECDSACanonicality::canonical:
                std::cout << "  Canonicality: ⚠ CANONICAL (but not fully)\n";
                std::cout << "  (S > order/2 - could be malleated)\n";
                break;
        }
    }
    else
    {
        std::cout << "  Canonicality: ✗ INVALID\n";
    }

    // Compute "transaction ID" (hash of data + signature)
    std::string combined = txData + std::string(
        reinterpret_cast<char const*>(signature.data()),
        signature.size());

    uint256 txID = sha512Half(makeSlice(combined));

    std::cout << "\nTransaction ID (hash of data + signature):\n";
    std::cout << "  " << strHex(txID) << "\n";

    std::cout << "\nIf signature were malleable:\n";
    std::cout << "  - Attacker could flip S to order-S\n";
    std::cout << "  - Signature still valid\n";
    std::cout << "  - But transaction ID would change!\n";
    std::cout << "  - Applications tracking original ID would fail\n";

    std::cout << "\nCanonical signatures prevent this:\n";
    std::cout << "  - Only one valid signature per message\n";
    std::cout << "  - Transaction ID is stable\n";
    std::cout << "  - No malleability attacks possible\n";
}

Exercise 4: Hash Function Performance

Problem

Benchmark SHA-512-Half vs SHA-256 to verify that SHA-512-Half is faster on 64-bit systems.

Solution

#include <chrono>

void exercise4()
{
    std::cout << "=== Exercise 4: Hash Function Performance ===\n\n";

    // Prepare test data
    const size_t dataSize = 1024 * 1024;  // 1 MB
    std::vector<uint8_t> data(dataSize, 0xAA);

    constexpr int iterations = 1000;

    // Benchmark SHA-512-Half
    {
        auto start = std::chrono::high_resolution_clock::now();

        for (int i = 0; i < iterations; ++i)
        {
            uint256 hash = sha512Half(makeSlice(data));
        }

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

        double throughput = (dataSize * iterations) / (duration.count() / 1000.0) / (1024 * 1024);

        std::cout << "SHA-512-Half:\n";
        std::cout << "  Time: " << duration.count() << " ms\n";
        std::cout << "  Throughput: " << throughput << " MB/s\n\n";
    }

    // Benchmark SHA-256
    {
        auto start = std::chrono::high_resolution_clock::now();

        for (int i = 0; i < iterations; ++i)
        {
            uint256 hash = sha256(makeSlice(data));
        }

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

        double throughput = (dataSize * iterations) / (duration.count() / 1000.0) / (1024 * 1024);

        std::cout << "SHA-256:\n";
        std::cout << "  Time: " << duration.count() << " ms\n";
        std::cout << "  Throughput: " << throughput << " MB/s\n\n";
    }

    std::cout << "Result: SHA-512-Half is typically faster on 64-bit CPUs\n";
    std::cout << "Reason: SHA-512 uses 64-bit operations, SHA-256 uses 32-bit\n";
}

Exercise 5: Base58Check Encoding

Problem

Implement a function that encodes and decodes account IDs, verifying the checksum works correctly.

Solution

void exercise5()
{
    std::cout << "=== Exercise 5: Base58Check Encoding ===\n\n";

    // Generate a public key and account ID
    auto [publicKey, secretKey] = randomKeyPair(KeyType::ed25519);
    AccountID accountID = calcAccountID(publicKey);

    std::cout << "Account ID (hex): " << strHex(accountID) << "\n";

    // Encode to Base58Check address
    std::string address = toBase58(accountID);
    std::cout << "Address (Base58): " << address << "\n\n";

    // Decode back
    auto decoded = parseBase58<AccountID>(address);

    if (decoded)
    {
        std::cout << "✓ Decoding successful\n";
        std::cout << "Decoded (hex): " << strHex(*decoded) << "\n";

        if (*decoded == accountID)
        {
            std::cout << "✓ Decoded value matches original\n\n";
        }
    }
    else
    {
        std::cout << "✗ Decoding failed\n\n";
    }

    // Test with corrupted address
    std::cout << "Testing checksum verification:\n\n";

    std::string corrupted = address;
    // Flip one character
    corrupted[10] = (corrupted[10] == 'A') ? 'B' : 'A';

    std::cout << "Corrupted address: " << corrupted << "\n";

    auto decodedCorrupted = parseBase58<AccountID>(corrupted);

    if (!decodedCorrupted)
    {
        std::cout << "✓ Checksum verification failed (as expected)\n";
        std::cout << "Corrupted address rejected\n";
    }
    else
    {
        std::cout << "✗ Checksum verification passed (unexpected!)\n";
    }
}

Exercise 6: Multi-Algorithm Comparison

Problem

Compare signing and verification performance between ed25519 and secp256k1.

Solution

void exercise6()
{
    std::cout << "=== Exercise 6: Algorithm Comparison ===\n\n";

    std::string message = "Performance test message";
    constexpr int iterations = 1000;

    // Test Ed25519
    {
        std::cout << "Ed25519:\n";

        auto [pk, sk] = randomKeyPair(KeyType::ed25519);

        // Measure signing
        auto startSign = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < iterations; ++i)
        {
            auto sig = sign(pk, sk, makeSlice(message));
        }
        auto endSign = std::chrono::high_resolution_clock::now();
        auto signTime = std::chrono::duration_cast<std::chrono::microseconds>(
            endSign - startSign);

        // Measure verification
        auto sig = sign(pk, sk, makeSlice(message));
        auto startVerify = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < iterations; ++i)
        {
            verify(pk, makeSlice(message), makeSlice(sig), true);
        }
        auto endVerify = std::chrono::high_resolution_clock::now();
        auto verifyTime = std::chrono::duration_cast<std::chrono::microseconds>(
            endVerify - startVerify);

        std::cout << "  Signing:   " << (signTime.count() / iterations) << " μs/op\n";
        std::cout << "  Verify:    " << (verifyTime.count() / iterations) << " μs/op\n\n";
    }

    // Test secp256k1
    {
        std::cout << "secp256k1:\n";

        auto [pk, sk] = randomKeyPair(KeyType::secp256k1);

        // Measure signing
        auto startSign = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < iterations; ++i)
        {
            auto sig = sign(pk, sk, makeSlice(message));
        }
        auto endSign = std::chrono::high_resolution_clock::now();
        auto signTime = std::chrono::duration_cast<std::chrono::microseconds>(
            endSign - startSign);

        // Measure verification
        auto sig = sign(pk, sk, makeSlice(message));
        auto startVerify = std::chrono::high_resolution_clock::now();
        for (int i = 0; i < iterations; ++i)
        {
            verify(pk, makeSlice(message), makeSlice(sig), true);
        }
        auto endVerify = std::chrono::high_resolution_clock::now();
        auto verifyTime = std::chrono::duration_cast<std::chrono::microseconds>(
            endVerify - startVerify);

        std::cout << "  Signing:   " << (signTime.count() / iterations) << " μs/op\n";
        std::cout << "  Verify:    " << (verifyTime.count() / iterations) << " μs/op\n\n";
    }

    std::cout << "Conclusion:\n";
    std::cout << "  Ed25519 is typically 4-5× faster than secp256k1\n";
    std::cout << "  Especially significant for verification (done by all validators)\n";
}

Challenge Exercises

Challenge 1: Implement Your Own Base58 Encoder

Write a Base58 encoder from scratch without using library functions. Verify it produces the same output as rippled's implementation.

Challenge 2: Signature Batch Verification

Implement Ed25519 batch verification and measure the speedup compared to individual verification.

Challenge 3: Key Derivation Path

Implement a key derivation path system (like BIP-32) for XRPL using deterministic key generation.

Challenge 4: Timing Attack Demonstration

Write a program that demonstrates a timing attack on non-constant-time string comparison.

Summary

These exercises cover:

  1. Exercise 1: Basic key generation and signing

  2. Exercise 2: Deterministic key generation

  3. Exercise 3: Signature malleability

  4. Exercise 4: Hash function benchmarking

  5. Exercise 5: Base58Check encoding

  6. Exercise 6: Algorithm performance comparison

Learning outcomes:

  • Hands-on experience with XRPL cryptography

  • Understanding of security properties

  • Performance characteristics

  • Common pitfalls and how to avoid them

Complete these exercises to solidify your understanding of XRPL's cryptographic foundations!

Last updated