Transactors: Transaction Processing Framework
Introduction
Transactors are the heart of transaction processing in the XRP Ledger. Every transaction type—from simple XRP payments to complex escrow operations—is implemented as a Transactor, a C++ class that defines how that transaction is validated and executed. Understanding the transactor framework is essential for anyone who wants to contribute to the core protocol, implement custom transaction types, or debug transaction-related issues.
The transactor architecture ensures that all transactions undergo rigorous validation before modifying ledger state, maintaining the integrity and security that makes the XRP Ledger reliable for financial applications.
The Transactor Base Class
Architecture Overview
Every transaction type in Rippled inherits from the Transactor
base class, which provides the fundamental framework for transaction processing. This inheritance model ensures consistent behavior across all transaction types while allowing each type to implement its specific business logic.
The base Transactor
class is defined in src/ripple/app/tx/impl/Transactor.h
and provides:
Common validation logic - Signature verification, fee checks, sequence number validation
Helper methods - Account balance queries, ledger state access, fee calculation
Virtual methods - Hooks for transaction-specific logic (preflight, preclaim, doApply)
Transaction context - Access to the ledger, transaction data, and application state
Base Class Structure
class Transactor
{
public:
// Main entry point for transaction application
static std::pair<TER, bool>
apply(Application& app, OpenView& view, STTx const& tx, ApplyFlags flags);
// Virtual methods for transaction-specific logic
static NotTEC preflight(PreflightContext const& ctx);
static TER preclaim(PreclaimContext const& ctx);
virtual TER doApply() = 0;
protected:
// Constructor - available to derived classes
Transactor(ApplyContext& ctx);
// Helper methods
TER payFee();
TER checkSeq();
TER checkSign(PreclaimContext const& ctx);
// Member variables
ApplyContext& ctx_;
beast::Journal j_;
AccountID account_;
XRPAmount mPriorBalance;
XRPAmount mSourceBalance;
};
Key Concepts
ApplyContext: Provides access to the transaction being processed, the ledger view, and application services. This context object is passed through all stages of transaction processing.
Transaction Engine Result (TER): Every validation step returns a TER code indicating success (tesSUCCESS
), temporary failure (ter
codes), or permanent failure (tem
or tef
codes). These codes determine whether a transaction can be retried or should be permanently rejected.
Ledger Views: Transactors work with "views" of the ledger state, allowing tentative modifications that can be committed or rolled back. This ensures atomic transaction processing.
Three-Phase Validation Process
The transactor framework implements a rigorous three-phase validation process. Each phase has a specific purpose and access to different levels of information, creating a defense-in-depth approach to transaction validation.
Phase 1: Preflight
Purpose: Static validation that doesn't require ledger state
Access: Only the raw transaction data and protocol rules
When It Runs: Before any ledger state is accessed, can run in parallel
What It Checks:
Transaction format is valid
Required fields are present
Field values are within valid ranges
Amounts are positive and properly formatted
No malformed or contradictory data
Key Characteristic: Preflight checks are deterministic and stateless—they depend only on the transaction itself, not on current ledger state.
Preflight Example: Payment Transaction
NotTEC Payment::preflight(PreflightContext const& ctx)
{
// Check if the Payment transaction type is enabled
if (!ctx.rules.enabled(featurePayment))
return temDISABLED;
// Call base class preflight checks
auto const ret = preflight1(ctx);
if (!isTesSuccess(ret))
return ret;
// Verify destination account is specified
if (!ctx.tx.isFieldPresent(sfDestination))
return temDST_NEEDED;
// Verify amount is specified and valid
auto const amount = ctx.tx[sfAmount];
if (!amount)
return temBAD_AMOUNT;
// Amount must be positive
if (amount <= zero)
return temBAD_AMOUNT;
// Check for valid currency code if not XRP
if (!isXRP(amount))
{
if (!amount.issue().currency)
return temBAD_CURRENCY;
}
// Additional format validations...
return preflight2(ctx);
}
Why Preflight Matters: By catching format errors early, preflight prevents wasting resources on obviously invalid transactions. It also provides fast feedback to clients about transaction formatting issues.
Phase 2: Preclaim
Purpose: Validation requiring read-only access to ledger state
Access: Current ledger state (read-only), transaction data, protocol rules
When It Runs: After preflight passes, but before any state modifications
What It Checks:
Source account exists and has sufficient balance
Destination account exists (or can be created)
Required authorizations are in place
Trust lines exist for non-XRP currencies
Account flags and settings permit the transaction
Sequence numbers are correct
Key Characteristic: Preclaim can read ledger state but cannot modify it. This allows for safe concurrent execution and caching of preclaim results.
Preclaim Example: Payment Transaction
TER Payment::preclaim(PreclaimContext const& ctx)
{
// Get source and destination account IDs
AccountID const src = ctx.tx[sfAccount];
AccountID const dst = ctx.tx[sfDestination];
// Source and destination cannot be the same
if (src == dst)
return temREDUNDANT;
// Check if destination account exists
auto const dstID = ctx.tx[sfDestination];
auto const sleDst = ctx.view.read(keylet::account(dstID));
// If destination doesn't exist, check if we can create it
if (!sleDst)
{
auto const amount = ctx.tx[sfAmount];
// Only XRP can create accounts
if (!isXRP(amount))
return tecNO_DST;
// Amount must meet reserve requirement
if (amount < ctx.view.fees().accountReserve(0))
return tecNO_DST_INSUF_XRP;
}
else
{
// Destination exists - check if it requires dest tag
auto const flags = sleDst->getFlags();
if (flags & lsfRequireDestTag)
{
// Destination requires a tag but none provided
if (!ctx.tx.isFieldPresent(sfDestinationTag))
return tecDST_TAG_NEEDED;
}
// Check if destination has disallowed XRP
if (flags & lsfDisallowXRP && isXRP(ctx.tx[sfAmount]))
return tecNO_TARGET;
}
// Check source account balance
auto const sleSrc = ctx.view.read(keylet::account(src));
if (!sleSrc)
return terNO_ACCOUNT;
auto const balance = (*sleSrc)[sfBalance];
auto const amount = ctx.tx[sfAmount];
// Ensure sufficient balance (including fee)
if (balance < amount + ctx.tx[sfFee])
return tecUNFUNDED_PAYMENT;
return tesSUCCESS;
}
Why Preclaim Matters: Preclaim catches state-dependent errors before attempting state modifications. This prevents partially-applied transactions and provides clear error messages about why a transaction cannot succeed.
Phase 3: DoApply
Purpose: Actual ledger state modification
Access: Full read/write access to ledger state
When It Runs: After both preflight and preclaim succeed
What It Does:
Debits source account
Credits destination account
Creates or modifies ledger objects
Applies transaction-specific business logic
Records transaction metadata
Consumes transaction fee
Key Characteristic: DoApply modifies ledger state. All changes are atomic—either the entire transaction succeeds and all changes are applied, or it fails and no changes are made.
DoApply Example: Payment Transaction
TER Payment::doApply()
{
// Pay the transaction fee (happens for all transactions)
auto const result = payFee();
if (result != tesSUCCESS)
return result;
// Get amount to send
auto const amount = ctx_.tx[sfAmount];
auto const dst = ctx_.tx[sfDestination];
// Perform the actual transfer
auto const transferResult = accountSend(
view(), // Ledger view to modify
account_, // Source account
dst, // Destination account
amount, // Amount to transfer
j_ // Journal for logging
);
if (transferResult != tesSUCCESS)
return transferResult;
// Handle partial payments and path finding if applicable
if (ctx_.tx.isFlag(tfPartialPayment))
{
// Partial payment logic...
}
// Record transaction metadata
ctx_.deliver(amount);
return tesSUCCESS;
}
Why DoApply Matters: This is where the actual ledger state changes happen. DoApply ensures that only transactions that have passed all validation steps can modify the ledger, maintaining data integrity.
Transaction Types in Detail
The XRP Ledger supports numerous transaction types, each implemented as a specific transactor. Understanding the most common types helps you navigate the codebase and understand protocol capabilities.
Payment
File: src/ripple/app/tx/impl/Payment.cpp
Purpose: Transfer XRP or issued currencies between accounts
Key Features:
Direct XRP transfers
Issued currency transfers via trust lines
Path-based payments (automatic currency conversion)
Partial payments (deliver less than requested if full amount unavailable)
Common Fields:
Account
- Source accountDestination
- Recipient accountAmount
- Amount to deliverSendMax
(optional) - Maximum amount to sendPaths
(optional) - Payment paths for currency conversionDestinationTag
(optional) - Identifier for the recipient
Use Cases:
Simple XRP transfers
Issued currency payments
Cross-currency payments
Payment channel settlements
OfferCreate
File: src/ripple/app/tx/impl/CreateOffer.cpp
Purpose: Place an offer on the decentralized exchange (DEX)
Key Features:
Buy or sell any currency pair
Immediate-or-cancel orders
Fill-or-kill orders
Passive offers (don't consume existing offers)
Auto-bridging via XRP
Common Fields:
TakerPays
- Asset the taker (matcher) paysTakerGets
- Asset the taker receivesExpiration
(optional) - When offer expiresOfferSequence
(optional) - Sequence of offer to replace
Use Cases:
Currency exchange
Market making
Arbitrage
Limit orders
OfferCancel
File: src/ripple/app/tx/impl/CancelOffer.cpp
Purpose: Remove an offer from the order book
Key Features:
Cancel by offer sequence number
Only offer owner can cancel
Common Fields:
OfferSequence
- Sequence number of offer to cancel
TrustSet
File: src/ripple/app/tx/impl/SetTrust.cpp
Purpose: Create or modify a trust line for issued currencies
Key Features:
Set trust limit for a currency
Authorize/deauthorize trust lines
Configure trust line flags
Common Fields:
LimitAmount
- Trust line limit and currencyQualityIn
(optional) - Exchange rate for incoming transfersQualityOut
(optional) - Exchange rate for outgoing transfers
Use Cases:
Accept issued currencies
Set credit limits
Freeze trust lines
EscrowCreate
File: src/ripple/app/tx/impl/Escrow.cpp
Purpose: Lock XRP until conditions are met
Key Features:
Time-based release (CryptoConditions)
Conditional release (Interledger Protocol conditions)
Guaranteed delivery or return
Common Fields:
Destination
- Who can claim the escrowAmount
- Amount of XRP to escrowFinishAfter
(optional) - Earliest finish timeCancelAfter
(optional) - When escrow can be cancelledCondition
(optional) - Cryptographic condition for release
EscrowFinish
File: src/ripple/app/tx/impl/Escrow.cpp
Purpose: Complete an escrow and deliver XRP
Key Features:
Must meet time and/or condition requirements
Can be executed by anyone (typically destination)
Common Fields:
Owner
- Account that created the escrowOfferSequence
- Sequence of EscrowCreate transactionFulfillment
(optional) - Fulfillment of cryptographic condition
EscrowCancel
File: src/ripple/app/tx/impl/Escrow.cpp
Purpose: Return escrowed XRP to owner
Key Features:
Only after CancelAfter time passes
Can be executed by anyone
AccountSet
File: src/ripple/app/tx/impl/SetAccount.cpp
Purpose: Modify account settings and flags
Key Features:
Set account flags
Configure transfer rate
Set domain and message key
Configure email hash
Common Fields:
SetFlag
/ClearFlag
- Flags to modifyTransferRate
(optional) - Fee for transferring issued currenciesDomain
(optional) - Domain associated with accountMessageKey
(optional) - Public key for encrypted messaging
Important Flags:
asfRequireDest
- Require destination tagasfRequireAuth
- Require authorization for trust linesasfDisallowXRP
- Disallow XRP paymentsasfDefaultRipple
- Enable rippling by default
SignerListSet
File: src/ripple/app/tx/impl/SetSignerList.cpp
Purpose: Create or modify multi-signature configuration
Key Features:
Define list of authorized signers
Set signing quorum
Enable complex authorization schemes
Common Fields:
SignerQuorum
- Required signature weightSignerEntries
- List of authorized signers with weights
PaymentChannelCreate
File: src/ripple/app/tx/impl/PayChan.cpp
Purpose: Open a unidirectional payment channel
Key Features:
Lock XRP for fast, off-ledger payments
Asynchronous payments with cryptographic claims
Efficient micropayments
Creating Custom Transactors
When implementing new features through amendments, you'll often need to create custom transactors. Here's the complete process:
Step 1: Define Transaction Format
Add your transaction type to src/ripple/protocol/TxFormats.cpp
:
add(jss::MyCustomTx,
ttMY_CUSTOM_TX,
{
// Required fields
{sfAccount, soeREQUIRED},
{sfDestination, soeREQUIRED},
{sfCustomField, soeREQUIRED},
// Optional fields
{sfOptionalField, soeOPTIONAL},
},
commonFields);
Step 2: Create Transactor Class
Create src/ripple/app/tx/impl/MyCustomTx.h
:
#ifndef RIPPLE_TX_MYCUSTOMTX_H_INCLUDED
#define RIPPLE_TX_MYCUSTOMTX_H_INCLUDED
#include <ripple/app/tx/impl/Transactor.h>
namespace ripple {
class MyCustomTx : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit MyCustomTx(ApplyContext& ctx) : Transactor(ctx) {}
static NotTEC preflight(PreflightContext const& ctx);
static TER preclaim(PreclaimContext const& ctx);
TER doApply() override;
};
} // namespace ripple
#endif
Step 3: Implement Preflight
Create src/ripple/app/tx/impl/MyCustomTx.cpp
:
#include <ripple/app/tx/impl/MyCustomTx.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/Feature.h>
namespace ripple {
NotTEC MyCustomTx::preflight(PreflightContext const& ctx)
{
// Check if amendment is enabled
if (!ctx.rules.enabled(featureMyCustomTx))
return temDISABLED;
// Perform base class preflight checks
auto const ret = preflight1(ctx);
if (!isTesSuccess(ret))
return ret;
// Validate custom field format
if (!ctx.tx.isFieldPresent(sfCustomField))
return temMALFORMED;
auto const customValue = ctx.tx[sfCustomField];
if (customValue < 0 || customValue > 1000000)
return temBAD_AMOUNT;
// Additional validation...
return preflight2(ctx);
}
Step 4: Implement Preclaim
TER MyCustomTx::preclaim(PreclaimContext const& ctx)
{
// Get account IDs
AccountID const src = ctx.tx[sfAccount];
AccountID const dst = ctx.tx[sfDestination];
// Verify destination account exists
auto const sleDst = ctx.view.read(keylet::account(dst));
if (!sleDst)
return tecNO_DST;
// Check source account has sufficient balance
auto const sleSrc = ctx.view.read(keylet::account(src));
if (!sleSrc)
return terNO_ACCOUNT;
auto const balance = (*sleSrc)[sfBalance];
auto const fee = ctx.tx[sfFee];
if (balance < fee)
return tecUNFUNDED;
// Additional state-based validation...
return tesSUCCESS;
}
Step 5: Implement DoApply
TER MyCustomTx::doApply()
{
// Pay transaction fee
auto const result = payFee();
if (result != tesSUCCESS)
return result;
// Get transaction fields
auto const dst = ctx_.tx[sfDestination];
auto const customValue = ctx_.tx[sfCustomField];
// Perform custom logic
// Example: Create a new ledger object
auto const sleNew = std::make_shared<SLE>(
keylet::custom(account_, ctx_.tx.getSeqProxy().value()));
sleNew->setAccountID(sfAccount, account_);
sleNew->setAccountID(sfDestination, dst);
sleNew->setFieldU32(sfCustomField, customValue);
// Insert into ledger
view().insert(sleNew);
// Log the operation
JLOG(j_.trace()) << "MyCustomTx applied successfully";
return tesSUCCESS;
}
} // namespace ripple
Step 6: Register the Transactor
Add to src/ripple/app/tx/applySteps.cpp
:
#include <ripple/app/tx/impl/MyCustomTx.h>
// In the invoke function, add:
case ttMY_CUSTOM_TX:
return MyCustomTx::makeTxConsequences(ctx);
Step 7: Write Tests
Create src/test/app/MyCustomTx_test.cpp
:
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
namespace ripple {
namespace test {
class MyCustomTx_test : public beast::unit_test::suite
{
public:
void testBasicOperation()
{
using namespace jtx;
Env env(*this, supported_amendments() | featureMyCustomTx);
// Create test accounts
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
// Submit custom transaction
Json::Value jv;
jv[jss::Account] = alice.human();
jv[jss::Destination] = bob.human();
jv[jss::TransactionType] = jss::MyCustomTx;
jv[jss::CustomField] = 12345;
jv[jss::Fee] = "10";
env(jv);
env.close();
// Verify results
// Add assertions...
}
void run() override
{
testBasicOperation();
// More tests...
}
};
BEAST_DEFINE_TESTSUITE(MyCustomTx, app, ripple);
} // namespace test
} // namespace ripple
Transaction Lifecycle Within Framework
Understanding how a transaction flows through the transactor framework helps debug issues and optimize performance.
Complete Flow Diagram
Transaction Submission
↓
Preflight (Static Validation)
↓
✓ Pass → Continue
✗ Fail → Reject (return tem code)
↓
Preclaim (State Validation)
↓
✓ Pass → Continue
✗ Fail → Reject (return tec/ter code)
↓
Enter Consensus
↓
Reach Agreement
↓
DoApply (State Modification)
↓
✓ Success → Commit changes
✗ Fail → Rollback (still consumes fee)
↓
Transaction Finalized in Ledger
Error Code Categories
tem (Malformed): Transaction is permanently invalid due to format issues
Example:
temMALFORMED
,temBAD_AMOUNT
,temDISABLED
Action: Reject immediately, never retry
tef (Failure): Transaction failed during local checks
Example:
tefFAILURE
,tefPAST_SEQ
Action: Reject, may indicate client error
ter (Retry): Transaction failed but might succeed later
Example:
terQUEUED
,terPRE_SEQ
Action: Can be retried after conditions change
tec (Claimed Fee): Transaction failed but consumed fee
Example:
tecUNFUNDED
,tecNO_DST
,tecNO_PERMISSION
Action: Failed permanently, fee charged
tes (Success): Transaction succeeded
Example:
tesSUCCESS
Action: Changes committed to ledger
Hands-On Exercise
Exercise: Trace and Modify a Payment Transaction
Objective: Understand the payment transactor implementation through debugging and modification.
Part 1: Code Exploration
Step 1: Navigate to the Payment transactor
cd rippled/src/ripple/app/tx/impl/
open Payment.cpp # or use your IDE
Step 2: Identify the three phases
Find and read:
Payment::preflight()
- Lines implementing static checksPayment::preclaim()
- Lines checking ledger statePayment::doApply()
- Lines modifying state
Step 3: Trace a specific check
Follow how the Payment transactor checks if a destination requires a destination tag:
// In preclaim:
if (sleDst->getFlags() & lsfRequireDestTag)
{
if (!ctx.tx.isFieldPresent(sfDestinationTag))
return tecDST_TAG_NEEDED;
}
Questions:
Where is
lsfRequireDestTag
defined?How is this flag set on an account?
What transaction type sets this flag?
Part 2: Debug a Payment
Step 1: Set up standalone mode with logging
rippled --conf=rippled.cfg --standalone
Enable transaction logging:
rippled log_level Transaction trace
Step 2: Create test accounts
# Create and fund two accounts
rippled account_info <address1>
rippled account_info <address2>
Step 3: Set destination tag requirement
# Set requireDestTag flag on destination account
rippled submit '{
"TransactionType": "AccountSet",
"Account": "<address2>",
"SetFlag": 1,
"Fee": "12"
}'
Step 4: Try payment without destination tag
# This should fail with tecDST_TAG_NEEDED
rippled submit '{
"TransactionType": "Payment",
"Account": "<address1>",
"Destination": "<address2>",
"Amount": "1000000",
"Fee": "12"
}'
Step 5: Try payment with destination tag
# This should succeed
rippled submit '{
"TransactionType": "Payment",
"Account": "<address1>",
"Destination": "<address2>",
"Amount": "1000000",
"DestinationTag": 12345,
"Fee": "12"
}'
Part 3: Modify the Transactor (Advanced)
Step 1: Add custom logging
Edit Payment.cpp
and add logging to doApply()
:
TER Payment::doApply()
{
JLOG(j_.info()) << "Payment doApply started";
JLOG(j_.info()) << "Source: " << account_;
JLOG(j_.info()) << "Destination: " << ctx_.tx[sfDestination];
JLOG(j_.info()) << "Amount: " << ctx_.tx[sfAmount];
// ... existing code ...
}
Step 2: Recompile rippled
cd rippled/build
cmake --build . --target rippled
Step 3: Run with your modified code
./rippled --conf=rippled.cfg --standalone
Step 4: Submit a payment and observe your logs
Analysis Questions
Answer these based on your exploration:
What happens in each validation phase?
List the checks performed in preflight
List the checks performed in preclaim
What state modifications occur in doApply?
How are transaction fees handled?
Where is
payFee()
called?What happens if an account can't pay the fee?
How does the code handle XRP vs issued currencies?
Find the code that distinguishes between them
How do payment paths work for issued currencies?
What's the role of the
accountSend()
helper?Where is it implemented?
What does it do internally?
Key Takeaways
Core Concepts
✅ Three-Phase Validation: Preflight (static), Preclaim (read state), DoApply (modify state) ensures robust transaction processing
✅ Inheritance Architecture: All transaction types inherit from Transactor base class, ensuring consistent behavior
✅ Error Code System: tem/tef/ter/tec/tes codes provide clear feedback about transaction status
✅ Atomic Execution: Transactions either fully succeed or fully fail (except fee consumption)
✅ State Views: Ledger modifications happen in views that can be committed or rolled back
Development Skills
✅ Codebase Location: Transaction implementations in src/ripple/app/tx/impl/
✅ Creating Custom Transactions: Follow the pattern of defining format, implementing phases, registering transactor
✅ Debugging: Use standalone mode and logging to trace transaction execution
✅ Testing: Write comprehensive unit tests for all transaction scenarios
✅ Amendment Integration: New transaction types typically require amendments for activation
Common Patterns and Best Practices
Pattern 1: Check-Then-Act
Always validate before modifying state:
// Bad - might partially modify state before failing
auto const result1 = modifyState1();
auto const result2 = modifyState2(); // If this fails, state1 is modified
if (result2 != tesSUCCESS)
return result2;
// Good - validate first, then modify
if (!canModifyState1())
return tecFAILURE;
if (!canModifyState2())
return tecFAILURE;
modifyState1();
modifyState2();
Pattern 2: Use Helper Functions
The Transactor base class provides many helpers:
// Check sequence number
auto const result = checkSeq();
if (result != tesSUCCESS)
return result;
// Pay fee
auto const feeResult = payFee();
if (feeResult != tesSUCCESS)
return feeResult;
// Check authorization
if (!hasAuthority())
return tecNO_PERMISSION;
Pattern 3: Ledger Object Patterns
Creating, modifying, and deleting ledger objects:
// Read existing object
auto const sle = view().read(keylet::account(accountID));
if (!sle)
return tecNO_TARGET;
// Modify object
auto sleMutable = view().peek(keylet::account(accountID));
(*sleMutable)[sfBalance] = newBalance;
view().update(sleMutable);
// Create new object
auto const sleNew = std::make_shared<SLE>(keylet);
sleNew->setFieldU32(sfFlags, 0);
view().insert(sleNew);
// Delete object
view().erase(sle);
Additional Resources
Official Documentation
XRP Ledger Dev Portal: xrpl.org/docs
Transaction Types: xrpl.org/transaction-types
Create Custom Transactors: xrpl.org/create-custom-transactors
Codebase References
src/ripple/app/tx/impl/
- All transactor implementationssrc/ripple/app/tx/impl/Transactor.h
- Base transactor classsrc/ripple/protocol/TxFormats.cpp
- Transaction format definitionssrc/ripple/protocol/TER.h
- Transaction result codes
Related Topics
Protocols - How transactions are propagated across the network
Transaction Lifecycle - Complete journey from submission to ledger
Application Layer - How transactors integrate with the overall system
Next Steps
Now that you understand how transactions are processed through transactors, explore how the Application layer orchestrates all system components.
➡️ Continue to: Application Layer - Central Orchestration
⬅️ Back to: Rippled II Overview
Get Started
Access the course: docs.xrpl-commons.org/core-dev-bootcamp
Got questions? Contact us here: Submit Feedback
© 2025 XRPL Commons - Core Dev Bootcamp
Last updated