XRPL Technology

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.

Was this helpful?

Related Questions

Go Deeper

Expand your knowledge with these related lessons

Hash Functions in XRPL

55 minadvanced

Hash Time-Locked Contracts on XRPL

50 minadvanced

Hash-Based Signatures - The Conservative Option

45 minadvanced

Have more questions?

Browse our complete FAQ or contact support.