How the network agrees on a deterministic, salted canonical order for transactions in a ledger.
What you'll learn
≈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.
In brief: a salted, deterministic order that every node computes identically.
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.
Transactions are ordered using a composite key with the following precedence:
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:
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:
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:
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:
Why Salting Matters:
Anti-Gaming Measure:
Fairness:
Security:
Purpose: Orders transactions from the same account by their sequence number or ticket
Mechanism:
SeqProxy which handles both sequence numbers and ticketsWhy This Matters:
Purpose: Final tie-breaker to ensure deterministic ordering
Mechanism:
When Used:
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:
Result:
Replacement Logic:
Removal Handling:
Salt Reset:
Determinism:
Efficiency:
Security:
Predictability:
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.
In brief: how a node builds and proposes its transaction set.
Validators collect transactions from multiple sources to build their proposed transaction sets:
Network Submissions:
Local Submissions:
Peer Relays:
Location: rippled/src/xrpld/app/consensus/RCLConsensus.cpp
Purpose: Prepares the initial transaction set and proposal for the next consensus round.
Process:
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.
The Core Benefit: Canonical ordering separates the consensus problem into two independent concerns:
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));
}
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:
In brief: how queued transactions are prioritized by fee.
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:
Fee Escalation:
Dynamic Adjustment:
Tie-Breaking:
Market Mechanism:
Sequence Enforcement:
Queue Depth Limits:
maximumTxnPerAccount transactions in queuexrpld.cfg)maximum_txn_per_account settingFairness Mechanism:
Resource Protection:
Priority Ordering:
Capacity Planning:
Aging Policies:
Overflow Handling:
Location: rippled/src/xrpld/app/misc/detail/TxQ.cpp
Dependency Tracking:
What Is a Blocker?
Example:
Account State Requirements:
Sequence Gap Handling:
Resource Availability:
Temporary vs. Permanent Failures:
Temporary Failures:
Permanent Failures:
Backoff Strategies:
Retry Limits:
retriesAllowed countMaybeTx::retriesAllowed = 10)Success Tracking:
Periodic Cleanup:
State Synchronization:
Memory Management:
Performance Monitoring:
Graceful Degradation:
State Recovery:
Consistency Checks:
Fallback Procedures:
Location: rippled/src/xrpld/app/consensus/RCLCxTx.h
Purpose: Adapts a SHAMapItem transaction for consensus
Functionality:
Location: rippled/src/xrpld/app/consensus/RCLCxTx.h
Purpose: Adapts a SHAMap to represent a set of transactions
Functionality:
Location: rippled/src/xrpld/consensus/ConsensusProposal.h
Purpose: Represents a proposal made by a node during consensus
Contains:
rippled/include/xrpl/ledger/CanonicalTXSet.h - Canonical transaction set headerrippled/src/libxrpl/ledger/CanonicalTXSet.cpp - Canonical ordering implementationrippled/src/xrpld/app/consensus/RCLConsensus.cpp - Consensus proposal creationrippled/src/xrpld/app/consensus/RCLCxTx.h - Transaction and set adaptorsrippled/src/xrpld/consensus/Consensus.h - Generic consensus templaterippled/src/xrpld/consensus/Consensus.cpp - Consensus state determinationrippled/src/xrpld/consensus/ConsensusTypes.h - Consensus data structuresrippled/src/xrpld/consensus/DisputedTx.h - Dispute tracking and resolutionrippled/src/xrpld/consensus/ConsensusParms.h - Consensus parametersrippled/src/xrpld/app/misc/TxQ.h - Transaction queue headerrippled/src/xrpld/app/misc/detail/TxQ.cpp - Transaction queue implementationrippled/src/xrpld/consensus/ConsensusProposal.h - Proposal structureThis 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 saltSeqProxy, then transaction idinclude/xrpl/ledger/CanonicalTXSet.hNext up. Consensus closed. Final teaching phase: time to change the protocol itself. First, study a real, recent, big extension: the AMM architecture.
Resources
Assignments
0 of 2 complete