Hash functions are the workhorses of cryptographic systems. While signatures prove authorization and keys establish identity, hash functions ensure integrity and enable efficient data structures. In XRPL, hash functions are everywhere—transaction IDs, ledger object keys, Merkle trees, address generation, and more.
This chapter explores how XRPL uses hash functions, why specific algorithms were chosen, and how they provide the integrity guarantees the system depends on.
What is a Cryptographic Hash Function?
A cryptographic hash function takes arbitrary input and produces a fixed-size output:
sha512Half("Hello") == sha512Half("Hello") // Always true
// Same input always produces same output
2. Fast to Compute
// Can hash gigabytes per second
auto hash = sha512Half(largeData); // Microseconds to milliseconds
3. Avalanche Effect
4. Preimage Resistance (One-Way)
5. Collision Resistance
SHA-512-Half: The Primary Workhorse
Why SHA-512-Half?
Why truncate SHA-512 instead of using SHA-256?
Performance on 64-bit processors:
On 64-bit systems (which all modern servers are), SHA-512 is faster than SHA-256 despite producing more output. By truncating to 256 bits, we get the best of both worlds.
Implementation
Usage Throughout XRPL
Transaction IDs:
Ledger Object Keys:
Merkle Tree Nodes:
Secure Variant: sha512Half_s
When to use the secure variant:
Hashing secret keys or seeds
Deriving keys from passwords
Any operation involving sensitive data
Why it matters:
RIPESHA: Address Generation
The Double Hash
Why Two Hash Functions?
1. Defense in Depth
2. Compactness
3. Quantum Resistance (Partial)
Usage
SHA-256: Checksum and Encoding
Double SHA-256 for Base58Check
Why double SHA-256?
Historical reasons (inherited from early cryptocurrency designs):
Provides defense against length-extension attacks
Standard pattern for checksums
Well-tested over many years
Checksum properties:
Hash Prefixes: Domain Separation
Why use prefixes?
Prevent cross-protocol attacks where a hash from one context is used in another:
Example Usage
Incremental Hashing
Hash functions can process data incrementally:
Benefits:
Stream large files without loading into memory
Hash complex data structures field by field
More efficient for large inputs
Example: Hashing a transaction
Hash Collisions: Why We Don't Worry
Birthday Paradox
The "birthday attack" on a 256-bit hash requires:
Conclusion: Collision attacks on SHA-512-Half are not feasible with current or foreseeable technology.
Collision Resistance in Practice
A collision in any of these would be catastrophic, but the probability is negligible.
Performance Considerations
Hashing Speed
Caching Hashes
Why cache?
Merkle tree nodes are hashed repeatedly
Caching avoids redundant computation
Invalidate when node contents change
Hash Function Summary
Function
Output Size
Speed
Primary Use
SHA-512-Half
256 bits
~650 MB/s
Transaction IDs, object keys, Merkle trees
SHA-256
256 bits
~450 MB/s
Base58Check checksums
RIPEMD-160
160 bits
~200 MB/s
Part of RIPESHA (address generation)
RIPESHA
160 bits
~300 MB/s
Account IDs, node IDs
Best Practices
✅ DO:
Use sha512Half for new protocols
Use hash prefixes for domain separation
Cache computed hashes when appropriate
Use secure variant for sensitive data
❌ DON'T:
Don't use non-cryptographic hashes for security
Don't implement your own hash function
Don't assume hashes are unique without checking
Summary
Hash functions in XRPL provide:
Integrity: Detect any data modification
Identification: Unique IDs for transactions and objects
Efficiency: Fast computation on modern CPUs
Security: Collision and preimage resistance
Key algorithms:
SHA-512-Half: Primary hash (fast on 64-bit systems)
RIPESHA: Address generation (compact, defense in depth)
SHA-256: Checksums (standard, well-tested)
Usage patterns:
Always use hash prefixes for domain separation
Cache hashes when recomputed frequently
Use secure variants for sensitive data
Trust collision resistance but code defensively
In the next chapter, we'll explore Base58Check encoding and how XRPL makes binary data human-readable.
sha512Half("Hello") → 0x7F83B165...
sha512Half("Hello!") → 0xC89F3AB2... // Completely different!
// One bit change → ~50% of output bits flip
// Given hash, cannot find input
uint256 hash = 0x7F83B165...;
// No way to compute: input = reverse_hash(hash);
// Cannot find two inputs with same hash
// sha512Half(x) == sha512Half(y) where x != y
// Computationally infeasible
// Not SHA-256, but SHA-512 truncated to 256 bits
template <class... Args>
uint256 sha512Half(Args const&... args)
{
sha512_half_hasher h;
hash_append(h, args...);
return static_cast<typename sha512_half_hasher::result_type>(h);
}
SHA-512: Operates on 64-bit words → ~650 MB/s on modern CPUs
SHA-256: Operates on 32-bit words → ~450 MB/s on modern CPUs
SHA-512-Half = SHA-512 speed + SHA-256 output size
// Secure variant that erases internal state
uint256 sha512Half_s(Slice const& data)
{
sha512_half_hasher h;
h(data.data(), data.size());
auto result = static_cast<uint256>(h);
// Hasher destructor securely erases internal state
// This prevents sensitive data from lingering in memory
return result;
}
// Regular variant
auto hash1 = sha512Half(secretData);
// SHA512_CTX still contains secretData fragments in memory
// Secure variant
auto hash2 = sha512Half_s(secretData);
// SHA512_CTX is securely erased
class ripesha_hasher
{
private:
openssl_sha256_hasher sha_;
public:
using result_type = ripemd160_hasher::result_type; // 20 bytes
void operator()(void const* data, std::size_t size) noexcept
{
// First: SHA-256
sha_(data, size);
}
operator result_type() noexcept
{
// Get SHA-256 result (32 bytes)
auto const sha256_digest =
static_cast<openssl_sha256_hasher::result_type>(sha_);
// Second: RIPEMD-160 of the SHA-256
ripemd160_hasher ripe;
ripe(sha256_digest.data(), sha256_digest.size());
return static_cast<result_type>(ripe); // 20 bytes
}
};
If SHA-256 is broken:
RIPEMD-160 provides second layer
If RIPEMD-160 is broken:
SHA-256 provides protection
Breaking both: requires defeating two independent algorithms
Public Key: 33 bytes
↓ SHA-256
SHA-256 hash: 32 bytes
↓ RIPEMD-160
Account ID: 20 bytes (40% smaller than public key)
Quantum computers may break elliptic curves:
PublicKey → SecretKey (vulnerable)
But cannot reverse hashes:
AccountID ↛ PublicKey (still secure)
This provides time to upgrade the system if quantum computers emerge.
// Calculate account ID from public key
AccountID calcAccountID(PublicKey const& pk)
{
ripesha_hasher h;
h(pk.data(), pk.size());
return AccountID{static_cast<ripesha_hasher::result_type>(h)};
}
// Calculate node ID from public key
NodeID calcNodeID(PublicKey const& pk)
{
ripesha_hasher h;
h(pk.data(), pk.size());
return NodeID{static_cast<ripesha_hasher::result_type>(h)};
}
4 bytes = 32 bits = 2^32 possible values
Probability of random corruption matching checksum: 1 in 4,294,967,296
Effectively catches all typos and errors.
// Without prefixes (BAD):
hash_tx = SHA512Half(tx_data)
hash_msg = SHA512Half(msg_data)
// If tx_data == msg_data, then hash_tx == hash_msg
// Could cause confusion/attacks
// With prefixes (GOOD):
hash_tx = SHA512Half(PREFIX_TX, tx_data)
hash_msg = SHA512Half(PREFIX_MSG, msg_data)
// Even if tx_data == msg_data, hash_tx != hash_msg
// Transaction ID
uint256 getTransactionID(STTx const& tx)
{
Serializer s;
s.add32(HashPrefix::transactionID); // Add prefix first
tx.addWithoutSigningFields(s);
return sha512Half(s.slice());
}
// Signing data (different prefix, different hash)
uint256 getSigningHash(STTx const& tx)
{
Serializer s;
s.add32(HashPrefix::txSign); // Different prefix
tx.addWithoutSigningFields(s);
return sha512Half(s.slice());
}
// Instead of hashing all at once:
auto hash = sha512Half(bigData); // Requires loading all data
// Can hash incrementally:
sha512_half_hasher h;
h(chunk1.data(), chunk1.size());
h(chunk2.data(), chunk2.size());
h(chunk3.data(), chunk3.size());
auto hash = static_cast<uint256>(h);
Number of hashes to find collision = 2^(256/2) = 2^128
2^128 = 340,282,366,920,938,463,463,374,607,431,768,211,456
If you could compute 1 trillion hashes per second:
Time = 2^128 / (10^12) seconds
= 10^25 years
(Universe age ≈ 10^10 years)
// XRPL relies on collision resistance for:
// 1. Transaction IDs must be unique
uint256 txID = sha512Half(tx);
// 2. Ledger object keys must not collide
uint256 accountKey = sha512Half(HashPrefix::account, accountID);
// 3. Merkle tree integrity
uint256 nodeHash = sha512Half(leftChild, rightChild);