How are transaction hashes computed?
Last updated:
Transaction hashes on the XRP Ledger serve as unique identifiers for every transaction, computed using a deterministic cryptographic process. Understanding this computation is essential for developers building wallets, explorers, or verification systems.
The Transaction Hash Formula
A transaction hash is computed as:
``` TransactionHash = SHA-512Half(HashPrefix || SerializedTransaction) ```
Where: - SHA-512Half: First 256 bits of SHA-512 - HashPrefix: 4-byte prefix '54584E00' (hex) = 'TXN\0' (ASCII) - ||: Concatenation operator - SerializedTransaction: Canonical binary encoding
Step-by-Step Process
Step 1: Prepare Transaction
Remove signature-related fields (they're not part of the hash):
```javascript function prepareTransactionForHashing(tx) { // Create copy to avoid mutating original const txCopy = { ...tx }; // Remove fields that are NOT part of hash computation delete txCopy.Signers; // Multi-sig signatures delete txCopy.TxnSignature; // Single signature delete txCopy.hash; // The hash itself (if present) // All other fields are included return txCopy; }
const transaction = { TransactionType: 'Payment', Account: 'rN7n7otQDd6FczFgLdlqtyMVrn3HMfgnZh', Destination: 'rLHzPsX6oXkzU9rFkyJ5R8J4HqBzZT4Sfp', Amount: '1000000', Fee: '12', Sequence: 1, SigningPubKey: 'ED9434799226374926EDA3B54B1B461B4ABF7237962EAE18528FEA67595397FA32', TxnSignature: '...' // This will be removed };
const prepared = prepareTransactionForHashing(transaction); ```
Step 2: Canonical Serialization
Serialize the transaction using ripple-binary-codec:
```javascript const { encode } = require('ripple-binary-codec');
function serializeTransaction(tx) { // Canonical encoding ensures: // - Deterministic field ordering // - Consistent type encoding // - No ambiguity in representation const encoded = encode(tx); return Buffer.from(encoded, 'hex'); }
// Example serialization const serialized = serializeTransaction(prepared); console.log('Serialized (hex):', serialized.toString('hex')); console.log('Length:', serialized.length, 'bytes'); ```
Canonical Encoding Rules:
```python # Field ordering by field ID (not alphabetical) field_ids = { 'TransactionType': (1, 1), # Type 1, Field 1 'Flags': (2, 2), # Type 2, Field 2 'Sequence': (2, 4), # Type 2, Field 4 'Amount': (6, 1), # Type 6, Field 1 'Fee': (6, 8), # Type 6, Field 8 'Account': (8, 1), # Type 8, Field 1 'Destination': (8, 3), # Type 8, Field 3 # ... many more fields }
# Encoding process: # 1. Sort fields by (type, field) tuple # 2. Encode each field with type prefix # 3. Concatenate in order # 4. Result is deterministic and unique ```
Step 3: Add Hash Prefix
Prepend the transaction hash prefix:
```javascript function addHashPrefix(serialized) { // Transaction hash prefix: 'TXN\0' const prefix = Buffer.from('54584E00', 'hex'); // Concatenate prefix and serialized transaction return Buffer.concat([prefix, serialized]); }
const withPrefix = addHashPrefix(serialized); ```
Why Hash Prefix?
Hash prefixes prevent collision attacks:
```python # Without prefix: hash(ledger_data) could equal hash(transaction_data) # If they happen to encode to same bytes
# With prefix: hash('LWR\0' || ledger_data) != hash('TXN\0' || transaction_data) # Impossible collision due to different prefixes
# This is called "domain separation" in cryptography ```
Step 4: Compute SHA-512Half
Apply SHA-512 and take first 256 bits:
```javascript const crypto = require('crypto');
function computeHash(data) { // Compute full SHA-512 (512 bits) const fullHash = crypto.createHash('sha512') .update(data) .digest(); // Take first 32 bytes (256 bits) const sha512Half = fullHash.slice(0, 32); // Convert to uppercase hex (XRPL convention) return sha512Half.toString('hex').toUpperCase(); }
const transactionHash = computeHash(withPrefix); console.log('Transaction hash:', transactionHash); // Example: E4C6D7B6... (64 hex characters = 256 bits) ```
Complete Implementation
```javascript const { encode } = require('ripple-binary-codec'); const crypto = require('crypto');
function computeTransactionHash(transaction) { // Step 1: Remove signature fields const tx = { ...transaction }; delete tx.Signers; delete tx.TxnSignature; delete tx.hash; // Step 2: Canonical serialization const encoded = encode(tx); const serialized = Buffer.from(encoded, 'hex'); // Step 3: Add hash prefix const prefix = Buffer.from('54584E00', 'hex'); // 'TXN\0' const withPrefix = Buffer.concat([prefix, serialized]); // Step 4: SHA-512Half const fullHash = crypto.createHash('sha512') .update(withPrefix) .digest(); const hash = fullHash.slice(0, 32); return hash.toString('hex').toUpperCase(); }
// Usage const tx = { TransactionType: 'Payment', Account: 'rN7n7otQDd6FczFgLdlqtyMVrn3HMfgnZh', Destination: 'rLHzPsX6oXkzU9rFkyJ5R8J4HqBzZT4Sfp', Amount: '1000000', Fee: '12', Sequence: 1, SigningPubKey: 'ED5F5AC8B98974A3CA843326D9B88CEBD0560177B973EE0B149F04795F0DC6A983' };
const hash = computeTransactionHash(tx); console.log('Hash:', hash); ```
Using XRPL Libraries
Most developers should use existing libraries:
```javascript const xrpl = require('xrpl');
// Method 1: Hash is returned when signing const wallet = xrpl.Wallet.fromSeed('sEdV...'); const tx = { TransactionType: 'Payment', Account: wallet.address, Destination: 'rReceiver...', Amount: '1000000', Fee: '12', Sequence: 1 };
const signed = wallet.sign(tx); console.log('Transaction hash:', signed.hash); // Hash is automatically computed
// Method 2: Compute hash from signed blob const { decode } = require('ripple-binary-codec'); const { computeTransactionHash } = require('ripple-hashes');
const decoded = decode(signed.tx_blob); const hash = computeTransactionHash(decoded); console.log('Recomputed hash:', hash); ```
Hash Properties
Determinism:
```javascript // Same transaction always produces same hash const tx = { /* ... */ };
const hash1 = computeTransactionHash(tx); const hash2 = computeTransactionHash(tx);
console.log('Hashes equal:', hash1 === hash2); // Result: true (always) ```
Uniqueness:
```javascript // Different transactions produce different hashes const tx1 = { TransactionType: 'Payment', Account: 'rN7n7...', Sequence: 1, Amount: '1000000' };
const tx2 = { TransactionType: 'Payment', Account: 'rN7n7...', Sequence: 2, // Different sequence Amount: '1000000' };
const hash1 = computeTransactionHash(tx1); const hash2 = computeTransactionHash(tx2);
console.log('Hashes different:', hash1 !== hash2); // Result: true (practically always) ```
Signature Independence:
```javascript // Hash doesn't include signature const tx = { /* transaction without signature */ }; const hash = computeTransactionHash(tx);
// Add signature (doesn't change hash) tx.TxnSignature = '3045...'; const hashAfterSigning = computeTransactionHash(tx);
console.log('Hash unchanged:', hash === hashAfterSigning); // Result: true (signature is removed before hashing) ```
What's Included in Hash
All transaction fields EXCEPT signatures:
```javascript const includedFields = [ 'TransactionType', 'Account', 'Destination', 'Amount', 'Fee', 'Sequence', 'SigningPubKey', // Public key IS included 'SourceTag', 'DestinationTag', 'LastLedgerSequence', 'Memos', 'Paths', 'SendMax', 'NetworkID', // ... all other transaction fields ];
const excludedFields = [ 'TxnSignature', // Single signature 'Signers', // Multi-sig signatures 'hash' // The hash itself ]; ```
Why Public Key is Included:
```javascript // Public key included in hash because: // 1. It's part of transaction identity // 2. Different signers = different transactions // 3. Prevents signature substitution attacks
const tx = { Account: 'rN7n7...', SigningPubKey: 'ED5F5AC8...', // This IS part of hash TxnSignature: '3045...', // This is NOT part of hash // ... }; ```
Multi-Signed Transaction Hashing
Multi-signed transactions are hashed differently:
```javascript // Multi-signed transaction const multiSignedTx = { TransactionType: 'Payment', Account: 'rMultiSig...', Amount: '1000000', Fee: '12', Sequence: 1, SigningPubKey: '', // Empty for multi-sig Signers: [ // Multiple signatures { Signer: { Account: 'rSigner1...', SigningPubKey: 'ED...', TxnSignature: '...' } }, { Signer: { Account: 'rSigner2...', SigningPubKey: 'ED...', TxnSignature: '...' } } ] };
// Hash computation: // 1. Remove entire 'Signers' array // 2. Hash remaining fields // 3. Signers array is validated separately const hash = computeTransactionHash(multiSignedTx); // Signers array not included in hash ```
Verification Example
```javascript const xrpl = require('xrpl');
async function verifyTransactionHash(client, txHash) { // Fetch transaction from ledger const response = await client.request({ command: 'tx', transaction: txHash, binary: false }); const tx = response.result; // Recompute hash const { decode } = require('ripple-binary-codec'); const { computeTransactionHash } = require('ripple-hashes'); // Use tx object from ledger (already has signature removed for hash) const recomputedHash = computeTransactionHash(tx); console.log('Ledger hash:', tx.hash); console.log('Recomputed:', recomputedHash); console.log('Match:', tx.hash === recomputedHash); return tx.hash === recomputedHash; } ```
Common Issues
Issue 1: Field Order
```javascript // WRONG: Manual serialization with wrong order const wrong = JSON.stringify(tx); // JSON.stringify uses alphabetical order // XRPL uses canonical field ID order
// CORRECT: Use ripple-binary-codec const correct = encode(tx); ```
Issue 2: Amount Representation
```javascript // Amounts must be strings (for XRP) or objects (for tokens)
// WRONG const tx1 = { Amount: 1000000 // Number };
// CORRECT const tx2 = { Amount: '1000000' // String };
// Different hash due to encoding difference ```
Issue 3: Including Signature
```javascript // WRONG: Including signature in hash const tx = { Account: 'rN7n7...', TxnSignature: '3045...', // ... }; const wrongHash = computeHash(encode(tx)); // Signature should be removed first!
// CORRECT delete tx.TxnSignature; const correctHash = computeHash(encode(tx)); ```
Performance Considerations
```javascript // Transaction hash computation is fast const iterations = 10000; const tx = { /* typical transaction */ };
const start = Date.now(); for (let i = 0; i < iterations; i++) { computeTransactionHash(tx); } const elapsed = Date.now() - start;
console.log('Hashes per second:', Math.round(iterations / (elapsed / 1000))); // Result: ~100,000 - 500,000 hashes/second // Very fast, not a bottleneck ```
Security Considerations
```python # Hash collision resistance collision_probability = 2^128 # Birthday attack # = 340,282,366,920,938,463,463,374,607,431,768,211,456 # Computationally infeasible
# Preimage attack resistance preimage_probability = 2^256 # = 115,792,089,237,316,195,423,570,985,008,687,907,853, # 269,984,665,640,564,039,457,584,007,913,129,639,936 # Absolutely impossible with any conceivable technology ```
Transaction hashes provide unique, tamper-proof identifiers for every XRPL transaction. The deterministic computation ensures that any party can independently verify a transaction's hash, making it a cornerstone of XRPL's trustless verification model.