Transaction fees and sequence numbers are fundamental mechanisms that ensure the XRP Ledger operates securely and efficiently. Fees prevent spam and compensate the network for processing transactions, while sequence numbers prevent replay attacks and ensure transaction ordering.
Understanding how these mechanisms work is essential for implementing transactors correctly and for building applications that submit transactions reliably.
Transaction Fees
Every transaction on the XRP Ledger requires a fee, paid in XRP. This fee is destroyed (burned), permanently removing it from circulation.
Base Fee
The base fee is the minimum fee for a standard transaction:
static XRPAmountTransactor::calculateBaseFee(ReadView const& view, STTx const& tx){// Get the reference fee from the ledgerreturnview.fees().base;}
The current reference base fee is 10 drops (0.00001 XRP).
Fee Calculation
Different transaction types may have different fee multipliers:
Fee Escalation (Transaction Queue)
When the network is busy, the required fee increases:
Fee Checking in Preclaim
The base Transactor class checks fees during preclaim:
Fee Payment
Fees are paid at the start of doApply:
Account Reserves
Accounts must maintain a minimum XRP balance called the reserve. The reserve has two components:
Base Reserve: Fixed amount every account must hold (currently 1 XRP)
Owner Reserve: Additional amount per owned object (currently 0.2 XRP per object)
Reserve Calculation
Checking Reserves Before Creating Objects
Before creating a new object that will increase the owner count, verify the account can afford it:
Why mPriorBalance?
Using the balance before fee deduction allows accounts to use reserve XRP to pay transaction fees. This is important for:
Deleting objects when an account is low on funds
Sending the last XRP out of an account
Sequence Numbers
Each account has a sequence number that starts at 1 and increments with each transaction. This prevents:
Replay attacks: A transaction can only be applied once
Transaction ordering issues: Transactions are applied in sequence order
Sequence Number Checking
Sequence Number Consumption
After a successful transaction (or tec failure), the sequence number is incremented:
Tickets
Tickets provide an alternative to strict sequence ordering. A ticket is a pre-reserved sequence number that can be used later.
Creating Tickets
The TicketCreate transaction reserves a range of sequence numbers:
Using Tickets
Instead of Sequence, use TicketSequence:
Ticket Handling in Transactors
LastLedgerSequence
Transactions can specify a maximum ledger sequence for inclusion:
If the transaction is not included by this ledger, it becomes invalid:
Best Practice: Always set LastLedgerSequence to prevent transactions from being stuck indefinitely. A common value is current_ledger + 4.
mPriorBalance and mSourceBalance
The Transactor base class tracks two balance values:
These are set during the reset() method before doApply() runs:
Usage:
Use mPriorBalance for reserve checks (allows fee payment from reserves)
Use mSourceBalance for balance-dependent operations (actual available funds)
Fee-Related Result Codes
Code
Meaning
When Returned
temBAD_FEE
Fee is malformed
Negative fee
telINSUF_FEE_P
Fee too low
Below minimum for network load
terINSUF_FEE_B
Can't afford fee
Balance < fee
tecINSUFFICIENT_RESERVE
Reserve not met
Creating object without enough XRP
Sequence-Related Result Codes
Code
Meaning
When Returned
tefPAST_SEQ
Sequence already used
Transaction replayed or sequence too low
terPRE_SEQ
Sequence too high
Earlier transaction not yet applied
tefMAX_LEDGER
Transaction expired
LastLedgerSequence exceeded
tefNO_TICKET
Ticket not found
TicketSequence doesn't exist
Best Practices
Always set LastLedgerSequence: Prevent stuck transactions
Check reserves before creating objects: Use mPriorBalance
Handle sequence gaps: Use tickets for out-of-order transactions
Account for fee escalation: During high load, fees increase
Don't hardcode fees: Query the current fee level
Consider ticket usage: For systems that need flexible ordering
// Some transactions cost more than the base fee
// For example, multi-signed transactions cost more per signature
XRPAmount baseFee = calculateBaseFee(view, tx);
// Account for additional signatures
if (tx.isFieldPresent(sfSigners))
{
auto const& signers = tx.getFieldArray(sfSigners);
baseFee = baseFee * (1 + signers.size());
}
static XRPAmount
Transactor::minimumFee(
Application& app,
XRPAmount baseFee,
Fees const& fees,
ApplyFlags flags)
{
// During high load, fees escalate
if (flags & tapNO_ESCALATION)
return baseFee;
return app.getTxQ().minimumFee(baseFee);
}
static TER
Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
auto const feePaid = ctx.tx[sfFee].xrp();
// Fee must be non-negative
if (feePaid < beast::zero)
return temBAD_FEE;
// Fee must be sufficient
if (feePaid < baseFee)
return telINSUF_FEE_P;
// Account must have enough to pay fee
auto const sle = ctx.view.read(keylet::account(ctx.tx[sfAccount]));
auto const balance = (*sle)[sfBalance].xrp();
if (balance < feePaid)
return terINSUF_FEE_B;
return tesSUCCESS;
}
TER
Transactor::payFee()
{
auto const feePaid = ctx_.tx[sfFee].xrp();
// Get the account SLE
auto const sle = view().peek(keylet::account(account_));
// Deduct the fee
auto const balance = sle->getFieldAmount(sfBalance);
sle->setFieldAmount(sfBalance, balance - feePaid);
// The fee is destroyed (no destination)
return tesSUCCESS;
}
STAmount accountReserve = view().fees().accountReserve(ownerCount);
// This is equivalent to:
// baseReserve + (ownerCount * ownerReserve)
// e.g., 1 XRP + (5 objects * 0.2 XRP) = 2 XRP
TER CreateCheck::doApply()
{
auto const sle = view().peek(keylet::account(account_));
// Calculate reserve with one additional object
STAmount const reserve{
view().fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
// Use mPriorBalance (before fee deduction)
if (mPriorBalance < reserve)
return tecINSUFFICIENT_RESERVE;
// Continue with object creation...
}
static NotTEC
Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j)
{
auto const account = tx[sfAccount];
auto const sle = view.read(keylet::account(account));
if (!sle)
return terNO_ACCOUNT;
auto const txSeq = tx.getSequence();
auto const acctSeq = (*sle)[sfSequence];
if (txSeq != acctSeq)
{
if (txSeq < acctSeq)
return tefPAST_SEQ; // Already used
else
return terPRE_SEQ; // Too high, need earlier tx first
}
return tesSUCCESS;
}
TER
Transactor::consumeSeqProxy(SLE::pointer const& sleAccount)
{
auto const txSeq = ctx_.tx.getSequence();
auto const acctSeq = (*sleAccount)[sfSequence];
// Increment the account sequence
(*sleAccount)[sfSequence] = acctSeq + 1;
return tesSUCCESS;
}
// Use ticket #5 instead of the account sequence
{
"TransactionType": "Payment",
"Account": "rAccount...",
"TicketSequence": 5,
// No "Sequence" field
}
// Get the sequence value (works for both regular sequence and tickets)
std::uint32_t const seq = ctx_.tx.getSeqValue();
// For creating objects, use this value as the identifier
Keylet const checkKeylet = keylet::check(account_, seq);
{
"TransactionType": "Payment",
"Account": "rAccount...",
"Sequence": 42,
"LastLedgerSequence": 75000000 // Expire after this ledger
}