- 1. Introduction
- 2. System Design
- 3. Transaction Processing Pipeline
- 4. Transaction Result Codes
- 5. Ledger Views and Sandboxes
Important
N.B.: Transaction processing in rippled is a complex system. This document presents a simplified view focused on providing sufficient context for understanding payment-related documentation. It covers the essential concepts and mechanisms without exhaustively detailing every aspect of transaction processing.
Transactions are the mechanism for modifying the XRP Ledger state. New transactions representing user intent enter the network exclusively through RPC submission - clients submit transactions via commands like submit or submit_multisigned to a rippled server. Once a transaction passes initial validation, it is relayed to other nodes through peer-to-peer propagation via TMTransaction protocol messages.
Every transaction, regardless of how it arrived at a node, goes through the same three-phase processing pipeline: preflight (static validation), preclaim (ledger-based validation), and doApply (execution). All transaction types inherit from the Transactor base class, which provides the common infrastructure for these validation and execution stages. Both RPC-submitted and peer-propagated transactions converge at processTransaction, which orchestrates the preflight, preclaim, and doApply stages.
When a ledger closes, consensus determines which transactions are included and each server independently computes the same deterministic transaction order. Transactions are then applied in multiple passes to ensure all transactions that can successfully execute are included in the ledger, with early passes allowing retries for transactions that may succeed after other transactions are applied.
The diagram below shows the key classes involved in transaction processing. Methods shown are commonly used during transaction validation and execution, not an exhaustive list.
classDiagram
class STObject {
<<base class>>
+getAccountID(SField)
+isFieldPresent(SField)
+getFieldAmount(SField)
+isFlag(uint32_t)
}
class STTx {
+getTransactionID()
+getTxnType()
+getSeqProxy()
+getSigningPubKey()
+checkSign()
}
class Transactor {
<<abstract>>
#ApplyContext ctx_
#AccountID account_
#XRPAmount mPriorBalance
#XRPAmount mSourceBalance
+operator()() ApplyResult
+apply() TER
+doApply()* TER
+preclaim()$ TER
+checkSeqProxy()$ NotTEC
+checkFee()$ TER
+checkSign()$ NotTEC
}
class Payment {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class CreateOffer {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class CancelOffer {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class SetTrust {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class AMMCreate {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class AMMDeposit {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class AMMWithdraw {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class AMMVote {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class AMMBid {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class AMMDelete {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class CredentialCreate {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class CredentialAccept {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class CredentialDelete {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class PermissionedDomainSet {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class PermissionedDomainDelete {
+preflight()$ NotTEC
+preclaim()$ TER
+doApply() TER
}
class PreflightContext {
+Application app
+STTx tx
+Rules rules
+ApplyFlags flags
}
class PreclaimContext {
+Application app
+ReadView view
+STTx tx
+TER preflightResult
+ApplyFlags flags
}
class ApplyContext {
+Application app
+OpenView view
+STTx tx
+TER preclaimResult
}
class PreflightResult {
+STTx tx
+TxConsequences consequences
+NotTEC ter
}
class PreclaimResult {
+ReadView view
+STTx tx
+TER ter
+bool likelyToClaimFee
}
class ApplyResult {
+TER ter
+bool applied
+TxMeta metadata
}
STTx --|> STObject : inherits
STTx --> Transactor : processed by
Transactor <|-- Payment
Transactor <|-- CreateOffer
Transactor <|-- CancelOffer
Transactor <|-- SetTrust
Transactor <|-- AMMCreate
Transactor <|-- AMMDeposit
Transactor <|-- AMMWithdraw
Transactor <|-- AMMVote
Transactor <|-- AMMBid
Transactor <|-- AMMDelete
Transactor <|-- CredentialCreate
Transactor <|-- CredentialAccept
Transactor <|-- CredentialDelete
Transactor <|-- PermissionedDomainSet
Transactor <|-- PermissionedDomainDelete
PreflightContext --> PreflightResult : used to create
PreclaimContext --> PreclaimResult : used to create
ApplyContext --> ApplyResult : used to create
Figure: Simplified Transaction Class Diagram showing payment-related Transactors
Transaction processing follows a three-phase pipeline: preflight (static validation), preclaim (ledger-based validation), and doApply (execution). Each phase can fail and return an error to the client. The Transactor base class coordinates this flow by calling into derived transaction classes at specific validation and execution points.
The table below shows all functions called during each phase. The "Implemented By" column indicates whether the function is implemented in applySteps.cpp (the top-level orchestrator for each phase), the Transactor base class (providing common behavior for all transactions), or the Derived transaction-specific class (e.g., Payment, AMMCreate). "Transactor (overridable)" means the base class provides a default implementation that derived classes may optionally override.
| Phase | Function | Implemented By | Description |
|---|---|---|---|
| Preflight | invokePreflight<T>() |
Transactor | Orchestrates preflight phase: checks tx type feature, calls other checks |
checkExtraFeatures() |
Transactor (overridable) | Check if optional fields require specific amendments | |
preflight1() |
Transactor | Basic validation (account, fee, flags) - calls preflight0() |
|
preflight() |
Derived | Required override - transaction-specific static validation | |
preflight2() |
Transactor | Signature validation | |
preflightSigValidated() |
Transactor (overridable) | Optional post-signature validation | |
| Preclaim | invoke_preclaim() |
applySteps.cpp | Orchestrates preclaim phase |
checkSeqProxy() |
Transactor | Validate sequence number or ticket | |
checkPriorTxAndLastLedger() |
Transactor | Check prior transaction and last ledger sequence | |
checkPermission() |
Transactor | Verify account permissions | |
checkSign() |
Transactor | Verify signature authorization | |
checkFee() |
Transactor | Verify sufficient balance for fee | |
preclaim() |
Derived | Transaction-specific ledger-based validation | |
| Apply | doApply() |
applySteps.cpp | Orchestrates apply phase |
operator()() |
Transactor | Entry point, exception handling | |
apply() |
Transactor | Orchestrates doApply flow | |
preCompute() |
Transactor | Initialize balances | |
consumeSeqProxy() |
Transactor | Consume sequence or delete ticket | |
payFee() |
Transactor | Deduct transaction fee | |
doApply() |
Derived | Required override - transaction-specific execution |
Every transaction is processed through three distinct phases:
preflight -> preclaim -> doApply
Purpose: Static validation - checks that don't require ledger state
Context: PreflightContext
app: Application instancetx: Transaction being validatedrules: Amendment rules in effectflags: Apply flags
Validation flow:
Preflight validation is orchestrated by Transactor::invokePreflight<T>() which calls the following stages in order:
-
Transaction Type Feature Check: Verify the transaction type itself is enabled
- Check if transaction type requires a specific amendment (via
Permission::getInstance().getTxFeature()) - Return
temDISABLEDif required amendment is not enabled
- Check if transaction type requires a specific amendment (via
-
checkExtraFeatures(): Check optional field amendments (Transactor base class method)
- Each transaction can override to check if optional fields require specific amendments
- Called before preflight1, allows early rejection based on amendment rules
- Example: Payment checks if
sfCredentialIDsfield requiresfeatureCredentialsamendment - Example: CreateOffer checks if
sfDomainIDfield requiresfeaturePermissionedDEXamendment - Returns
false(causestemDISABLED) if required amendments are not enabled - Returns
trueby default (base class implementation)
-
preflight1(): Account and fee field validation (Transactor base class method)
- Check
sfTicketSequencefield validity (requiresfeatureTicketBatchamendment) - Check
sfDelegatefield validity (requiresfeaturePermissionDelegationV1_1amendment) - Calls preflight0() internally for early sanity checks:
- Verify transaction ID is not zero
- Verify NetworkID matches (for networks > 1024)
- Check for invalid pseudo-transaction flags
- Verify
Accountfield is present and not zero - Validate
Feefield is XRP, non-negative, and within acceptable range - Check signing key validity via
preflightCheckSigningKey() - Verify
AccountTxnIDandTicketSequenceare not both present (incompatible) - Check
tfInnerBatchTxnflag validity (requiresfeatureBatchamendment)
- Check
-
Derived::preflight(): Transaction-specific validation (override in derived class)
- Each transaction type implements its own preflight checks
- Example: Payment verifies amount fields, path structure, etc.
- Returns
NotTECerror code ortesSUCCESS
-
preflight2(): Signature validation (Transactor base class method)
- Check for simulation mode via
preflightCheckSimulateKeys() - Verify signature appears valid (cryptographic check)
- Validate multi-signature if present
- Check signature authorization requirements
- Check for simulation mode via
-
preflightSigValidated(): Post-signature validation (Transactor base class method, rarely overridden)
- Optional checks after signature validation
- Returns
tesSUCCESSby default
Output: PreflightResult containing:
- Transaction result code (NotTEC)
- TxConsequences (fee, potential spend, sequences consumed)
- Original context information
Transactions that fail preflight validation are never added to the ledger. Preflight returns error codes like tem (malformed) that indicate fundamental problems with the transaction format. Since preflight does not access ledger state, these failures are detected before the transaction could claim a fee or consume a sequence number. If preflight fails, preclaim is not executed.1
Transaction Consequences:
During preflight, each transaction computes its TxConsequences - metadata describing the transaction's impact on the account and subsequent transactions. Transactions are classified into two categories: normal transactions (payments, offers, etc.) that perform standard operations, and blocker transactions that modify account properties affecting whether subsequent transactions can claim a fee (such as setting authorization requirements). The consequences track several properties:
fee_: Transaction fee in XRPpotentialSpend_: Maximum XRP that could be spent (excluding fee)seqProx_: Sequence or ticket being usedsequencesConsumed_: Number of sequences consumed (usually 1)
These properties are read by TxQ (transaction queue) to determine if transactions can be queued, estimate account balance, and determine transaction ordering constraints.
Purpose: Ledger-based validation - determines if transaction will claim a fee
Context: PreclaimContext
app: Application instanceview: Read-only ledger viewtx: Transaction being validatedpreflightResult: Result from preflightflags: Apply flags
Validation checks:
Preclaim validation is divided into two phases:
Phase 1: Pre-signature validation (must return NotTEC - no tec codes allowed)
checkSeqProxy: Verify sequence number or ticket existscheckPriorTxAndLastLedger: Check PriorTxnID and LastLedgerSequence fieldscheckPermission: Verify delegate permissions (if sfDelegate field present); can be overridden by specific transactions for additional permission checkscheckSign: Verify signature matches account authorization (master key, regular key, or multisig)
All checks before and including signature verification must return NotTEC codes. Allowing tec results before signature verification would risk fee theft, as the fee would be charged before confirming the signature is valid.
Phase 2: Post-signature validation (can return TER including tec codes)
checkFee: Verify account has sufficient balance for fee- Transaction-specific checks (from derived class):
- Implemented in derived class
preclaim()method - Example: Payment checks if destination exists, validates paths, credentials, etc.
- Implemented in derived class
Output: PreclaimResult containing:
- Transaction result code
likelyToClaimFeeflag (true if tesSUCCESS or tec code)- Original context information
Transactions that fail preclaim may or may not be added to the ledger depending on the error code. The likelyToClaimFee flag is set to true if the preclaim result is tesSUCCESS or a tec error code (values >= 100).2 Transactions with tec errors are added to the ledger, consume the fee, and increment the account's sequence number, even though the transaction's intended operation fails. Other error codes (tem, tef, ter, tel) result in the transaction not being added to the ledger.3 This distinction ensures the network is protected from spam (by charging fees for transactions that pass basic validation) while not penalizing users for transactions that fail due to malformation or other non-chargeable issues.
Purpose: Execute the transaction and modify ledger state
Context: ApplyContext
app: Application instancetx: Transaction being executedpreclaimResult: Result from preclaimview(): Writable ledger view (OpenView)
Execution flow:
-
doApply wrapper (in applySteps.cpp):
- Verifies ledger sequence matches between preclaim and apply views
- Returns
{tefEXCEPTION, false}if sequence mismatch - Checks
likelyToClaimFeeflag - if false, returns preclaim result without applying - Creates ApplyContext and invokes the transactor
- Catches exceptions and returns
{tefEXCEPTION, false}on any exception
-
Transactor::operator() (entry point for transaction execution):
- Checks if preclaim result is
tesSUCCESS - If yes, calls
apply()method - Handles various result codes (tecOVERSIZE, tecKILLED, etc.)
- Determines if transaction should be applied to ledger
- Checks if preclaim result is
-
Transactor::apply() (base class execution):
- Calls
preCompute()to initialize mPriorBalance and mSourceBalance - Calls
consumeSeqProxy()to consume sequence or delete ticket - Calls
payFee()to deduct transaction fee - Updates AccountTxnID if present
- Calls derived class
doApply()for transaction-specific logic
- Calls
-
Derived class::doApply() (transaction-specific):
- Implements the actual transaction logic
- Modifies ledger state through the view
- Returns TER code indicating success/failure
Output: ApplyResult containing:
- Final TER code
appliedflag (whether transaction was applied to ledger)- Transaction metadata (if applied)
Transaction result codes (TER) are categorized by prefix and meaning:
| Prefix | Range | Meaning | Fee Claimed | Included in Ledger |
|---|---|---|---|---|
| tel | -399 to -300 | Local error - should not be relayed | No | No |
| tem | -299 to -200 | Malformed transaction - permanent failure | No | No |
| tef | -199 to -100 | Failed to apply - not retried, but could succeed under different ledger state4 | No | No |
| ter | -99 to -1 | Temporary failure that will be retried by the server that returned the result code | No | No |
| tes | 0 | Success | Yes | Yes |
| tec | 100+ | Claimed fee - failed but fee charged | Yes | Yes |
Ledger views provide controlled access to the ledger state during transaction processing. The view system implements a hierarchy where each layer can wrap another, allowing for staged state changes and conditional application. Changes made to a view can be applied to its parent or discarded.
Each layer:
- Reads through to parent layers
- Writes accumulate at current layer
- apply() pushes changes to parent
RawView
Subclasses can modify any ledger entries.
ReadView
Provides read-only access to ledger state:
- Query ledger entries via read()
- Check existence via exists()
- Access fees and amendment rules
ApplyView
Extends ReadView with write operations:
- peek(): Get mutable reference to ledger entry
- insert(): Create new ledger entry
- update(): Mark entry as modified
- erase(): Delete ledger entry
Changes are tracked but not committed until explicitly applied.
Sandbox
A writable view that batches state changes:
- Layers on top of another ApplyView or ReadView
- Accumulates all state modifications in memory
- Changes applied atomically via apply(RawView&) (the parent view implements RawView) or discarded by destructing the sandbox
Usage pattern:
// Create sandbox on top of base view
Sandbox sb(&baseView);
// Make changes
auto sle = sb.peek(keylet::account(alice));
sle->setFieldU32(sfSequence, 100);
sb.update(sle);
// Apply all changes atomically
sb.apply(ctx.rawView());
// OR: discard by letting sb go out of scopePaymentSandbox
During a payment or offer crossing, intermediate steps transfer funds between accounts. Without special handling, credits from one step could make subsequent steps see inflated balances, allowing more liquidity than actually exists.
PaymentSandbox maintains two tracking systems:
-
Normal sandbox (
items_): Tracks all actual ledger entry modifications:- AccountRoot balance changes (XRP)
- RippleState balance changes (tokens/IOUs)
- MPToken balance changes (MPTs)
- AccountRoot owner count changes
- Any other ledger entry modifications
-
Deferred credits table (
tab_): Tracks metadata for query purposes during transaction execution:- Credits, debits, self-debits, and original balances (for XRP, tokens, and MPTs)
- Maximum owner count seen per account
Hooks for Balance Management:
Accounts in a payment are not allowed to use assets acquired during that payment. Balance hooks are virtual methods declared on ReadView and ApplyView that PaymentSandbox overrides to enforce this rule. When the flow engine queries an account's balance (e.g., via accountHolds or xrpLiquid), the balance hook subtracts newly acquired credits, so subsequent steps see only the pre-payment balance. Credit hooks record each transfer into tab_ so the balance hooks have the data they need. There are separate hooks for IOUs (XRP and tokens) and MPTs:
IOU Hooks (XRP and Tokens):
balanceHookIOU(account, issuer, amount): Returns the usable balance, adjusted so that newly acquired assets are not counted5creditHookIOU(from, to, amount, preCreditBalance): Records IOU credits intab_for later querying
MPT Hooks:
balanceHookMPT(account, issue, amount): Returns the usable MPT balance, adjusted so that newly acquired assets are not countedbalanceHookSelfIssueMPT(issue, amount): Returns issuer's self-debit balance for MPTcreditHookMPT(from, to, amount, preCreditBalanceHolder, preCreditBalanceIssuer): Records MPT credits intab_for later queryingissuerSelfDebitHookMPT(issue, amount, preCreditBalance): Records issuer self-debit operations intab_
Note: Actual balance changes are always written through view.update() which modifies items_. The credit hooks are called alongside the actual change to track metadata in tab_ for query purposes during transaction execution.
Hooks for Reserve Management:
Accounts cannot use freed reserves acquired during the transaction's execution. PaymentSandbox enforces this through:
-
ownerCountHook(account, count): Returns the maximumOwnerCountthe account has reached during the transaction's execution (tracked intab_), not the current value. When calculating available balance (viaxrpLiquid), this ensures freed reserves cannot be used mid-transaction. -
adjustOwnerCountHook(account, cur, next): Records owner count changes intab_to maintain the maximum value across all nested payment sandboxes.
Example: Account starts with OwnerCount = 3:
- Transaction deletes a trust line -> OwnerCount becomes 2 (written to
items_, tracked intab_) - Reserve calculation checks available balance
ownerCountHookreturns 3 (max fromtab_)- Account cannot use the freed reserve until transaction completes
Applying Changes:
When apply() is called, changes are committed as follows:
-
apply(RawView& to): Commits allitems_to ledger (all actual ledger entry modifications). Thetab_metadata is not committed - it's only used during transaction execution for queries. -
apply(PaymentSandbox& to): Merges bothitems_(ledger changes) andtab_(metadata) to parent PaymentSandbox. This allows nested sandboxes to propagate both actual changes and deferred credit metadata up the chain.
Sandboxes can be layered to create hierarchies of changes. For example:
RawView (actual ledger)
↑
Sandbox sb1 (transaction-level changes)
↑
PaymentSandbox psb (payment-level changes)
↑
PaymentSandbox nested (strand-level changes)
When apply() is called, all accumulated changes are pushed to the parent view by iterating over modified entries and applying each one. The parent can be another Sandbox (staged commit) or a RawView (final commit).
The atomicity guarantee is RAII-based: either apply() is called and all buffered changes propagate to the parent, or the sandbox is destroyed without calling apply() and all changes are discarded.
Conditional atomicity allows transactions to prepare multiple potential outcomes and commit only one based on the result. By creating two parallel sandboxes on the same parent view, the transaction can work on both a success path and a failure path simultaneously, then selectively apply only the appropriate one6.
// Create two parallel sandboxes on the same parent view
Sandbox sb(&ctx_.view()); // success path
Sandbox sbCancel(&ctx_.view()); // failure path (e.g., cleanup only)
auto const result = applyGuts(sb, sbCancel);
// Apply only the appropriate sandbox
if (result.second)
sb.apply(ctx_.rawView());
else
sbCancel.apply(ctx_.rawView());Footnotes
-
Preflight result check before preclaim:
applySteps.cpp↩ -
likelyToClaimFee flag calculation:
applySteps.h↩ -
doApply checks likelyToClaimFee flag:
applySteps.cpp↩ -
Balance hook description from source comments:
ReadView.h↩ -
Conditional atomicity pattern in CreateOffer:
CreateOffer.cpp↩