Files
rippled/src/libxrpl/tx/Transactor.cpp
Pratik Mankawde 2683a30765 perf(telemetry): only build apply-span attributes when the span is recorded
Transactor::operator()() set its apply-stage attributes unconditionally, so
every transaction applied paid for a TxFormats::findByType() lookup and a
64-char string built from the view's parent hash, plus one or two transToken()
lookups in the exit funnel, whether or not anything recorded them.

Guard both blocks on the span being active. This is the pattern the sibling
preflight and preclaim spans already use in applySteps.cpp, with a comment
giving this exact reason -- the apply stage was simply missed.

Behaviour is unchanged where the span is live, and setAttribute on an inactive
guard was already a no-op.
2026-08-26 19:09:47 +01:00

1744 lines
58 KiB
C++

#include <xrpl/tx/Transactor.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/to_string.h> // IWYU pragma: keep
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h> // IWYU pragma: keep
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/SignerEntries.h>
#include <xrpl/tx/apply.h>
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/detail/TxApplySpanNames.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <stdexcept>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl {
/**
* Performs early sanity checks on the txid
*/
NotTEC
preflight0(PreflightContext const& ctx, std::uint32_t flagMask)
{
if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn))
{
JLOG(ctx.j.warn()) << "Pseudo transactions cannot contain the "
"tfInnerBatchTxn flag.";
return temINVALID_FLAG;
}
if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID))
{
uint32_t const nodeNID = ctx.registry.get().getNetworkIDService().getNetworkID();
std::optional<uint32_t> txNID = ctx.tx[~sfNetworkID];
if (nodeNID <= 1024)
{
// legacy networks have ids less than 1024, these networks cannot
// specify NetworkID in txn
if (txNID)
return telNETWORK_ID_MAKES_TX_NON_CANONICAL;
}
else
{
// new networks both require the field to be present and require it
// to match
if (!txNID)
return telREQUIRES_NETWORK_ID;
if (*txNID != nodeNID)
return telWRONG_NETWORK;
}
}
auto const txID = ctx.tx.getTransactionID();
if (txID == beast::kZero)
{
JLOG(ctx.j.warn()) << "applyTransaction: transaction id may not be zero";
return temINVALID;
}
if ((ctx.tx.getFlags() & flagMask) != 0u)
{
JLOG(ctx.j.debug()) << ctx.tx.peekAtField(sfTransactionType).getFullText()
<< ": invalid flags.";
return temINVALID_FLAG;
}
return tesSUCCESS;
}
namespace detail {
/**
* Checks the validity of the transactor signing key.
*
* Normally called from preflight1.
*/
NotTEC
preflightCheckSigningKey(STObject const& sigObject, beast::Journal j)
{
if (auto const spk = sigObject.getFieldVL(sfSigningPubKey);
!spk.empty() && !publicKeyType(makeSlice(spk)))
{
JLOG(j.debug()) << "preflightCheckSigningKey: invalid signing key";
return temBAD_SIGNATURE;
}
return tesSUCCESS;
}
std::optional<NotTEC>
preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::Journal j)
{
if ((flags & TapDryRun) != 0u) // simulation
{
std::optional<Slice> const signature = sigObject[~sfTxnSignature];
if (signature && !signature->empty())
{
// NOTE: This code should never be hit because it's checked in the
// `simulate` RPC
return temINVALID; // LCOV_EXCL_LINE
}
if (!sigObject.isFieldPresent(sfSigners))
{
// no signers, no signature - a valid simulation
return tesSUCCESS;
}
for (auto const& signer : sigObject.getFieldArray(sfSigners))
{
if (signer.isFieldPresent(sfTxnSignature) && !signer[sfTxnSignature].empty())
{
// NOTE: This code should never be hit because it's
// checked in the `simulate` RPC
return temINVALID; // LCOV_EXCL_LINE
}
}
Slice const signingPubKey = sigObject[sfSigningPubKey];
if (!signingPubKey.empty())
{
// trying to single-sign _and_ multi-sign a transaction
return temINVALID;
}
return tesSUCCESS;
}
return {};
}
} // namespace detail
static NotTEC
preflight1Sponsor(PreflightContext const& ctx)
{
bool const hasSponsor = ctx.tx.isFieldPresent(sfSponsor);
bool const hasSponsorFlags = ctx.tx.isFieldPresent(sfSponsorFlags);
bool const hasSponsorSig = ctx.tx.isFieldPresent(sfSponsorSignature);
if ((hasSponsor || hasSponsorFlags || hasSponsorSig) && !ctx.rules.enabled(featureSponsor))
return temDISABLED;
if (hasSponsor != hasSponsorFlags)
{
JLOG(ctx.j.debug()) << "preflight1: sponsor and sponsor flags mismatch";
return temINVALID_FLAG;
}
if (hasSponsorSig && (!hasSponsor || !hasSponsorFlags))
{
JLOG(ctx.j.debug()) << "preflight1: sponsor signature without sponsor definition";
return temMALFORMED;
}
if (hasSponsorFlags)
{
auto const sponsorFlags = ctx.tx.getFieldU32(sfSponsorFlags);
if (((sponsorFlags & spfSponsorFlagMask) != 0u) || sponsorFlags == 0)
{
JLOG(ctx.j.debug()) << "preflight1: invalid sponsor flags";
return temINVALID_FLAG;
}
// Reserve sponsorship is only permitted for an explicit allow-list of
// transaction types, for v1. All other tx types reject spfSponsorReserve here.
if (isReserveSponsored(ctx.tx))
{
if (!isReserveSponsorAllowed(ctx.tx.getTxnType()))
{
JLOG(ctx.j.debug())
<< "preflight1: spfSponsorReserve not allowed for this transaction type";
return temINVALID_FLAG;
}
}
}
if (hasSponsor && ctx.tx.getAccountID(sfSponsor) == ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.debug()) << "preflight1: Sponsor account cannot be the same as the account";
return temMALFORMED;
}
return tesSUCCESS;
}
/**
* Performs early sanity checks on the account and fee fields
*/
NotTEC
Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
{
if (ctx.tx.isFieldPresent(sfDelegate))
{
if (!ctx.rules.enabled(featurePermissionDelegationV1_1))
return temDISABLED;
if (ctx.tx[sfDelegate] == ctx.tx[sfAccount])
return temBAD_SIGNER;
auto const& perm = Permission::getInstance();
auto const txType = ctx.tx.getTxnType();
// If the transaction is not delegable and does not have granular permissions, fail earlier
// with temINVALID. This is to prevent transactions that are not delegable at all from
// being processed further in the invokeCheckPermission function.
if (!perm.isDelegable(Permission::txToPermissionType(txType), ctx.rules) &&
!perm.hasGranularPermissions(txType))
return temINVALID;
}
if (auto const ret = preflight0(ctx, flagMask))
return ret;
auto const id = ctx.tx.getAccountID(sfAccount);
if (id == beast::kZero)
{
JLOG(ctx.j.warn()) << "preflight1: bad account id";
return temBAD_SRC_ACCOUNT;
}
// No point in going any further if the transaction fee is malformed.
auto const fee = ctx.tx.getFieldAmount(sfFee);
if (!fee.native() || fee.negative() || !isLegalAmount(fee.xrp()))
{
JLOG(ctx.j.debug()) << "preflight1: invalid fee";
return temBAD_FEE;
}
if (auto const ret = detail::preflightCheckSigningKey(ctx.tx, ctx.j))
return ret;
// An AccountTxnID field constrains transaction ordering more than the
// Sequence field. Tickets, on the other hand, reduce ordering
// constraints. Because Tickets and AccountTxnID work against one
// another the combination is unsupported and treated as malformed.
//
// We return temINVALID for such transactions.
if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
return temINVALID;
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG;
// Reject if the inner batch flag and parentBatchId are inconsistent.
// A standalone tx with tfInnerBatchTxn but no parentBatchId is an
// attack attempt. A tx with parentBatchId but without tfInnerBatchTxn
// is a programming error.
if (ctx.tx.isFlag(tfInnerBatchTxn) != ctx.parentBatchId.has_value())
return temINVALID_INNER_BATCH;
if (auto const ter = preflight1Sponsor(ctx); !isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
/**
* Checks whether the signature appears valid
*/
NotTEC
Transactor::preflight2(PreflightContext const& ctx)
{
if (auto const ret = detail::preflightCheckSimulateKeys(ctx.flags, ctx.tx, ctx.j))
{
// Skips following checks if the transaction is being simulated,
// regardless of success or failure
return *ret;
}
// Skip the signature check on batch inner transactions. preflight1 already
// enforces both conditions; re-checking them as defense in depth guarantees
// we never return success (and so skip signature validation) for an inner
// transaction unless the amendment is enabled and it really sits inside a
// batch.
if (ctx.tx.isFlag(tfInnerBatchTxn))
{
if (!ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG;
if (!ctx.parentBatchId.has_value())
return temINVALID_INNER_BATCH;
return tesSUCCESS;
}
// Do not add any checks after this point that are relevant for
// batch inner transactions. They will be skipped.
auto const sigValid = checkValidity(ctx.registry.get().getHashRouter(), ctx.tx, ctx.rules);
if (sigValid.first == Validity::SigBad)
{ // LCOV_EXCL_START
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
return temINVALID;
// LCOV_EXCL_STOP
}
// Do not add any checks after this point that are relevant for
// batch inner transactions. They will be skipped.
return tesSUCCESS;
}
NotTEC
Transactor::preflightUniversal(PreflightContext const& ctx)
{
if (ctx.rules.enabled(fixCleanup3_2_0) && hasInvalidAmount(ctx.tx, ctx.j))
return temBAD_AMOUNT;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
Transactor::Transactor(ApplyContext& ctx)
: ctx_(ctx)
, sink_(ctx.journal, toShortString(ctx.tx.getTransactionID()) + " ")
, j_(sink_)
, accountID_(ctx.tx.getAccountID(sfAccount))
{
}
bool
Transactor::validDataLength(std::optional<Slice> const& slice, std::size_t maxLength)
{
if (!slice)
return true;
return !slice->empty() && slice->length() <= maxLength;
}
std::uint32_t
Transactor::getFlagsMask(PreflightContext const& ctx)
{
return tfUniversalMask;
}
NotTEC
Transactor::preflightSigValidated(PreflightContext const& ctx)
{
return tesSUCCESS;
}
NotTEC
Transactor::checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate));
if (!sle)
return terNO_DELEGATE_PERMISSION;
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
if (!Permission::getInstance().hasGranularPermissions(tx.getTxnType()))
return terNO_DELEGATE_PERMISSION;
heldGranularPermissions = getGranularPermission(sle, tx.getTxnType());
if (heldGranularPermissions.empty())
return terNO_DELEGATE_PERMISSION;
if (!Permission::getInstance().checkGranularSandbox(tx, heldGranularPermissions))
return terNO_DELEGATE_PERMISSION;
return tesSUCCESS;
}
NotTEC
Transactor::checkSponsor(ReadView const& view, STTx const& tx)
{
if (!tx.isFieldPresent(sfSponsor))
return tesSUCCESS;
// Reserve sponsorship with permissioned delegation is disallowed.
if (tx.isFieldPresent(sfDelegate) && isReserveSponsored(tx))
return temINVALID;
if (!view.exists(keylet::account(tx.getAccountID(sfSponsor))))
return terNO_ACCOUNT;
// Skip Sponsorship existence checks if the sponsor has signed the transaction - this
// transaction is valid regardless of the Sponsorship object.
// The use of the Sponsorship object is properly handled in
// getFeePayer/checkReserve/increaseOwnerCount/decreaseOwnerCount.
if (tx.isFieldPresent(sfSponsorSignature))
return tesSUCCESS;
// If the transaction contains sfDelegate, the Sponsorship object should be
// between the sponsor and the delegate.
auto const sponsorshipSle =
view.read(keylet::sponsorship(tx.getAccountID(sfSponsor), tx.getInitiator()));
// sponsorship object missing for pre-funded (no co-signing) tx
if (!sponsorshipSle)
return terNO_PERMISSION;
if (isFeeSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForFee))
return terNO_PERMISSION;
if (isReserveSponsored(tx) && sponsorshipSle->isFlag(lsfSponsorshipRequireSignForReserve))
return terNO_PERMISSION;
return tesSUCCESS;
}
XRPAmount
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// Returns the fee in fee units.
// The computation has two parts:
// * The base fee, which is the same for most transactions.
// * The additional cost of each multisignature on the transaction.
// * The additional cost of each multisignature on the sponsor.
XRPAmount const baseFee = view.fees().base;
// Each signer adds one more baseFee to the minimum required fee
// for the transaction.
std::size_t const signerCount =
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0;
std::size_t sponsorSignerCount = 0;
if (tx.isFieldPresent(sfSponsorSignature))
{
auto const sponsorObj = tx.getFieldObject(sfSponsorSignature);
if (sponsorObj.isFieldPresent(sfSigners))
sponsorSignerCount += sponsorObj.getFieldArray(sfSigners).size();
}
return baseFee + ((signerCount + sponsorSignerCount) * baseFee);
}
XRPAmount
Transactor::calculateBaseFee(
ReadView const& view,
STTx const& tx,
std::uint32_t extraBaseFeeMultiplier)
{
return calculateBaseFee(view, tx) + view.fees().base * extraBaseFeeMultiplier;
}
// Returns the fee in fee units, not scaled for load.
XRPAmount
Transactor::calculateOwnerReserveFee(ReadView const& view, STTx const& tx)
{
// Assumption: One reserve increment is typically much greater than one base
// fee.
// This check is in an assert so that it will come to the attention of
// developers if that assumption is not correct. If the owner reserve is not
// significantly larger than the base fee (or even worse, smaller), we will
// need to rethink charging an owner reserve as a transaction fee.
// TODO: This function is static, and I don't want to add more parameters.
// When it is finally refactored to be in a context that has access to the
// Application, include "app().getOverlay().networkID() > 2 ||" in the
// condition.
XRPL_ASSERT(
view.fees().increment > view.fees().base * 100,
"xrpl::Transactor::calculateOwnerReserveFee : Owner reserve is "
"reasonable");
return view.fees().increment;
}
XRPAmount
Transactor::minimumFee(
ServiceRegistry& registry,
XRPAmount baseFee,
Fees const& fees,
ApplyFlags flags)
{
return scaleFeeLoad(baseFee, registry.getFeeTrack(), fees, (flags & TapUnlimited) != 0u);
}
TER
Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
if (!ctx.tx[sfFee].native())
return temBAD_FEE;
auto const feePaid = ctx.tx[sfFee].xrp();
if ((ctx.flags & TapBatch) != 0u)
{
if (feePaid == beast::kZero)
return tesSUCCESS;
JLOG(ctx.j.trace()) << "Batch: Fee must be zero.";
return temBAD_FEE; // LCOV_EXCL_LINE
}
if (!isLegalAmount(feePaid) || feePaid < beast::kZero)
return temBAD_FEE;
// Only check fee is sufficient when the ledger is open.
if (ctx.view.open())
{
auto const feeDue = minimumFee(ctx.registry, baseFee, ctx.view.fees(), ctx.flags);
if (feePaid < feeDue)
{
JLOG(ctx.j.trace()) << "Insufficient fee paid: " << to_string(feePaid) << "/"
<< to_string(feeDue);
return telINSUF_FEE_P;
}
}
if (feePaid == beast::kZero)
return tesSUCCESS;
auto const feePayer = getFeePayer(ctx.view, ctx.tx);
auto const payerSle = ctx.view.read(feePayer.keylet);
if (!payerSle)
{
if (feePayer.type == FeePayerType::SponsorPreFunded)
{
// Sanity check: already checked in checkSponsor
return tefINTERNAL; // LCOV_EXCL_LINE
}
return terNO_ACCOUNT;
}
XRPAmount maxSpendable = beast::kZero;
if (feePayer.type == FeePayerType::SponsorPreFunded)
{
if (payerSle->getType() != ltSPONSORSHIP)
return tefINTERNAL; // LCOV_EXCL_LINE
if (payerSle->isFieldPresent(feePayer.balanceField))
maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp();
if (payerSle->isFieldPresent(sfMaxFee))
{
auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp();
maxSpendable = std::min(maxSpendable, cap);
}
}
else
{
if (payerSle->getType() != ltACCOUNT_ROOT)
return tefINTERNAL; // LCOV_EXCL_LINE
if (feePayer.type == FeePayerType::SponsorCoSigned)
{
auto const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j);
maxSpendable = payerSle->getFieldAmount(sfBalance).xrp() - sponsorReserve;
}
else
{
maxSpendable = payerSle->getFieldAmount(feePayer.balanceField).xrp();
}
}
// NOTE: Because preclaim evaluates against a static readview, it
// does not reflect fee deductions from other transactions paid by
// the same account within the current ledger.
// As a result, if an account's balance is over-committed across multiple
// transactions, this check may pass optimistically.
// The fee shortfall will be handled by the Transactor::reset mechanism,
// which caps the fee to the remaining actual balance.
if (maxSpendable < feePaid)
{
JLOG(ctx.j.trace()) << "Insufficient balance:" << " balance=" << to_string(maxSpendable)
<< " paid=" << to_string(feePaid);
if ((maxSpendable > beast::kZero) && !ctx.view.open())
{
// Closed ledger, non-zero balance, less than fee
return tecINSUFF_FEE;
}
return terINSUF_FEE_B;
}
return tesSUCCESS;
}
TER
Transactor::payFee()
{
auto const feePaid = ctx_.tx[sfFee].xrp();
auto const feePayer = getFeePayer(view(), ctx_.tx);
auto const sle = view().peek(feePayer.keylet);
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id);
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (feePaid == beast::kZero)
return tesSUCCESS;
XRPAmount balance = beast::kZero;
if (sle->isFieldPresent(feePayer.balanceField))
{
balance = sle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return tefINTERNAL; // LCOV_EXCL_LINE
}
// A co-signed sponsor pays the fee out of its own account balance, but must
// never be charged into its account reserve, and a pre-funded sponsorship's
// fee is capped by sfMaxFee. Mirror the spendable amount computed in
// checkFee() so both limits are enforced on the apply path too.
XRPAmount spendable = balance;
if (feePayer.type == FeePayerType::SponsorCoSigned)
{
auto const sponsorReserve = accountReserve(view(), sle, j_);
// max(balance - reserve, 0) with overflow handling
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
}
else if (feePayer.type == FeePayerType::SponsorPreFunded && sle->isFieldPresent(sfMaxFee))
{
auto const cap = sle->getFieldAmount(sfMaxFee).xrp();
spendable = std::min(spendable, cap);
}
// Only sponsor fee-payers reject here on insufficient funds. For an
// ordinary account, the fee falls through and is capped by reset(), which
// caps to the account's balance. That capping is wrong for sponsors: a
// co-signed sponsor would be charged into its own reserve, and a prefunded
// sponsorship's fee amount should be rejected rather than partially spent.
if (feePaid > spendable &&
(feePayer.type == FeePayerType::SponsorPreFunded ||
feePayer.type == FeePayerType::SponsorCoSigned))
{
if ((spendable > beast::kZero) && !view().open())
return tecINSUFF_FEE;
return terINSUF_FEE_B;
}
auto const feeAmountAfter = balance - feePaid;
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
{
// Because ltSponsorship.sfFeeAmount is soeOptional
sle->makeFieldAbsent(feePayer.balanceField);
}
else
{
sle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
}
view().update(sle);
// VFALCO Should we call view().rawDestroyXRP() here as well?
return tesSUCCESS;
}
NotTEC
Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j)
{
auto const id = tx.getAccountID(sfAccount);
auto const sle = view.read(keylet::account(id));
if (!sle)
{
JLOG(j.trace()) << "applyTransaction: delay: source account does not exist "
<< toBase58(id);
return terNO_ACCOUNT;
}
SeqProxy const tSeqProx = tx.getSeqProxy();
SeqProxy const aSeq = SeqProxy::rawSequence((*sle)[sfSequence]);
if (tSeqProx.isSeq())
{
if (tx.isFieldPresent(sfTicketSequence))
{
JLOG(j.trace()) << "applyTransaction: has both a TicketSequence "
"and a non-zero Sequence number";
return temSEQ_AND_TICKET;
}
if (tSeqProx != aSeq)
{
if (aSeq < tSeqProx)
{
JLOG(j.trace()) << "applyTransaction: has future sequence number "
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
return terPRE_SEQ;
}
// It's an already-used sequence number.
JLOG(j.trace()) << "applyTransaction: has past sequence number "
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
return tefPAST_SEQ;
}
}
else if (tSeqProx.isTicket())
{
// Bypass the type comparison. Apples and oranges.
if (aSeq.value() <= tSeqProx.value())
{
// If the Ticket number is greater than or equal to the
// account sequence there's the possibility that the
// transaction to create the Ticket has not hit the ledger
// yet. Allow a retry.
JLOG(j.trace()) << "applyTransaction: has future ticket id "
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
return terPRE_TICKET;
}
// Transaction can never succeed if the Ticket is not in the ledger.
if (!view.exists(keylet::ticket(id, tSeqProx)))
{
JLOG(j.trace()) << "applyTransaction: ticket already used or never created "
<< "a_seq=" << aSeq << " t_seq=" << tSeqProx;
return tefNO_TICKET;
}
}
return tesSUCCESS;
}
NotTEC
Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx)
{
auto const id = ctx.tx.getAccountID(sfAccount);
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
{
JLOG(ctx.j.trace()) << "applyTransaction: delay: source account does not exist "
<< toBase58(id);
return terNO_ACCOUNT;
}
if (ctx.tx.isFieldPresent(sfAccountTxnID) &&
(sle->getFieldH256(sfAccountTxnID) != ctx.tx.getFieldH256(sfAccountTxnID)))
return tefWRONG_PRIOR;
if (ctx.tx.isFieldPresent(sfLastLedgerSequence) &&
(ctx.view.seq() > ctx.tx.getFieldU32(sfLastLedgerSequence)))
return tefMAX_LEDGER;
if (ctx.view.txExists(ctx.tx.getTransactionID()))
return tefALREADY;
return tesSUCCESS;
}
TER
Transactor::consumeSeqProxy(SLE::pointer const& sleAccount)
{
XRPL_ASSERT(sleAccount, "xrpl::Transactor::consumeSeqProxy : non-null account");
SeqProxy const seqProxy = ctx_.tx.getSeqProxy();
if (seqProxy.isSeq())
{
// Note that if this transaction is a TicketCreate, then
// the transaction will modify the account root sfSequence
// yet again.
sleAccount->setFieldU32(sfSequence, seqProxy.value() + 1);
return tesSUCCESS;
}
auto const keylet = keylet::ticket(accountID_, seqProxy);
return ticketDelete(view(), accountID_, keylet.key, j_);
}
// Remove a single Ticket from the ledger.
TER
Transactor::ticketDelete(
ApplyView& view,
AccountID const& account,
uint256 const& ticketIndex,
beast::Journal j)
{
// Delete the Ticket, adjust the account root ticket count, and
// reduce the owner count.
SLE::pointer const sleTicket = view.peek(keylet::ticket(ticketIndex));
if (!sleTicket)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Ticket disappeared from ledger.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
std::uint64_t const page{(*sleTicket)[sfOwnerNode]};
if (!view.dirRemove(keylet::ownerDir(account), page, ticketIndex, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Unable to delete Ticket from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Update the account root's TicketCount. If the ticket count drops to
// zero remove the (optional) field.
auto sleAccount = view.peek(keylet::account(account));
if (!sleAccount)
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not find Ticket owner account root.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
if (auto ticketCount = (*sleAccount)[~sfTicketCount])
{
if (*ticketCount == 1)
{
sleAccount->makeFieldAbsent(sfTicketCount);
}
else
{
ticketCount = *ticketCount - 1;
}
}
else
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "TicketCount field missing from account root.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Update the Ticket owner's reserve.
decreaseOwnerCountForObject(view, sleAccount, sleTicket, 1, j);
// Remove Ticket from ledger.
view.erase(sleTicket);
return tesSUCCESS;
}
// check stuff before you bother to lock the ledger
void
Transactor::preCompute()
{
XRPL_ASSERT(accountID_ != beast::kZero, "xrpl::Transactor::preCompute : nonzero account");
}
TER
Transactor::apply()
{
preCompute();
// If the transactor requires a valid account and the transaction doesn't
// list one, preflight will have already a flagged a failure.
auto const sle = view().peek(keylet::account(accountID_));
// sle must exist except for transactions
// that allow zero account.
XRPL_ASSERT(
sle != nullptr || accountID_ == beast::kZero,
"xrpl::Transactor::apply : non-null SLE or zero account");
if (sle)
{
preFeeBalance_ = STAmount{(*sle)[sfBalance]}.xrp();
TER result = consumeSeqProxy(sle);
if (!isTesSuccess(result))
return result;
result = payFee();
if (!isTesSuccess(result))
return result;
if (sle->isFieldPresent(sfAccountTxnID))
sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID());
view().update(sle);
}
return doApply();
}
NotTEC
Transactor::checkSign(
ReadView const& view,
ApplyFlags flags,
std::optional<uint256 const> const& parentBatchId,
AccountID const& idAccount,
STObject const& sigObject,
beast::Journal const j,
bool permitUncreatedAccount)
{
{
auto const sle = view.read(keylet::account(idAccount));
if ((view.rules().enabled(featureLendingProtocol) ||
view.rules().enabled(featureBatchV1_1) || view.rules().enabled(fixCleanup3_3_0)) &&
isPseudoAccount(sle))
{
// Pseudo-accounts can't sign transactions. This check is gated on a
// few different amendments so that it takes effect as soon as any of
// them is activated.
return tefBAD_AUTH;
}
}
auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
// Ignore signature check on batch inner transactions
if (parentBatchId && view.rules().enabled(featureBatchV1_1))
{
// Defensive Check: These values are also checked in Batch::preflight
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
sigObject.isFieldPresent(sfSigners))
{
return temINVALID_FLAG; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
if (((flags & TapDryRun) != 0u) && pkSigner.empty() && !sigObject.isFieldPresent(sfSigners))
{
// simulate: skip signature validation when neither SigningPubKey nor
// Signers are provided
return tesSUCCESS;
}
if (sigObject.isFieldPresent(sfSponsorSignature))
{
// Co-signed sponsorship
// Sanity check: already checked in preflight1
if (!sigObject.isFieldPresent(sfSponsor))
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sponsorID = sigObject.getAccountID(sfSponsor);
auto const sponsorSignature = sigObject.getFieldObject(sfSponsorSignature);
if (auto const ret = checkSign(view, flags, std::nullopt, sponsorID, sponsorSignature, j);
!isTesSuccess(ret))
return ret;
}
// If the pk is empty and not simulate or simulate and signers,
// then we must be multi-signing.
if (sigObject.isFieldPresent(sfSigners))
{
return checkMultiSign(view, flags, idAccount, sigObject, j);
}
// Check Single Sign
XRPL_ASSERT(!pkSigner.empty(), "xrpl::Transactor::checkSign : non-empty signer");
if (!publicKeyType(makeSlice(pkSigner)))
{
JLOG(j.trace()) << "checkSign: signing public key type is unknown";
return tefBAD_AUTH; // FIXME: should be better error!
}
// Look up the account.
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
auto const sleAccount = view.read(keylet::account(idAccount));
if (!sleAccount)
{
// An account that does not exist yet can only be authorized by its own
// master key, and only where an un-created signer is permitted (a batch
// whose earlier inner creates the account). Otherwise it cannot sign.
if (!permitUncreatedAccount)
return terNO_ACCOUNT;
if (idAccount != idSigner)
return tefBAD_AUTH;
return tesSUCCESS;
}
return checkSingleSign(view, idSigner, idAccount, sleAccount, j);
}
NotTEC
Transactor::checkSign(PreclaimContext const& ctx)
{
auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) ? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
}
NotTEC
Transactor::checkSingleSign(
ReadView const& view,
AccountID const& idSigner,
AccountID const& idAccount,
SLE::const_pointer sleAccount,
beast::Journal const j)
{
bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster);
// Signed with regular key.
if ((*sleAccount)[~sfRegularKey] == idSigner)
{
return tesSUCCESS;
}
// Signed with enabled master key.
if (!isMasterDisabled && idAccount == idSigner)
{
return tesSUCCESS;
}
// Signed with disabled master key.
if (isMasterDisabled && idAccount == idSigner)
{
return tefMASTER_DISABLED;
}
// Signed with any other key.
return tefBAD_AUTH;
}
NotTEC
Transactor::checkMultiSign(
ReadView const& view,
ApplyFlags flags,
AccountID const& id,
STObject const& sigObject,
beast::Journal const j)
{
// Get id's SignerList and Quorum.
STLedgerEntry::const_pointer const sleAccountSigners = view.read(keylet::signerList(id));
// If the signer list doesn't exist the account is not multi-signing.
if (!sleAccountSigners)
{
JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account.";
return tefNOT_MULTI_SIGNING;
}
// We have plans to support multiple SignerLists in the future. The
// presence and defaulted value of the SignerListID field will enable that.
XRPL_ASSERT(
sleAccountSigners->isFieldPresent(sfSignerListID),
"xrpl::Transactor::checkMultiSign : has signer list ID");
XRPL_ASSERT(
sleAccountSigners->getFieldU32(sfSignerListID) == 0,
"xrpl::Transactor::checkMultiSign : signer list ID is 0");
auto accountSigners = SignerEntries::deserialize(*sleAccountSigners, j, "ledger");
if (!accountSigners)
return accountSigners.error();
// Get the array of transaction signers.
STArray const& txSigners(sigObject.getFieldArray(sfSigners));
// Walk the accountSigners performing a variety of checks and see if
// the quorum is met.
// Both the multiSigners and accountSigners are sorted by account. So
// matching multi-signers to account signers should be a simple
// linear walk. *All* signers must be valid or the transaction fails.
std::uint32_t weightSum = 0;
auto iter = accountSigners->begin();
for (auto const& txSigner : txSigners)
{
AccountID const txSignerAcctID = txSigner.getAccountID(sfAccount);
// Attempt to match the SignerEntry with a Signer;
while (iter->account < txSignerAcctID)
{
if (++iter == accountSigners->end())
{
JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account.";
return tefBAD_SIGNATURE;
}
}
if (iter->account != txSignerAcctID)
{
// The SigningAccount is not in the SignerEntries.
JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account.";
return tefBAD_SIGNATURE;
}
// We found the SigningAccount in the list of valid signers. Now we
// need to compute the accountID that is associated with the signer's
// public key.
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
// spk being non-empty in non-simulate is checked in
// STTx::checkMultiSign
if (!spk.empty() && !publicKeyType(makeSlice(spk)))
{
JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown";
return tefBAD_SIGNATURE;
}
XRPL_ASSERT(
(flags & TapDryRun) || !spk.empty(),
"xrpl::Transactor::checkMultiSign : non-empty signer or "
"simulation");
AccountID const signingAcctIDFromPubKey =
spk.empty() ? txSignerAcctID : calcAccountID(PublicKey(makeSlice(spk)));
// Verify that the signingAcctID and the signingAcctIDFromPubKey
// belong together. Here are the rules:
//
// 1. "Phantom account": an account that is not in the ledger
// A. If signingAcctID == signingAcctIDFromPubKey and the
// signingAcctID is not in the ledger then we have a phantom
// account.
// B. Phantom accounts are always allowed as multi-signers.
//
// 2. "Master Key"
// A. signingAcctID == signingAcctIDFromPubKey, and signingAcctID
// is in the ledger.
// B. If the signingAcctID in the ledger does not have the
// asfDisableMaster flag set, then the signature is allowed.
//
// 3. "Regular Key"
// A. signingAcctID != signingAcctIDFromPubKey, and signingAcctID
// is in the ledger.
// B. If signingAcctIDFromPubKey == signingAcctID.RegularKey (from
// ledger) then the signature is allowed.
//
// No other signatures are allowed. (January 2015)
// In any of these cases we need to know whether the account is in
// the ledger. Determine that now.
auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID));
if (signingAcctIDFromPubKey == txSignerAcctID)
{
// Either Phantom or Master. Phantoms automatically pass.
if (sleTxSignerRoot)
{
// Master Key. Account may not have asfDisableMaster set.
std::uint32_t const signerAccountFlags = sleTxSignerRoot->getFieldU32(sfFlags);
if ((signerAccountFlags & lsfDisableMaster) != 0u)
{
JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster.";
return tefMASTER_DISABLED;
}
}
}
else
{
// May be a Regular Key. Let's find out.
// Public key must hash to the account's regular key.
if (!sleTxSignerRoot)
{
JLOG(j.trace()) << "applyTransaction: Non-phantom signer "
"lacks account root.";
return tefBAD_SIGNATURE;
}
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
{
JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey.";
return tefBAD_SIGNATURE;
}
if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey))
{
JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey.";
return tefBAD_SIGNATURE;
}
}
// The signer is legitimate. Add their weight toward the quorum.
weightSum += iter->weight;
}
// Cannot perform transaction if quorum is not met.
if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum))
{
JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum.";
return tefBAD_QUORUM;
}
// Met the quorum. Continue.
return tesSUCCESS;
}
//------------------------------------------------------------------------------
static void
removeUnfundedOffers(ApplyView& view, std::vector<uint256> const& offers, beast::Journal viewJ)
{
int removed = 0;
for (auto const& index : offers)
{
if (auto const sleOffer = view.peek(keylet::offer(index)))
{
// offer is unfunded
offerDelete(view, sleOffer, viewJ);
if (++removed == kUnfundedOfferRemoveLimit)
return;
}
}
}
static void
removeExpiredNFTokenOffers(
ApplyView& view,
std::vector<uint256> const& offers,
beast::Journal viewJ)
{
std::size_t removed = 0;
for (auto const& index : offers)
{
if (auto const offer = view.peek(keylet::nftokenOffer(index)))
{
nft::deleteTokenOffer(view, offer);
if (++removed == kExpiredOfferRemoveLimit)
return;
}
}
}
static void
removeExpiredCredentials(ApplyView& view, std::vector<uint256> const& creds, beast::Journal viewJ)
{
for (auto const& index : creds)
{
if (auto const sle = view.peek(keylet::credential(index)))
{
if (auto const ter = credentials::deleteSLE(view, sle, viewJ); !isTesSuccess(ter))
{
JLOG(viewJ.error())
<< "removeExpiredCredentials: failed to delete expired credential. Err: "
<< transToken(ter);
}
}
}
}
static void
removeDeletedTrustLines(
ApplyView& view,
std::vector<uint256> const& trustLines,
beast::Journal viewJ)
{
if (trustLines.size() > kMaxDeletableAmmTrustLines)
{
JLOG(viewJ.error()) << "removeDeletedTrustLines: deleted trustlines exceed max "
<< trustLines.size();
return;
}
for (auto const& index : trustLines)
{
if (auto const sleState = view.peek({ltRIPPLE_STATE, index});
!isTesSuccess(deleteAMMTrustLine(view, sleState, std::nullopt, viewJ)))
{
JLOG(viewJ.error()) << "removeDeletedTrustLines: failed to delete AMM trustline";
}
}
}
/**
* Reset the context, discarding any changes made and adjust the fee.
*
* @param fee The transaction fee to be charged.
* @return A pair containing the transaction result and the actual fee charged.
*/
std::pair<TER, XRPAmount>
Transactor::reset(XRPAmount fee)
{
ctx_.discard();
auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount)));
// The account should never be missing from the ledger. But if it
// is missing then we can't very well charge it a fee, can we?
if (!txnAcct)
return {tefINTERNAL, beast::kZero};
auto const feePayer = getFeePayer(view(), ctx_.tx);
auto const payerSle = view().peek(feePayer.keylet);
if (!payerSle)
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
XRPAmount balance = beast::kZero;
if (payerSle->isFieldPresent(feePayer.balanceField))
{
balance = payerSle->getFieldAmount(feePayer.balanceField).xrp();
}
else if (feePayer.balanceField != sfFeeAmount)
{
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
}
if (feePayer.type == FeePayerType::SponsorPreFunded && payerSle->isFieldPresent(sfMaxFee))
{
auto const cap = payerSle->getFieldAmount(sfMaxFee).xrp();
fee = std::min(fee, cap);
}
// A co-signed sponsor must never be charged into its own account reserve,
// so the fee is capped to the balance above the reserve rather than to the
// full balance.
XRPAmount spendable = balance;
if (feePayer.type == FeePayerType::SponsorCoSigned)
{
auto const sponsorReserve = accountReserve(view(), payerSle, j_);
// max(balance - reserve, 0) with overflow handling
spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero;
}
// balance should have already been checked in checkFee / preFlight.
XRPL_ASSERT(
(fee == beast::kZero || balance != beast::kZero) && (!view().open() || balance >= fee),
"xrpl::Transactor::reset : valid balance");
// We retry/reject the transaction if the account balance is zero or
// we're applying against an open ledger and the balance is less than
// the fee
if (fee > spendable)
fee = spendable;
// Since we reset the context, we need to charge the fee and update
// the account's sequence number (or consume the Ticket) again.
//
// If for some reason we are unable to consume the ticket or sequence
// then the ledger is corrupted. Rather than make things worse we
// reject the transaction.
auto const feeAmountAfter = balance - fee;
if (feeAmountAfter == beast::kZero && feePayer.balanceField == sfFeeAmount)
{
// Because ltSponsorship.sfFeeAmount is soeOptional
payerSle->makeFieldAbsent(feePayer.balanceField);
}
else
{
payerSle->setFieldAmount(feePayer.balanceField, feeAmountAfter);
}
TER const ter{consumeSeqProxy(txnAcct)};
XRPL_ASSERT(isTesSuccess(ter), "xrpl::Transactor::reset : result is tesSUCCESS");
if (isTesSuccess(ter))
{
view().update(txnAcct);
if (payerSle != txnAcct)
view().update(payerSle);
}
return {ter, fee};
}
FeePayer
Transactor::getFeePayer(ReadView const& view, STTx const& tx)
{
if (tx.isFieldPresent(sfSponsor) && isFeeSponsored(tx))
{
auto const sponsorID = tx.getAccountID(sfSponsor);
auto const sponseeID = tx.getInitiator();
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
// if pre-funded sponsorship exists, prefer it
if (view.exists(sponsorshipKeylet))
{
// pre funded
return FeePayer{
.id = sponsorID,
.keylet = sponsorshipKeylet,
.balanceField = sfFeeAmount,
.type = FeePayerType::SponsorPreFunded};
}
// Checked in Transactor::checkSponsor
XRPL_ASSERT(
tx.isFieldPresent(sfSponsorSignature),
"xrpl::getFeePayer has sponsor signature without a sponsorship object");
// co-signed
return FeePayer{
.id = sponsorID,
.keylet = keylet::account(sponsorID),
.balanceField = sfBalance,
.type = FeePayerType::SponsorCoSigned};
}
AccountID const payerID = tx.getInitiator();
auto const payerAccountKeylet = keylet::account(payerID);
auto const payerType =
tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account;
return FeePayer{
.id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
}
// The sole purpose of this function is to provide a convenient, named
// location to set a breakpoint, to be used when replaying transactions.
void
Transactor::trapTransaction(uint256 txHash) const
{
JLOG(j_.debug()) << "Transaction trapped: " << txHash;
}
std::tuple<TER, XRPAmount, bool>
Transactor::processPersistentChanges(TER result, XRPAmount fee)
{
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
// FIXME: This mechanism for doing work while returning a `tec` is
// awkward and very limiting. A more general purpose approach
// should be used, making it possible to do more useful work
// when transactions fail with a `tec` code.
auto typesForResult = [](TER const ter) {
std::unordered_set<LedgerEntryType> types;
if ((ter == tecOVERSIZE) || (ter == tecKILLED))
{
types.insert(ltOFFER);
}
else if (ter == tecINCOMPLETE)
{
types.insert(ltRIPPLE_STATE);
}
else if (ter == tecEXPIRED)
{
types.insert(ltNFTOKEN_OFFER);
types.insert(ltCREDENTIAL);
}
return types;
};
// Build a list of ledger entry types to collect, based on the
// result code. Only deleted objects of these types will be
// re-applied after the context is reset.
auto const typesToCollect = typesForResult(result);
std::map<LedgerEntryType, std::vector<uint256>> deletedObjects;
if (!typesToCollect.empty())
{
ctx_.visit(
[&typesToCollect, &deletedObjects](
uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
if (isDelete)
{
XRPL_ASSERT(
before && after,
"xrpl::Transactor::processPersistentChanges : non-null "
"SLE inputs");
if (before && after)
{
auto const type = before->getType();
if (typesToCollect.contains(type))
{
// For offers, only collect unfunded removals
// (where TakerPays is unchanged)
if (type == ltOFFER &&
before->getFieldAmount(sfTakerPays) !=
after->getFieldAmount(sfTakerPays))
return;
deletedObjects[type].push_back(index);
}
}
}
});
}
// Reset the context, potentially adjusting the fee.
{
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
}
// Re-apply the collected deletions, but only if the reset succeeded
// and the post-reset result still allows the same deletion type.
auto const typesToApply = typesForResult(result);
if (isTecClaim(result) && !typesToApply.empty())
{
auto const viewJ = ctx_.registry.get().getJournal("View");
for (auto const& [type, ids] : deletedObjects)
{
if (ids.empty() || !typesToApply.contains(type))
continue;
switch (type)
{
case ltOFFER:
removeUnfundedOffers(view(), ids, viewJ);
break;
case ltNFTOKEN_OFFER:
removeExpiredNFTokenOffers(view(), ids, viewJ);
break;
case ltRIPPLE_STATE:
removeDeletedTrustLines(view(), ids, viewJ);
break;
case ltCREDENTIAL:
removeExpiredCredentials(view(), ids, viewJ);
break;
// LCOV_EXCL_START
default:
UNREACHABLE(
"xrpl::Transactor::processPersistentChanges() : "
"unexpected type");
break;
// LCOV_EXCL_STOP
}
}
}
return {result, fee, isTecClaim(result)};
}
[[nodiscard]] TER
Transactor::checkInvariants(TER result, XRPAmount fee, InvariantScope scope)
{
if (scope == InvariantScope::Full)
return xrpl::checkInvariants(ctx_, result, fee, *this);
return xrpl::checkInvariants(ctx_, result, fee);
}
//------------------------------------------------------------------------------
ApplyResult
Transactor::operator()()
{
// Derive the trace_id from the transaction id so this apply-stage span
// shares one trace with the preflight and preclaim spans (which also use
// hashSpan on the same id).
auto const txID = ctx_.tx.getTransactionID();
auto span = telemetry::SpanGuard::hashSpan(
telemetry::TraceCategory::Transactions,
telemetry::tx_apply_span::transactor,
txID.data(),
txID.kBytes);
// Guard the attribute work behind the active check, as preflight does in
// applySteps.cpp: this runs for every transaction applied, and the type
// lookup and the parent-hash string are not free.
if (span)
{
// "apply" — the third apply-pipeline stage, after preflight and preclaim.
span.setAttribute(
telemetry::tx_apply_span::attr::stage, telemetry::tx_apply_span::val::apply);
if (auto const* fmt = TxFormats::getInstance().findByType(ctx_.tx.getTxnType()))
span.setAttribute(telemetry::tx_apply_span::attr::txType, fmt->getName().c_str());
// The ledger being worked on (seq + parent hash) — correlates this apply
// stage to the ledger/consensus trace it is building into.
span.setAttribute(
telemetry::tx_apply_span::attr::currentLedgerSeq,
static_cast<std::int64_t>(view().seq()));
span.setAttribute(
telemetry::tx_apply_span::attr::currentLedgerHash,
to_string(view().header().parentHash).c_str());
}
JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID();
// These global updates really should have been for every Transaction
// step: preflight, preclaim, and doApply. And even calculateBaseFee. See
// with_txn_type().
//
// raii classes for the current ledger rules.
CurrentTransactionRulesGuard const currentTransactionRulesGuard(view().rules());
#ifdef DEBUG
{
Serializer ser;
ctx_.tx.add(ser);
SerialIter sit(ser.slice());
STTx const s2(sit);
if (!s2.isEquivalent(ctx_.tx))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Transaction serdes mismatch";
JLOG(j_.fatal()) << ctx_.tx.getJson(JsonOptions::Values::None);
JLOG(j_.fatal()) << s2.getJson(JsonOptions::Values::None);
UNREACHABLE("xrpl::Transactor::operator() : transaction serdes mismatch");
// LCOV_EXCL_STOP
}
}
#endif
if (auto const& trap = ctx_.registry.get().getTrapTxID();
trap && *trap == ctx_.tx.getTransactionID())
{
trapTransaction(*trap);
}
auto result = ctx_.preclaimResult;
if (isTesSuccess(result))
result = apply();
// No transaction can return temUNKNOWN from apply,
// and it can't be passed in from a preclaim.
XRPL_ASSERT(result != temUNKNOWN, "xrpl::Transactor::operator() : result is not temUNKNOWN");
if (auto stream = j_.trace())
stream << "preclaim result: " << transToken(result);
auto fee = ctx_.tx.getFieldAmount(sfFee).xrp();
bool const canApply = std::invoke([&result, &fee, this] {
bool canApplyTmp = isTesSuccess(result);
if (ctx_.size() > kOversizeMetaDataCap)
result = tecOVERSIZE;
if (isTecClaim(result) && ((view().flags() & TapFailHard) != 0u))
{
// If the TapFailHard flag is set, a tec result
// must not do anything
ctx_.discard();
canApplyTmp = false;
}
else if (
(result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) ||
(result == tecEXPIRED) || (isTecClaimHardFail(result, view().flags())))
{
// This is and must remain the only place where `canApplyTmp` can change from false to
// true. Changing from true to false is no problem.
std::tie(result, fee, canApplyTmp) = processPersistentChanges(result, fee);
}
return canApplyTmp;
});
// Every exit from this function funnels through here, so this is also where
// the apply span records its outcome: each return path reports the engine
// result and whether the transaction was applied.
auto const logger = [this, &span](
TER result,
bool canApply,
std::optional<TxMeta>&& metadata = std::nullopt) -> ApplyResult {
JLOG(j_.trace()) << (canApply ? "applied " : "not applied ") << transToken(result);
// Also guarded: transToken() is a lookup returning a string, and this
// funnel runs on every exit path.
if (span)
{
span.setAttribute(
telemetry::tx_apply_span::attr::terResult, transToken(result).c_str());
span.setAttribute(telemetry::tx_apply_span::attr::applied, canApply);
// Mark the span as errored when the transaction was not applied or
// the engine result is not a success, so failed applies surface in
// span-status error counts alongside preflight and preclaim.
if (!canApply || !isTesSuccess(result))
span.setError(transToken(result));
}
return {result, canApply, std::move(metadata)};
};
if (!canApply)
return logger(result, canApply);
// First invariant pass: both protocol and transaction-specific
// checks run against the transaction's tentative outcome. If it
// does not return tecINVARIANT_FAILED, we can proceed to apply the
// tx.
result = checkInvariants(result, fee, InvariantScope::Full);
if (result == tecINVARIANT_FAILED)
{
// Fee-claim reset: roll the transaction's effects back so that
// only the fee deduction remains. This is the reset referenced
// by InvariantScope::ProtocolOnly.
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
// Re-check invariants against the post-reset (fee-claim only)
// state. The transaction's effects are gone, so the
// transaction-specific invariants no longer apply and only the
// protocol invariants are re-run. A failure here escalates to
// tefINVARIANT_FAILED and excludes the tx from the ledger.
if (isTesSuccess(result) || isTecClaim(result))
result = checkInvariants(result, fee, InvariantScope::ProtocolOnly);
}
// We ran through the invariant checker, which can, in some cases,
// return a tef error code. Don't apply the transaction in that case.
if (!isTecClaim(result) && !isTesSuccess(result))
return logger(result, false);
std::optional<TxMeta> metadata;
// Transaction succeeded fully or (retries are not allowed and the
// transaction could claim a fee)
// The transactor and invariant checkers guarantee that this will
// *never* trigger but if it, somehow, happens, don't allow a tx
// that charges a negative fee.
if (fee < beast::kZero)
Throw<std::logic_error>("fee charged is negative!");
// Charge whatever fee they specified. The fee has already been
// deducted from the balance of the account that issued the
// transaction. We just need to account for it in the ledger
// header.
if (!view().open() && fee != beast::kZero)
ctx_.destroyXRP(fee);
// Once we call apply, we will no longer be able to look at view()
metadata = ctx_.apply(result);
if ((ctx_.flags() & TapDryRun) != 0u)
return logger(result, false, std::move(metadata));
return logger(result, canApply, std::move(metadata));
}
} // namespace xrpl