SHA-512Half, hash prefixes and ledger namespaces, and how hashing underpins IDs, keys and integrity.
What you'll learn
≈60 min · Intermediate · builds on Security foundations
Hashes are the workhorses of XRPL, behind every transaction ID, ledger key and Merkle root. In this module you'll learn why the ledger leans on SHA-512Half, how hash prefixes keep different kinds of hashes from ever colliding, and how account addresses are born from RIPEMD160∘SHA256. Small functions, enormous responsibility.
A cryptographic hash function takes arbitrary input and produces a fixed-size output:
Input (any size) → Hash Function → Output (fixed size)
"Hello" → sha512Half → 0x7F83B165...
"Hello World!" → sha512Half → 0xA591A6D4...
[1 MB file] → sha512Half → 0x3C9F2A8B...
1. Deterministic
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
sha512Half("Hello") → 0x7F83B165...
sha512Half("Hello!") → 0xC89F3AB2... // Completely different!
// One bit change → ~50% of output bits flip
4. Preimage Resistance (One-Way)
// Given hash, cannot find input
uint256 hash = 0x7F83B165...;
// No way to compute: input = reverse_hash(hash);
5. Collision Resistance
// Cannot find two inputs with same hash
// sha512Half(x) == sha512Half(y) where x != y
// Computationally infeasible
In brief: the main hash across XRPL: run SHA-512 and keep the first 256 bits.
// 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);
}
Why truncate SHA-512 instead of using SHA-256?
Performance on 64-bit processors:
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
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.
// From src/libxrpl/protocol/digest.cpp
class sha512_half_hasher
{
private:
SHA512_CTX ctx_;
public:
using result_type = uint256;
sha512_half_hasher()
{
SHA512_Init(&ctx_);
}
void operator()(void const* data, std::size_t size) noexcept
{
SHA512_Update(&ctx_, data, size);
}
operator result_type() noexcept
{
// Compute full SHA-512 (64 bytes)
std::uint8_t digest[64];
SHA512_Final(digest, &ctx_);
// Return first 32 bytes (256 bits)
result_type result;
std::memcpy(result.data(), digest, 32);
return result;
}
};
Transaction IDs:
uint256 STTx::getTransactionID() const
{
Serializer s;
s.add32(HashPrefix::TransactionId);
addWithoutSigningFields(s);
return sha512Half(s.slice());
}
Ledger Object Keys:
Keylet keylet::account(AccountID const& id) noexcept
{
// Ledger object keys use a LedgerNameSpace prefix (not a HashPrefix).
// indexHash() does sha512Half(std::uint16_t(space), args...).
return Keylet{ltACCOUNT_ROOT, indexHash(LedgerNameSpace::Account, id)};
}
Merkle Tree Nodes:
uint256 SHAMapInnerNode::getHash() const
{
if (hashValid_)
return hash_;
Serializer s;
for (auto const& child : children_)
s.add256(child.getHash());
hash_ = sha512Half(s.slice());
hashValid_ = true;
return hash_;
}
// 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;
}
When to use the secure variant:
Why it matters:
// 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
In brief: RIPEMD160 over SHA-256 turns a public key into an account ID.
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
}
};
1. Defense in Depth
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
2. Compactness
Public Key: 33 bytes
↓ SHA-256
SHA-256 hash: 32 bytes
↓ RIPEMD-160
Account ID: 20 bytes (40% smaller than public key)
3. Quantum Resistance (Partial)
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)};
}
// From src/libxrpl/protocol/tokens.cpp
std::string encodeBase58Token(
TokenType type,
void const* token,
std::size_t size)
{
std::vector<uint8_t> buffer;
buffer.push_back(static_cast<uint8_t>(type));
buffer.insert(buffer.end(), token, token + size);
// Compute checksum: first 4 bytes of SHA-256(SHA-256(data))
auto const hash1 = sha256(makeSlice(buffer));
auto const hash2 = sha256(makeSlice(hash1));
// Append checksum
buffer.insert(buffer.end(), hash2.begin(), hash2.begin() + 4);
// Base58 encode
return base58Encode(buffer);
}
Why double SHA-256?
Historical reasons (inherited from early cryptocurrency designs):
Checksum properties:
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.
In brief: a distinct prefix per use so a transaction hash can never collide with a ledger-object hash.
// From include/xrpl/protocol/HashPrefix.h
enum class HashPrefix : std::uint32_t
{
transactionID = 0x54584E00, // 'TXN\0'
txSign = 0x53545800, // 'STX\0'
txMultiSign = 0x534D5400, // 'SMT\0'
manifest = 0x4D414E00, // 'MAN\0'
ledgerMaster = 0x4C575200, // 'LWR\0'
ledgerInner = 0x4D494E00, // 'MIN\0'
ledgerLeaf = 0x4D4C4E00, // 'MLN\0'
accountRoot = 0x41525400, // 'ART\0'
};
Why use prefixes?
Prevent cross-protocol attacks where a hash from one context is used in another:
// 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());
}
Key idea. Domain separation means the same bytes hashed for two different purposes produce two different, non-interchangeable hashes. It quietly prevents a whole class of attacks.
Hash functions can process data incrementally:
// 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);
Benefits:
Example: Hashing a transaction
Serializer s;
s.add32(HashPrefix::TransactionId);
s.addVL(tx.getFieldVL(sfAccount));
s.addVL(tx.getFieldVL(sfDestination));
s.add64(tx.getFieldU64(sfAmount));
// ... more fields ...
return sha512Half(s.slice());
In brief: why finding a collision is astronomically unlikely in practice.
The "birthday attack" on a 256-bit hash requires:
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)
Conclusion: Collision attacks on SHA-512-Half are not feasible with current or foreseeable technology.
// 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 = indexHash(LedgerNameSpace::Account, accountID);
// 3. Merkle tree integrity
uint256 nodeHash = sha512Half(leftChild, rightChild);
A collision in any of these would be catastrophic, but the probability is negligible.
// Benchmark results (approximate, hardware-dependent):
SHA-512-Half: ~650 MB/s
SHA-256: ~450 MB/s
RIPEMD-160: ~200 MB/s
For 1 KB transaction:
SHA-512-Half: ~1.5 microseconds
class SHAMapNode
{
private:
uint256 hash_;
bool hashValid_;
public:
uint256 getHash() const
{
if (hashValid_)
return hash_; // Return cached value
// Compute hash (expensive)
hash_ = computeHash();
hashValid_ = true;
return hash_;
}
void invalidateHash()
{
hashValid_ = false; // Force recomputation next time
}
};
Why cache?
| 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 |
Use sha512Half for new protocols
uint256 hash = sha512Half(data); // Fast and standard
Use hash prefixes for domain separation
uint256 hash = sha512Half(HashPrefix::custom, data);
Cache computed hashes when appropriate
if (cached)
return cachedHash;
cachedHash = sha512Half(data);
return cachedHash;
Use secure variant for sensitive data
uint256 hash = sha512Half_s(secretData);
Don't use non-cryptographic hashes for security
std::hash<std::string>{}(data); // ❌ NOT SECURE
Don't implement your own hash function
uint32_t myHash(data) { /* ... */ } // ❌ Don't do this
Don't assume hashes are unique without checking
// Even though collisions are infeasible, handle errors gracefully
if (hashExists(newHash))
handleCollision(); // Paranoid but correct
This appendix provides references to the cryptographic standards, RFCs, and specifications that XRPL's cryptography is built upon. Understanding these standards helps you understand why rippled makes certain design choices.
RFC 8032 - Edwards-Curve Digital Signature Algorithm (EdDSA)
What it defines:
Key parameters (Ed25519):
Why XRPL uses it:
SEC 2: Recommended Elliptic Curve Domain Parameters
What it defines:
Key parameters (secp256k1):
p = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE
BAAEDCE6 AF48A03B BFD25E8C D0364141
G = (x, y) where:
x = 79BE667E F9DCBBAC 55A06295 CE870B07
029BFCDB 2DCE28D9 59F2815B 16F81798
y = 483ADA77 26A3C465 5DA4FBFC 0E1108A8
FD17B448 A6855419 9C47D08F FB10D4B8
Why XRPL uses it:
FIPS 186-4 - Digital Signature Standard (DSS)
What it defines:
RFC 6979 - Deterministic Usage of DSA and ECDSA
What it defines:
Algorithm:
k = HMAC_DRBG(private_key, message_hash)
Why XRPL uses it:
FIPS 180-4 - Secure Hash Standard (SHS)
What it defines:
XRPL usage:
Why SHA-512-Half:
Original Paper: "RIPEMD-160: A Strengthened Version of RIPEMD"
XRPL usage:
Algorithm:
RIPESHA(data) = RIPEMD-160(SHA-256(data))
RFC 2898 - PKCS #5: Password-Based Cryptography Specification
What it defines:
Note: XRPL doesn't use PBKDF2 for key derivation (uses sha512Half of seed), but it's relevant for password-based seed derivation in wallets.
No formal RFC, but based on:
Alphabet:
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
XRPL implementation:
RFC 4648 - The Base16, Base32, and Base64 Data Encodings
XRPL usage:
RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2
What it defines:
XRPL usage:
RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3
Improvements over TLS 1.2:
ITU-T X.690 - ASN.1 encoding rules
What it defines:
Structure:
SEQUENCE {
r INTEGER,
s INTEGER
}
Encoded as:
0x30 [length]
0x02 [r-length] [r]
0x02 [s-length] [s]
NIST SP 800-90A - Recommendation for Random Number Generation
What it defines:
XRPL usage:
RFC 1751 - A Convention for Human-Readable 128-bit Keys
What it defines:
XRPL usage:
Example:
Seed (hex): DEDCE9CE67B451D852FD4E846FCDE31C
Words: MAD WARM EVEN SHOW BALK FELT
TOY STIR OBOE COST HOPE VAIN
NIST SP 800-57 - Recommendation for Key Management
What it defines:
Security Levels:
| Security bits | Symmetric | Hash | RSA key | ECC key |
|---|---|---|---|---|
| 128 | AES-128 | SHA-256 | 3072 | 256 |
| 192 | AES-192 | SHA-384 | 7680 | 384 |
| 256 | AES-256 | SHA-512 | 15360 | 512 |
XRPL compliance:
Website: https://www.openssl.org/
License: Apache License 2.0
What rippled uses:
Repository: https://github.com/bitcoin-core/secp256k1
License: MIT
What rippled uses:
Repository: https://github.com/floodyberry/ed25519-donna
License: Public Domain
What rippled uses:
This module covered the hash functions woven through XRPL. You learned why the ledger leans on SHA-512Half (the first 256 bits of SHA-512), how hash prefixes provide domain separation so a transaction hash can never collide with a ledger-object hash, and how account addresses are derived by RIPEMD160 over SHA-256. Small functions carrying enormous responsibility, behind every id, key, and Merkle root.
To remember:
HashPrefix adds a 4-byte domain tag so a tx hash can never collide with a ledger or validation hash (include/xrpl/protocol/HashPrefix.h)parent_hash plus the two tree roots account_hash and transaction_hashinclude/xrpl/protocol/digest.h; sha512Half_s is the state-wiping variantNext up. Hashes give identity; keys give authority. Next: how key pairs are generated and derived, and what those seed phrases really are.
Resources
Assignments
0 of 2 complete