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:
Generates a random ed25519 key pair
Derives the account ID and address
Signs a test message
Verifies the signature
Prints all intermediate values
Hints
Use
randomKeyPair(KeyType::ed25519)
Use
calcAccountID()
for the account IDUse
toBase58()
for the addressUse
sign()
andverify()
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 secp256k1Generate 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:
Exercise 1: Basic key generation and signing
Exercise 2: Deterministic key generation
Exercise 3: Signature malleability
Exercise 4: Hash function benchmarking
Exercise 5: Base58Check encoding
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