advanced 60 min

Transaction ordering

How the network agrees on a deterministic, salted canonical order for transactions in a ledger.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain the salted CanonicalTXSet ordering and its BFT role.
  • Understand SeqProxy ordering and tie-breaking.
  • Connect ordering to TxQ fee prioritisation.
Complete this module by mentor review and a quiz. Jump to assessment

Introduction

≈60 min · Advanced · builds on Consensus peers & the UNL

Watch this short video by XRPL Commons first, then dive into the details below.

If two nodes ordered transactions differently, they'd compute different ledgers, so order can't be left to chance. In this module you'll learn XRPL's salted canonical ordering, the sort key that combines account, sequence and transaction id, and how this deterministic order keeps every node in lockstep (and how it connects to fee-based TxQ prioritisation). A small idea with load-bearing consequences.

Canonical Transaction Ordering

In brief: a salted, deterministic order that every node computes identically.

Salted canonical ordering: the same pending transactions from accounts A, B, and C sort as B-A-C under one ledger's salt and C-B-A under the next, deterministic for every node yet unpredictable in advance.

The CanonicalTXSet Class

Location: rippled/include/xrpl/ledger/CanonicalTXSet.h and rippled/src/libxrpl/ledger/CanonicalTXSet.cpp

Purpose: Maintains a set of transactions in a deterministic, canonical order for processing in the XRPL ledger.

The CanonicalTXSet class is central to transaction ordering, ensuring all validators arrive at the same transaction sequence.

Ordering Mechanism: Full Sort Key

Transactions are ordered using a composite key with the following precedence:

1. Salted Account Key (uint256) - Primary Sort

Purpose: Creates unpredictable but deterministic ordering

Mechanism:

uint256 CanonicalTXSet::accountKey(AccountID const& account)
{
    uint256 ret = beast::kZero;
    memcpy(ret.begin(), account.begin(), account.size());
    ret ^= salt_;  // XOR with ledger-specific salt
    return ret;
}

How Salt is Generated:

Why Salting is Necessary: The salt ensures that all honest validators produce identical transaction ordering (Byzantine Fault Tolerance) while making the ordering unpredictable to prevent gaming. The salt must be:

  1. Deterministic - All validators must compute the same salt
  2. Agreed-upon - Derived from data already established by consensus
  3. Unpredictable - Changes every ledger to prevent strategic ordering manipulation
  4. Fair - No account can consistently get priority across ledgers

To achieve these properties, the salt is derived from consensus-agreed data - specifically, hashes that all validators already agreed upon in previous rounds.

Two Contexts Where Salted Ordering is Used:

Context 1: TxQ (Transaction Queue Ordering)

Purpose: Orders transactions in the queue for selection into proposed transaction sets

Salt Source: Hash of the parent (previous) ledger

Location: rippled/src/xrpld/app/misc/detail/TxQ.cpp

LedgerHash const& parentHash = view.header().parentHash;
MaybeTx::parentHashComp = parentHash;  // Salt set to parent ledger hash

How it works:

  • When building ledger N, salt = hash of ledger N-1
  • Example: Building ledger #100 → salt = hash(ledger #99)
  • All validators agreed on ledger N-1 in the previous consensus round
  • Therefore, all validators have the same salt for ledger N
  • Salt changes every ledger, preventing predictable ordering

Context 2: CanonicalTXSet (Retriable Transactions During Consensus)

Purpose: Orders transactions that failed to apply and can be retried in the next round

Salt Source: Hash of the transaction set itself (SHAMap hash)

Location: rippled/src/xrpld/app/consensus/RCLConsensus.cpp

CanonicalTXSet retriableTxs{result.txns.map_->getHash().as_uint256()};

How it works:

  • Salt = hash of the agreed-upon transaction set
  • All validators reach consensus on the transaction set during the current round
  • Therefore, all validators compute the same transaction set hash
  • This hash becomes the salt for ordering retriable transactions
  • Salt changes per consensus round based on the agreed set

The BFT Guarantee:

Both salt sources derive from data that all validators have already agreed upon through consensus:

Salt Source When Agreed BFT Property
Parent ledger hash Previous consensus round Fixed and identical across all honest validators
Transaction set hash Current consensus round Agreed upon during establish phase

Result:

  • All honest validators XOR the same salt with account IDs
  • All honest validators compute identical salted keys
  • All honest validators produce identical ordering
  • Byzantine validators cannot use different salt (would create different ordering → rejected by network)

Why Salting Matters:

Anti-Gaming Measure:

  • Prevents users from predicting their position in queue
  • Account addresses chosen for strategic advantage become useless
  • Cannot create accounts to get consistent priority

Fairness:

  • No account can consistently get priority
  • Each ledger randomizes ordering fairly
  • All accounts have equal opportunity

Security:

  • Prevents strategic manipulation of transaction timing
  • Makes it impossible to game the fee market
  • Protects against certain attack vectors

2. Sequence Proxy (SeqProxy) - Secondary Sort

Purpose: Orders transactions from the same account by their sequence number or ticket

Mechanism:

  • Uses SeqProxy which handles both sequence numbers and tickets
  • Ensures proper ordering within a single account
  • Maintains account transaction chronology

Why This Matters:

  • Account transactions must execute in order
  • Prevents later transactions from executing before earlier ones
  • Maintains account state consistency

3. Transaction ID (uint256) - Tertiary Sort

Purpose: Final tie-breaker to ensure deterministic ordering

Mechanism:

  • Uses transaction hash as last resort for ordering
  • Guarantees unique, deterministic ordering even for identical sequence numbers
  • Lexicographic comparison of transaction IDs

When Used:

  • Identical salted account key and sequence proxy
  • Ensures no ambiguity in final ordering
  • Provides complete determinism

Transaction Insertion

Process:

void CanonicalTXSet::insert(std::shared_ptr<STTx const> const& txn)
{
    map_.insert(std::make_pair(
        Key(accountKey(txn->getAccountID(sfAccount)),
            txn->getSeqProxy(),
            txn->getTransactionID()),
        txn));
}

Steps:

  1. Extract account ID from transaction
  2. Calculate salted account key
  3. Get sequence proxy (sequence or ticket)
  4. Get transaction ID
  5. Create composite key
  6. Insert into ordered map

Result:

  • Transaction placed in deterministic position
  • Same transaction always placed in same position
  • All validators arrive at identical ordering

Set Updates and Modifications

Replacement Logic:

  • If transaction with same account and sequence exists, new transaction may replace old
  • Replacement only valid if it meets criteria (e.g., higher fee)
  • Enforced by external logic, not CanonicalTXSet itself

Removal Handling:

  • When transaction is applied to ledger, it's removed from set
  • Next valid transaction for account is promoted
  • Set maintains proper ordering after removals

Salt Reset:

  • Set can be reset with new salt value
  • Used when starting new consensus round
  • Prevents ordering manipulation across rounds

Benefits of Canonical Ordering

Determinism:

  • Same inputs always produce same ordering
  • Validators independently reach same result
  • No coordination needed for ordering agreement

Efficiency:

  • Reduces consensus rounds needed for agreement
  • Validators propose similar transaction sets
  • Fewer disputes to resolve

Security:

  • Prevents strategic manipulation
  • Fair treatment for all participants
  • Resistant to various attack vectors

Predictability:

  • Users can understand ordering logic
  • Transparent and auditable process
  • Reproducible results

Key idea. The order is salted per ledger so nobody can predict or game it, yet it is deterministic so every node applies the same transactions in the same sequence.

Transaction Set Construction and Proposal

In brief: how a node builds and proposes its transaction set.

Initial Transaction Collection

Validators collect transactions from multiple sources to build their proposed transaction sets:

Network Submissions:

  • Transactions received from connected peers
  • Relayed across the peer-to-peer network
  • Arrive asynchronously from various sources

Local Submissions:

  • Transactions submitted directly to this node
  • From RPC clients or local applications
  • Given same treatment as network transactions

Peer Relays:

  • Transactions forwarded by other validators
  • Help ensure transaction coverage across network
  • Reduce transaction propagation delays

Proposal Set Building: RCLConsensus::Adaptor::onClose

Location: rippled/src/xrpld/app/consensus/RCLConsensus.cpp

Purpose: Prepares the initial transaction set and proposal for the next consensus round.

Process:

  1. Gather Open Transactions:
  • Collect transactions from open ledger
  • Pull from transaction queue (TxQ)
  • Consider fee levels and priorities
  1. Validation Phase:
  • Basic format and signature verification
  • Check transaction well-formedness
  • Verify cryptographic signatures
  1. Preliminary Filtering:
  • Remove obviously invalid transactions
  • Eliminate duplicates
  • Apply business logic checks
  1. Apply Canonical Ordering:
  • Create CanonicalTXSet with current salt
  • Insert transactions in canonical order
  • Generate deterministic transaction sequence
  1. Capacity Management:
  • Respect ledger size limits
  • Account for processing capacity
  • Ensure ledger can be built and validated in time
  1. Fee Prioritization (TxQ only):
  • Higher fee transactions more likely to be selected from queue
  • Affects inclusion in proposed set, NOT execution order within ledger
  • Once in ledger, canonical ordering determines sequence
  1. Account Limits:
  • Enforce per-account transaction limits per ledger
  • Prevent any account from monopolizing ledger space
  • Maintain fairness across accounts
  1. Finalize Proposal:
  • Generate transaction set hash
  • Create proposal message
  • Sign with validator private key

How Ordering Simplifies Consensus

In brief: why one shared order means every node reaches the same ledger.

** Reference:** For consensus mechanics (dispute resolution, avalanche, thresholds), see the Consensus lifecycle & phases module.

Ordering Reduces Dispute Complexity

The Core Benefit: Canonical ordering separates the consensus problem into two independent concerns:

  1. What to include (resolved by consensus voting)
  2. How to order (resolved by deterministic salt)

Impact on Disputes:

Metric Without Canonical Ordering With Canonical Ordering
Dispute Types Inclusion + Sequence Inclusion only
Complexity O(N²) potential disputes O(N) potential disputes
Comparison Compare sets AND orderings Compare sets only
Resolution Must agree on both Ordering automatic from salt

Code Reference: Consensus::createDisputes() at rippled/src/xrpld/consensus/Consensus.h

// Disputes created ONLY for set membership differences
auto differences = result_->txns.compare(o);
for (auto const& [txId, inThisSet] : differences) {
    // Dispute: Is txId IN or OUT? (not WHERE in the sequence)
    result_->disputes.emplace(txID, std::move(dtx));
}

Automatic Ordering Agreement

Once validators agree on which transactions to include (set membership), ordering is automatically identical:

Same Transaction Set + Same Salt → Identical Ordering

Example:
Validators A & B both agree: {TX1, TX2, TX3}
Salt (from previous ledger): 0xABCD...

Result:
Validator A: [TX2, TX1, TX3] ← Deterministic
Validator B: [TX2, TX1, TX3] ← Identical!

Why This Matters:

  • Consensus only resolves one dimension (inclusion)
  • Ordering automatically follows from agreed salt
  • No additional rounds needed for sequencing
  • Faster convergence to final ledger state

Transaction Queue (TxQ) Ordering

In brief: how queued transactions are prioritized by fee.

Fee Level Prioritization

Location: rippled/src/xrpld/app/misc/TxQ.h

OrderCandidates Comparator:

bool operator()(MaybeTx const& lhs, MaybeTx const& rhs) const {
    if (lhs.feeLevel == rhs.feeLevel)
        return (lhs.txID ^ MaybeTx::parentHashComp) < (rhs.txID ^ MaybeTx::parentHashComp);
    return lhs.feeLevel > rhs.feeLevel;
}

Fee Level Concept:

Base Fee:

  • Minimum fee required for transaction inclusion
  • Set by network consensus
  • Varies based on network load

Fee Escalation:

  • Higher fees increase transaction priority within queue
  • Transactions sorted by fee level
  • Market mechanism for prioritization

Dynamic Adjustment:

  • Fee requirements change based on network load
  • When ledger is full, higher fees needed
  • Market finds equilibrium price

Tie-Breaking:

  • When fee levels are equal, use transaction ID XOR parent hash
  • Ensures deterministic ordering
  • Prevents manipulation

Market Mechanism:

  • Users compete through fee levels for inclusion
  • Higher demand increases required fees
  • Supply-demand equilibrium

Per-Account Transaction Limits

Sequence Enforcement:

  • Transactions from same account must execute in sequence order
  • Gaps in sequence numbers create blockers
  • Ensures proper account state progression

Queue Depth Limits:

  • Each account limited to maximumTxnPerAccount transactions in queue
  • Default: 10 transactions per account (configurable in xrpld.cfg)
  • Prevents any account from monopolizing queue space
  • Configurable per-node basis via maximum_txn_per_account setting

Fairness Mechanism:

  • Ensures equitable access across all accounts
  • No single account can dominate queue
  • Balanced resource allocation

Resource Protection:

  • Prevents queue exhaustion attacks
  • Limits memory consumption per account
  • Protects node resources

Queue Management Strategies

Priority Ordering:

  • Sort by fee level within canonical ordering constraints
  • Higher fees get priority
  • But still must respect account sequence

Capacity Planning:

  • Balance queue size with processing capabilities
  • Monitor queue depth
  • Adjust acceptance criteria based on load

Aging Policies:

  • Handle long-queued transactions appropriately
  • May drop very old transactions
  • Prevent unbounded queue growth

Overflow Handling:

  • When queue full, drop lowest fee transactions
  • Manage queue when demand exceeds capacity
  • Clear space for higher value transactions

Transaction Blockers and Retries

Transaction Blocker Concepts

Location: rippled/src/xrpld/app/misc/detail/TxQ.cpp

Dependency Tracking:

What Is a Blocker?

  • A transaction that prevents subsequent transactions from being processed
  • Usually due to missing prior sequence number
  • Creates dependency chain

Example:

  • Account has sequence 100
  • Receives transactions for sequences 101, 103, 104
  • Transaction 101 must execute before 103 and 104
  • If 101 is missing, 103 and 104 are "blocked"

Account State Requirements:

  • Prerequisite conditions must be met
  • Sufficient balance for fees and operations
  • Proper sequence numbering
  • Valid account state

Sequence Gap Handling:

  • Detect missing sequence numbers in chains
  • Hold later transactions until gaps filled
  • Prevent out-of-order execution

Resource Availability:

  • Verify sufficient account resources exist
  • Check balance covers fees and reserves
  • Ensure operations can complete

Retry Mechanisms

Temporary vs. Permanent Failures:

Temporary Failures:

  • Insufficient fee (can be retried with higher fee)
  • Sequence gaps (retry when gap filled)
  • Temporary resource shortages

Permanent Failures:

  • Invalid signature
  • Malformed transaction
  • Impossible operations

Backoff Strategies:

  • Implement intelligent retry timing
  • Don't retry immediately
  • Exponential backoff for repeated failures

Retry Limits:

  • Each transaction has retriesAllowed count
  • Default: 10 retries per transaction (MaybeTx::retriesAllowed = 10)
  • Prevents infinite retry loops
  • After exhausting retries, transaction is dropped from queue
  • Account penalty flags may be set, reducing retries for other transactions from that account

Success Tracking:

  • Monitor retry success rates
  • Optimize retry policies
  • Learn from patterns

Queue Maintenance

Periodic Cleanup:

  • Remove expired or invalid transactions
  • Free memory from old transactions
  • Maintain queue health

State Synchronization:

  • Keep queue consistent with ledger state
  • Remove transactions that became invalid
  • Update based on ledger changes

Memory Management:

  • Prevent unbounded queue growth
  • Enforce size limits
  • Prioritize valuable transactions

Performance Monitoring:

  • Track queue efficiency metrics
  • Monitor fill rates
  • Optimize parameters

Error Recovery

Graceful Degradation:

  • Maintain service during partial failures
  • Continue processing what's possible
  • Degrade functionality rather than fail

State Recovery:

  • Rebuild queue state after system restarts
  • Recover from crashes
  • Restore consistent state

Consistency Checks:

  • Verify queue integrity periodically
  • Detect and fix corruption
  • Maintain data structure invariants

Fallback Procedures:

  • Alternative processing when primary mechanisms fail
  • Emergency modes for unusual conditions
  • Ensure continuous operation

Supporting Classes and Utilities

RCLCxTx

Location: rippled/src/xrpld/app/consensus/RCLCxTx.h

Purpose: Adapts a SHAMapItem transaction for consensus

Functionality:

  • Wraps transaction for consensus processing
  • Provides common interface
  • Handles XRPL-specific transaction details

RCLTxSet

Location: rippled/src/xrpld/app/consensus/RCLCxTx.h

Purpose: Adapts a SHAMap to represent a set of transactions

Functionality:

  • Transaction set representation
  • Efficient storage and lookup
  • Merkle tree structure for verification

ConsensusProposal

Location: rippled/src/xrpld/consensus/ConsensusProposal.h

Purpose: Represents a proposal made by a node during consensus

Contains:

  • Proposed transaction set hash
  • Close time
  • Sequence number
  • Signature

References to Source Code


Summary

This module explained why and how a ledger's transactions get a single, agreed order. XRPL uses a salted canonical ordering: a per-ledger salt makes the order unpredictable, while a deterministic sort key (the salted account key, then SeqProxy, then transaction id) makes every node compute the same order, and therefore the same resulting ledger. You also saw how the transaction queue prioritises queued transactions by fee.

To remember:

  • CanonicalTXSet orders a ledger's transactions deterministically with a per-ledger salt
  • Sort key: salted account key, then SeqProxy, then transaction id
  • Same-account transactions always stay in sequence order
  • The salt is unpredictable in advance but identical on every node: no position gaming, byte-identical ledgers
  • Consensus agrees on the SET; the order is then computed, not negotiated
  • The TxQ's fee-level ordering is a different thing: it ranks WAITING transactions, not the closed ledger
  • Code: include/xrpl/ledger/CanonicalTXSet.h
  • Watch out: never infer priority or economics from a transaction's position inside a closed ledger; the order is salted

Next up. Consensus closed. Final teaching phase: time to change the protocol itself. First, study a real, recent, big extension: the AMM architecture.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026