mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 16:56:48 +00:00
Bulk documentation pass covering 702 C++ source files in src/libxrpl, src/xrpld, and include/xrpl. Adds class, function, parameter, and invariant docs per docs/DOCUMENTATION_STANDARDS.md. Squashed from the original three-part series (part 1 / part 2 / part 3) to avoid merge-conflict noise when rebasing the work onto current develop.
1058 lines
42 KiB
C++
1058 lines
42 KiB
C++
/**
|
||
* @file Transactor.h
|
||
*
|
||
* Base class and context structures for the XRPL transaction-processing
|
||
* pipeline.
|
||
*
|
||
* Every transaction type (Payment, OfferCreate, AMM, NFT, etc.) inherits
|
||
* from `Transactor` and participates in a strict three-phase pipeline:
|
||
*
|
||
* - **preflight** — stateless, no ledger access; validates format, flags, and
|
||
* signature syntax via `invokePreflight<T>()`.
|
||
* - **preclaim** — read-only `ReadView`; checks sequence, fee balance, and
|
||
* signature validity against ledger state.
|
||
* - **doApply** — mutable `ApplyView`; applies state changes; only reached
|
||
* when preclaim returns `tesSUCCESS`.
|
||
*
|
||
* Compile-time polymorphism is achieved through name hiding, not virtual
|
||
* dispatch: derived classes define static methods (`preflight`,
|
||
* `checkExtraFeatures`, `getFlagsMask`, `preflightSigValidated`) that are
|
||
* resolved by the `invokePreflight<T>` template at compile time.
|
||
*
|
||
* @see PreflightContext, PreclaimContext, ApplyContext
|
||
*/
|
||
|
||
#pragma once
|
||
|
||
#include <xrpl/beast/utility/Journal.h>
|
||
#include <xrpl/beast/utility/WrappedSink.h>
|
||
#include <xrpl/protocol/Permissions.h>
|
||
#include <xrpl/protocol/XRPAmount.h>
|
||
#include <xrpl/tx/ApplyContext.h>
|
||
#include <xrpl/tx/applySteps.h>
|
||
|
||
#include <utility>
|
||
|
||
namespace xrpl {
|
||
|
||
/** Immutable context passed to all preflight checks.
|
||
*
|
||
* Carries everything a stateless preflight validation step needs: the raw
|
||
* transaction, the active ledger rules, apply flags, and — for batch inner
|
||
* transactions — the hash of the enclosing batch. No ledger view is
|
||
* included because preflight must not access ledger state.
|
||
*
|
||
* Two constructors enforce the batch/non-batch invariant at construction
|
||
* time: the batch constructor asserts `TapBatch` is set and records the
|
||
* `parentBatchId`; the non-batch constructor asserts `TapBatch` is clear
|
||
* and leaves `parentBatchId` empty.
|
||
*/
|
||
struct PreflightContext
|
||
{
|
||
public:
|
||
/** Service registry providing network ID, hash router, and load fees. */
|
||
std::reference_wrapper<ServiceRegistry> registry;
|
||
/** The transaction being validated. */
|
||
STTx const& tx;
|
||
/** Active ledger rules (amendments) at the time of validation. */
|
||
Rules const rules;
|
||
/** Apply flags controlling validation behavior (e.g., `TapDryRun`, `TapBatch`). */
|
||
ApplyFlags flags;
|
||
/** Hash of the enclosing batch transaction, present only for batch inner transactions. */
|
||
std::optional<uint256 const> parentBatchId;
|
||
/** Journal for diagnostic logging. */
|
||
beast::Journal const j;
|
||
|
||
/** Construct a context for a batch inner transaction.
|
||
*
|
||
* @param registry Service registry.
|
||
* @param tx The inner transaction.
|
||
* @param parentBatchId Hash of the outer batch transaction.
|
||
* @param rules Active ledger rules.
|
||
* @param flags Apply flags; `TapBatch` must be set.
|
||
* @param j Journal for logging.
|
||
*/
|
||
PreflightContext(
|
||
ServiceRegistry& registry,
|
||
STTx const& tx,
|
||
uint256 parentBatchId,
|
||
Rules rules,
|
||
ApplyFlags flags,
|
||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||
: registry(registry)
|
||
, tx(tx)
|
||
, rules(std::move(rules))
|
||
, flags(flags)
|
||
, parentBatchId(parentBatchId)
|
||
, j(j)
|
||
{
|
||
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
|
||
}
|
||
|
||
/** Construct a context for an ordinary (non-batch) transaction.
|
||
*
|
||
* @param registry Service registry.
|
||
* @param tx The transaction.
|
||
* @param rules Active ledger rules.
|
||
* @param flags Apply flags; `TapBatch` must NOT be set.
|
||
* @param j Journal for logging.
|
||
*/
|
||
PreflightContext(
|
||
ServiceRegistry& registry,
|
||
STTx const& tx,
|
||
Rules rules,
|
||
ApplyFlags flags,
|
||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||
: registry(registry), tx(tx), rules(std::move(rules)), flags(flags), j(j)
|
||
{
|
||
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
|
||
}
|
||
|
||
PreflightContext&
|
||
operator=(PreflightContext const&) = delete;
|
||
};
|
||
|
||
/** Immutable context passed to all preclaim checks.
|
||
*
|
||
* Extends `PreflightContext` with a read-only `ReadView` so that preclaim
|
||
* can verify account existence, sequence validity, fee sufficiency, and
|
||
* signature correctness against the current ledger state. The result of
|
||
* the earlier preflight phase is carried forward in `preflightResult` so
|
||
* that preclaim helpers can short-circuit when preflight already failed.
|
||
*
|
||
* The same batch/non-batch constructor duality as `PreflightContext`
|
||
* applies: `parentBatchId` presence must match the `TapBatch` flag,
|
||
* enforced by assertion in the unified constructor.
|
||
*/
|
||
struct PreclaimContext
|
||
{
|
||
public:
|
||
/** Service registry providing network ID, hash router, and load fees. */
|
||
std::reference_wrapper<ServiceRegistry> registry;
|
||
/** Read-only view of the ledger against which preclaim checks are evaluated. */
|
||
ReadView const& view;
|
||
/** The `NotTEC` code returned by the earlier preflight phase. */
|
||
TER preflightResult;
|
||
/** Apply flags (e.g., `TapDryRun`, `TapBatch`, `TapUnlimited`). */
|
||
ApplyFlags flags;
|
||
/** The transaction being evaluated. */
|
||
STTx const& tx;
|
||
/** Hash of the enclosing batch transaction; set iff `TapBatch` is active. */
|
||
std::optional<uint256 const> const parentBatchId;
|
||
/** Journal for diagnostic logging. */
|
||
beast::Journal const j;
|
||
|
||
/** Construct for a batch inner transaction (or ordinary tx with explicit batch ID).
|
||
*
|
||
* Asserts that `parentBatchId.has_value() == ((flags & TapBatch) == TapBatch)`.
|
||
*
|
||
* @param registry Service registry.
|
||
* @param view Read-only ledger view.
|
||
* @param preflightResult Result from the preflight phase.
|
||
* @param tx The transaction.
|
||
* @param flags Apply flags.
|
||
* @param parentBatchId Hash of the outer batch, or `std::nullopt`.
|
||
* @param j Journal for logging.
|
||
*/
|
||
PreclaimContext(
|
||
ServiceRegistry& registry,
|
||
ReadView const& view,
|
||
TER preflightResult,
|
||
STTx const& tx,
|
||
ApplyFlags flags,
|
||
std::optional<uint256> parentBatchId,
|
||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||
: registry(registry)
|
||
, view(view)
|
||
, preflightResult(preflightResult)
|
||
, flags(flags)
|
||
, tx(tx)
|
||
, parentBatchId(parentBatchId)
|
||
, j(j)
|
||
{
|
||
XRPL_ASSERT(
|
||
parentBatchId.has_value() == ((flags & TapBatch) == TapBatch),
|
||
"Parent Batch ID should be set if batch apply flag is set");
|
||
}
|
||
|
||
/** Construct for an ordinary (non-batch) transaction.
|
||
*
|
||
* @param registry Service registry.
|
||
* @param view Read-only ledger view.
|
||
* @param preflightResult Result from the preflight phase.
|
||
* @param tx The transaction.
|
||
* @param flags Apply flags; `TapBatch` must NOT be set.
|
||
* @param j Journal for logging.
|
||
*/
|
||
PreclaimContext(
|
||
ServiceRegistry& registry,
|
||
ReadView const& view,
|
||
TER preflightResult,
|
||
STTx const& tx,
|
||
ApplyFlags flags,
|
||
beast::Journal j = beast::Journal{beast::Journal::getNullSink()})
|
||
: PreclaimContext(registry, view, preflightResult, tx, flags, std::nullopt, j)
|
||
{
|
||
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
|
||
}
|
||
|
||
PreclaimContext&
|
||
operator=(PreclaimContext const&) = delete;
|
||
};
|
||
|
||
class TxConsequences;
|
||
struct PreflightResult;
|
||
// Needed for preflight specialization
|
||
class Change;
|
||
|
||
/** Base class for all XRPL transaction processors.
|
||
*
|
||
* Implements the three-phase transaction pipeline: preflight (stateless
|
||
* validation), preclaim (read-only ledger checks), and doApply (mutable
|
||
* ledger application). Every concrete transaction type (Payment,
|
||
* OfferCreate, AMMCreate, etc.) inherits from this class.
|
||
*
|
||
* Polymorphism in the preflight phase is achieved through compile-time
|
||
* name hiding rather than virtual dispatch. Derived classes define
|
||
* static methods — `preflight`, `preclaim`, `getFlagsMask`,
|
||
* `checkExtraFeatures`, `preflightSigValidated` — that are resolved by
|
||
* `invokePreflight<T>()` at the call site. See the comment block on
|
||
* `invokePreflight` for the rules on what derived classes must and must
|
||
* not define.
|
||
*
|
||
* The single virtual entry point for state mutation is `doApply()`.
|
||
* `operator()()` is the top-level dispatch called by the apply loop;
|
||
* it drives all three phases, handles fee claiming on failure, runs
|
||
* invariant checks, and manages `tapDRY_RUN` simulation semantics.
|
||
*
|
||
* @note Instances are not copyable. One transactor object is created
|
||
* per transaction application.
|
||
*/
|
||
class Transactor
|
||
{
|
||
protected:
|
||
/** Apply context holding the sandboxed ledger view and transaction. */
|
||
ApplyContext& ctx_;
|
||
/** Wrapped journal sink that prepends the transaction ID to each log line. */
|
||
beast::WrappedSink sink_;
|
||
/** Journal backed by `sink_`; use this for all logging inside transactors. */
|
||
beast::Journal const j_;
|
||
|
||
/** The account that submitted the transaction (`sfAccount`). */
|
||
AccountID const account_;
|
||
/** Account balance captured immediately before fee deduction in `apply()`.
|
||
*
|
||
* Reserve checks in `doApply` must use this value rather than the
|
||
* post-fee balance to allow accounts to dip into their reserve to pay
|
||
* the fee without violating the reserve requirement for new objects.
|
||
*/
|
||
XRPAmount preFeeBalance_{};
|
||
|
||
public:
|
||
virtual ~Transactor() = default;
|
||
Transactor(Transactor const&) = delete;
|
||
Transactor&
|
||
operator=(Transactor const&) = delete;
|
||
|
||
/** Controls how `TxConsequences` are produced for the transaction queue.
|
||
*
|
||
* - `Normal` — standard fee/sequence consequences (most transactors).
|
||
* - `Blocker` — signals that applying this transaction may prevent
|
||
* subsequent queued transactions from the same account from
|
||
* claiming fees (e.g., `SetRegularKey`, `AccountDelete`).
|
||
* - `Custom` — the transactor implements `makeTxConsequences()` for
|
||
* type-specific cost modeling (e.g., `Payment`, `OfferCreate`).
|
||
*
|
||
* Each derived class must declare:
|
||
* @code
|
||
* static constexpr ConsequencesFactoryType ConsequencesFactory{...};
|
||
* @endcode
|
||
* The correct factory is selected at compile time in `applySteps.cpp`
|
||
* via C++20 `requires` constraints.
|
||
*/
|
||
enum class ConsequencesFactoryType { Normal, Blocker, Custom };
|
||
|
||
/** Execute the full transaction pipeline for this transactor instance.
|
||
*
|
||
* Called by the apply loop after preclaim succeeds. Runs:
|
||
* 1. RAII numeric-rule guards (`NumberSO`, `CurrentTransactionRulesGuard`).
|
||
* 2. Debug-mode serialization round-trip check.
|
||
* 3. Optional debug trap (`trapTransaction`).
|
||
* 4. `apply()` if preclaim returned `tesSUCCESS`; otherwise returns the
|
||
* preclaim error directly.
|
||
* 5. `tecOVERSIZE` roll-back: if metadata grew too large, discards all
|
||
* mutations, re-deducts fee only, and removes unfunded offers found
|
||
* during the failed apply.
|
||
* 6. `tapFAIL_HARD`: on a `tec*` result, discards everything including
|
||
* the fee.
|
||
* 7. Invariant checks via `checkInvariants`; a failing invariant triggers
|
||
* a second reset and fee-only commit.
|
||
* 8. Forces `applied = false` when `tapDRY_RUN` is set.
|
||
*
|
||
* @return `{result, applied, metadata}`. `applied` is false when the
|
||
* transaction produces no ledger changes (dry-run, `tef*`, `tem*`,
|
||
* or invariant escalation to `tefINVARIANT_FAILED`).
|
||
*/
|
||
ApplyResult
|
||
operator()();
|
||
|
||
/** Return the mutable apply view for this transaction. */
|
||
ApplyView&
|
||
view()
|
||
{
|
||
return ctx_.view();
|
||
}
|
||
|
||
/** Return the read-only apply view for this transaction. */
|
||
[[nodiscard]] ApplyView const&
|
||
view() const
|
||
{
|
||
return ctx_.view();
|
||
}
|
||
|
||
/** Check all invariants for the current transaction.
|
||
*
|
||
* Runs transaction-specific invariants first (visitInvariantEntry +
|
||
* finalizeInvariants), then protocol-level invariants. Both layers
|
||
* always run; the worst failure code is returned.
|
||
*
|
||
* @param result the tentative TER from transaction processing.
|
||
* @param fee the fee consumed by the transaction.
|
||
*
|
||
* @return the final TER after all invariant checks.
|
||
*/
|
||
[[nodiscard]] TER
|
||
checkInvariants(TER result, XRPAmount fee);
|
||
|
||
// ---- Preclaim-phase static helpers (overridable via name hiding) --------
|
||
//
|
||
// These static functions are called from the preclaim dispatch in
|
||
// applySteps.cpp using name hiding to accomplish compile-time
|
||
// polymorphism. Derived classes can shadow them to add or replace
|
||
// validation logic. They are NOT virtual; the compiler provides no
|
||
// protection against incorrect overrides.
|
||
|
||
/** Verify the transaction's sequence number or ticket against the ledger.
|
||
*
|
||
* Returns `terNO_ACCOUNT` if the source account does not exist,
|
||
* `terPRE_SEQ` / `tefPAST_SEQ` for sequence-number mismatches, and
|
||
* `terPRE_TICKET` / `tefNO_TICKET` for ticket-based transactions.
|
||
*
|
||
* @param view Read-only ledger view.
|
||
* @param tx The transaction.
|
||
* @param j Journal for trace logging.
|
||
* @return `tesSUCCESS` if the sequence/ticket is consumable.
|
||
*/
|
||
static NotTEC
|
||
checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j);
|
||
|
||
/** Verify `sfAccountTxnID`, `sfLastLedgerSequence`, and duplicate detection.
|
||
*
|
||
* Returns `tefWRONG_PRIOR` if `sfAccountTxnID` does not match the
|
||
* account's last transaction hash, `tefMAX_LEDGER` if the current ledger
|
||
* sequence exceeds `sfLastLedgerSequence`, and `tefALREADY` if the
|
||
* transaction is already in the ledger.
|
||
*
|
||
* @param ctx Preclaim context.
|
||
* @return `tesSUCCESS` or a `tef*` / `ter*` error.
|
||
*/
|
||
static NotTEC
|
||
checkPriorTxAndLastLedger(PreclaimContext const& ctx);
|
||
|
||
/** Verify that the fee attached to the transaction is sufficient.
|
||
*
|
||
* For open-ledger transactions, the fee must meet the load-scaled
|
||
* minimum returned by `minimumFee()`. Also checks that the fee payer's
|
||
* account exists and has sufficient balance.
|
||
*
|
||
* @param ctx Preclaim context.
|
||
* @param baseFee Unscaled base fee computed by `calculateBaseFee()`.
|
||
* @return `tesSUCCESS`, `telINSUF_FEE_P`, `tecINSUFF_FEE`,
|
||
* `terINSUF_FEE_B`, or `terNO_ACCOUNT`.
|
||
*/
|
||
static TER
|
||
checkFee(PreclaimContext const& ctx, XRPAmount baseFee);
|
||
|
||
/** Verify the cryptographic signature for an ordinary transaction.
|
||
*
|
||
* Dispatches to `checkMultiSign()` when `sfSigners` is present, or
|
||
* `checkSingleSign()` otherwise. Skips the check for batch inner
|
||
* transactions (authorized by the outer batch) and dry-run simulations
|
||
* without a signing key. Rejects pseudo-account signers when
|
||
* `featureLendingProtocol` is active.
|
||
*
|
||
* @param ctx Preclaim context.
|
||
* @return `tesSUCCESS` or a `tef*` error code.
|
||
*/
|
||
static NotTEC
|
||
checkSign(PreclaimContext const& ctx);
|
||
|
||
/** Verify the `sfBatchSigners` array for an outer batch transaction.
|
||
*
|
||
* Iterates the batch signers, dispatching to `checkMultiSign()` or
|
||
* `checkSingleSign()` as appropriate. Allows a signer for an
|
||
* account that does not yet exist in the ledger, provided the signing
|
||
* key matches the account's master key (used for fund-on-creation inner
|
||
* transactions).
|
||
*
|
||
* @param ctx Preclaim context for the outer batch transaction.
|
||
* @return `tesSUCCESS` or a `tef*` error code.
|
||
*/
|
||
static NotTEC
|
||
checkBatchSign(PreclaimContext const& ctx);
|
||
|
||
/** Compute the base transaction fee in drops, unscaled for load.
|
||
*
|
||
* Base fee = ledger's configured base fee + one extra base fee per
|
||
* multisignature in `sfSigners`. Does not account for server load;
|
||
* use `minimumFee()` for the load-adjusted value.
|
||
*
|
||
* @param view Read-only ledger view (supplies `fees().base`).
|
||
* @param tx The transaction.
|
||
* @return Fee in drops (XRPAmount).
|
||
*/
|
||
static XRPAmount
|
||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||
|
||
/** Compile-time preflight dispatch for transaction type `T`.
|
||
*
|
||
* The canonical entry point for preflight validation. Executes the
|
||
* following steps in order, returning on the first non-`tesSUCCESS`:
|
||
*
|
||
* 1. Amendment gate: returns `temDISABLED` if the transaction's
|
||
* required amendment is not active.
|
||
* 2. `T::checkExtraFeatures(ctx)` — additional amendment gates defined
|
||
* by the derived class (return `temDISABLED` on failure).
|
||
* 3. `preflight1(ctx, T::getFlagsMask(ctx))` — validates account field,
|
||
* fee field, signing key format, network ID, flags, and
|
||
* ticket/AccountTxnID exclusivity.
|
||
* 4. `T::preflight(ctx)` — transaction-specific field validation.
|
||
* 5. `preflight2(ctx)` — cryptographic signature check via hash-router
|
||
* cache. Skipped for batch inner transactions.
|
||
* 6. `T::preflightSigValidated(ctx)` — optional post-signature checks
|
||
* (e.g., expensive crypto conditions).
|
||
*
|
||
* @note Do NOT define `invokePreflight` in a derived class. Instead,
|
||
* define any combination of the static methods above. Do NOT call
|
||
* `preflight1` or `preflight2` directly; they are called in the
|
||
* correct order by this template. Do NOT gate on amendments in
|
||
* `preflight`; use `checkExtraFeatures` for that. Do NOT validate
|
||
* flags in `preflight`; define `getFlagsMask` instead.
|
||
*
|
||
* @note The explicit specialization `invokePreflight<Change>` is
|
||
* defined in `Change.cpp` and uses entirely different logic because
|
||
* `Change` is a pseudo-transaction with no real sender.
|
||
*
|
||
* @tparam T The concrete transactor type.
|
||
* @param ctx Preflight context.
|
||
* @return `tesSUCCESS` or a `tem*` / `tel*` error.
|
||
*/
|
||
template <class T>
|
||
static NotTEC
|
||
invokePreflight(PreflightContext const& ctx);
|
||
|
||
/** Base-class preclaim hook; most transactors do not need to override this.
|
||
*
|
||
* The sequence/fee/sign checks are called directly by the preclaim
|
||
* dispatch in `applySteps.cpp` before this method. Override only to
|
||
* add extra read-only ledger checks that cannot be expressed as field
|
||
* validation in `preflight`.
|
||
*
|
||
* @param ctx Preclaim context.
|
||
* @return `tesSUCCESS` (base implementation).
|
||
*/
|
||
static TER
|
||
preclaim(PreclaimContext const& ctx)
|
||
{
|
||
return tesSUCCESS;
|
||
}
|
||
|
||
/** Verify delegate permissions if `sfDelegate` is present.
|
||
*
|
||
* If the transaction carries an `sfDelegate` field, reads the
|
||
* `DelegateObject` at `keylet::delegate(account, delegate)` and
|
||
* verifies that its permission set covers this transaction type.
|
||
* Returns `terNO_DELEGATE_PERMISSION` if the object is missing or the
|
||
* permission is not granted.
|
||
*
|
||
* Called as a static method during preclaim so the ledger check
|
||
* happens before any mutation.
|
||
*
|
||
* @param view Read-only ledger view.
|
||
* @param tx The transaction (may contain `sfDelegate`).
|
||
* @return `tesSUCCESS` or `terNO_DELEGATE_PERMISSION`.
|
||
*/
|
||
static NotTEC
|
||
checkPermission(ReadView const& view, STTx const& tx);
|
||
// -------------------------------------------------------------------------
|
||
|
||
/** Remove a single Ticket SLE and adjust the owner's ticket count and reserve.
|
||
*
|
||
* Used by `AccountDelete` (via a static interface) and by
|
||
* `consumeSeqProxy` when a ticket-based transaction is applied.
|
||
* Removes the ticket from the owner directory, decrements `sfTicketCount`
|
||
* on the account root, adjusts the owner reserve count, and erases the
|
||
* ticket SLE.
|
||
*
|
||
* @param view Mutable ledger view.
|
||
* @param account Owner of the ticket.
|
||
* @param ticketIndex Ledger index of the Ticket SLE.
|
||
* @param j Journal for fatal-error logging.
|
||
* @return `tesSUCCESS` or `tefBAD_LEDGER` if the ledger is corrupt.
|
||
*/
|
||
static TER
|
||
ticketDelete(
|
||
ApplyView& view,
|
||
AccountID const& account,
|
||
uint256 const& ticketIndex,
|
||
beast::Journal j);
|
||
|
||
protected:
|
||
/** Run the sequence/fee/state-mutation steps for a validated transaction.
|
||
*
|
||
* Called by `operator()()` when preclaim returned `tesSUCCESS`.
|
||
* Snapshots `preFeeBalance_`, advances the sequence (or consumes the
|
||
* ticket), deducts the fee, updates `sfAccountTxnID`, then calls
|
||
* `doApply()`.
|
||
*
|
||
* @return The TER returned by `doApply()`, or a `tef*` code if the
|
||
* sequence/fee bookkeeping fails (indicates ledger corruption).
|
||
*/
|
||
TER
|
||
apply();
|
||
|
||
/** Construct a transactor bound to the given apply context.
|
||
*
|
||
* Initialises `account_` from `ctx.tx[sfAccount]` and sets up the
|
||
* transaction-ID-prefixed journal sink.
|
||
*/
|
||
explicit Transactor(ApplyContext& ctx);
|
||
|
||
/** Perform any pre-apply computation that should not repeat per-ledger.
|
||
*
|
||
* Called at the start of `apply()` before `consumeSeqProxy` and
|
||
* `payFee`. The base implementation asserts that `account_` is
|
||
* non-zero. Derived classes may cache expensive lookups here.
|
||
*/
|
||
virtual void
|
||
preCompute();
|
||
|
||
/** Apply the transaction's state changes to the mutable ledger view.
|
||
*
|
||
* The sole virtual method in the pipeline. Only called when all
|
||
* preflight and preclaim checks have passed and the fee/sequence have
|
||
* been consumed.
|
||
*
|
||
* Implementations must return `tesSUCCESS` for a full commit.
|
||
* Returning a `tec*` code causes `operator()()` to roll back all
|
||
* mutations via `reset()` and re-apply the fee only. The tec rollback
|
||
* is automatic — there is no need to order mutations defensively or
|
||
* undo partial changes before returning `tec*`.
|
||
*
|
||
* @return `tesSUCCESS` or a `tec*` error. Must not return `tem*`,
|
||
* `tef*`, or `ter*` codes (those belong in preflight/preclaim).
|
||
*/
|
||
virtual TER
|
||
doApply() = 0;
|
||
|
||
/** Inspect a single ledger entry modified by this transaction.
|
||
*
|
||
* Called once for every SLE created, modified, or deleted by the
|
||
* transaction, before finalizeInvariants. Implementations should
|
||
* accumulate whatever state they need to verify transaction-specific
|
||
* post-conditions.
|
||
*
|
||
* @param isDelete true if the entry was erased from the ledger.
|
||
* @param before the entry's state before the transaction (nullptr
|
||
* for newly created entries).
|
||
* @param after the entry's state as supplied by the apply logic
|
||
* for this transaction. For deletions, this is the
|
||
* SLE being erased and is not guaranteed to be null;
|
||
* callers must use isDelete rather than after == nullptr
|
||
* to detect deletions.
|
||
*/
|
||
virtual void
|
||
visitInvariantEntry(
|
||
bool isDelete,
|
||
std::shared_ptr<SLE const> const& before,
|
||
std::shared_ptr<SLE const> const& after) = 0;
|
||
|
||
/** Check transaction-specific post-conditions after all entries have
|
||
* been visited.
|
||
*
|
||
* Called once after every modified ledger entry has been passed to
|
||
* visitInvariantEntry. Returns true if all transaction-specific
|
||
* invariants hold, or false to fail the transaction with
|
||
* tecINVARIANT_FAILED.
|
||
*
|
||
* @param tx the transaction being applied.
|
||
* @param result the tentative TER result so far.
|
||
* @param fee the fee consumed by the transaction.
|
||
* @param view read-only view of the ledger after the transaction.
|
||
* @param j journal for logging invariant failures.
|
||
*
|
||
* @return true if all invariants pass; false otherwise.
|
||
*/
|
||
[[nodiscard]] virtual bool
|
||
finalizeInvariants(
|
||
STTx const& tx,
|
||
TER result,
|
||
XRPAmount fee,
|
||
ReadView const& view,
|
||
beast::Journal const& j) = 0;
|
||
|
||
/** Compute the load-scaled minimum fee required to relay this transaction.
|
||
*
|
||
* Scales `baseFee` using the node's current `LoadFeeTrack`. The
|
||
* `TapUnlimited` flag suppresses load scaling (used for locally-submitted
|
||
* or admin transactions).
|
||
*
|
||
* @param registry Service registry (provides `getFeeTrack()`).
|
||
* @param baseFee Unscaled base fee from `calculateBaseFee()`.
|
||
* @param fees Fee schedule from the current ledger.
|
||
* @param flags Apply flags; `TapUnlimited` disables load scaling.
|
||
* @return Minimum fee in drops that the network will accept.
|
||
*/
|
||
static XRPAmount
|
||
minimumFee(ServiceRegistry& registry, XRPAmount baseFee, Fees const& fees, ApplyFlags flags);
|
||
|
||
/** Return the owner-reserve increment as a fee, in drops.
|
||
*
|
||
* Used by transactions that create a ledger object and wish to charge
|
||
* one full reserve increment as the transaction fee (e.g.,
|
||
* `AccountDelete`, `AMMCreate`, `LoanBrokerSet`).
|
||
* Asserts that the reserve increment is at least 100× the base fee,
|
||
* ensuring the anti-spam reserve is meaningful.
|
||
*
|
||
* @param view Read-only ledger view (supplies `fees().increment`).
|
||
* @param tx The transaction (unused; present for uniformity).
|
||
* @return `fees().increment` in drops.
|
||
*/
|
||
static XRPAmount
|
||
calculateOwnerReserveFee(ReadView const& view, STTx const& tx);
|
||
|
||
/** Low-level signature check used by both the preclaim and batch paths.
|
||
*
|
||
* Selects between `checkMultiSign` and `checkSingleSign` based on
|
||
* transaction contents. Handles the special cases for batch inner
|
||
* transactions (no signature required), dry-run simulation (no key or
|
||
* signers is valid), and pseudo-account rejection under
|
||
* `featureLendingProtocol`.
|
||
*
|
||
* The public `checkSign(PreclaimContext const&)` overload is a thin
|
||
* wrapper around this one.
|
||
*
|
||
* @param view Read-only ledger view.
|
||
* @param flags Apply flags.
|
||
* @param parentBatchId Set for batch inner transactions; suppresses sig check.
|
||
* @param idAccount The account whose key must authorize the transaction.
|
||
* @param sigObject The STObject containing `sfSigningPubKey` /
|
||
* `sfSigners` (usually `ctx.tx`).
|
||
* @param j Journal for trace logging.
|
||
* @return `tesSUCCESS` or a `tef*` error.
|
||
*/
|
||
static NotTEC
|
||
checkSign(
|
||
ReadView const& view,
|
||
ApplyFlags flags,
|
||
std::optional<uint256 const> const& parentBatchId,
|
||
AccountID const& idAccount,
|
||
STObject const& sigObject,
|
||
beast::Journal const j);
|
||
|
||
/** Amendment gate hook — override to gate the transaction on amendments.
|
||
*
|
||
* Called by `invokePreflight<T>` before `preflight1`. The base
|
||
* implementation always returns `true` (no extra gating). Derived
|
||
* classes that depend on amendments not listed in `transactions.macro`
|
||
* should override this method; return `false` to produce `temDISABLED`.
|
||
*
|
||
* @param ctx Preflight context.
|
||
* @return `true` if the transaction is permitted; `false` to disable it.
|
||
*/
|
||
static bool
|
||
checkExtraFeatures(PreflightContext const& ctx);
|
||
|
||
/** Flag-mask hook — override to declare valid flags for this transaction.
|
||
*
|
||
* The returned mask is passed to `preflight0` to reject unknown flag bits.
|
||
* The base implementation returns `tfUniversalMask`. Derived classes
|
||
* should override this to OR in their transaction-specific flag bits.
|
||
*
|
||
* @param ctx Preflight context.
|
||
* @return Bitmask of all valid flag bits for this transaction type.
|
||
*/
|
||
static std::uint32_t
|
||
getFlagsMask(PreflightContext const& ctx);
|
||
|
||
/** Post-signature preflight hook — override for expensive post-sig checks.
|
||
*
|
||
* Called by `invokePreflight<T>` after `preflight2` (signature
|
||
* verification). The base implementation returns `tesSUCCESS`.
|
||
* Derived classes that need to perform expensive checks that can only
|
||
* run after the signature is verified (e.g., crypto-condition validation
|
||
* in `EscrowFinish`) should override this.
|
||
*
|
||
* @param ctx Preflight context.
|
||
* @return `tesSUCCESS` or a `tem*` error.
|
||
*/
|
||
static NotTEC
|
||
preflightSigValidated(PreflightContext const& ctx);
|
||
|
||
/** Validate an optional blob field's length.
|
||
*
|
||
* Returns `false` if the slice is present but empty or exceeds
|
||
* `maxLength`; returns `true` if absent or within bounds.
|
||
*
|
||
* @param slice Optional blob (e.g., from `tx[~sfURI]`).
|
||
* @param maxLength Maximum permitted byte length.
|
||
* @return `true` if the length is valid.
|
||
*/
|
||
static bool
|
||
validDataLength(std::optional<Slice> const& slice, std::size_t maxLength);
|
||
|
||
/** Validate that an optional numeric field is within `[min, max]`.
|
||
*
|
||
* An absent optional (`std::nullopt`) is treated as valid — only
|
||
* present values are range-checked. This reflects the convention that
|
||
* optional fields are legal to omit.
|
||
*
|
||
* @tparam T Numeric type (must support `<=` comparison).
|
||
* @param value Optional field value.
|
||
* @param max Inclusive upper bound.
|
||
* @param min Inclusive lower bound (default-constructed, usually 0).
|
||
* @return `true` if absent or within `[min, max]`.
|
||
*/
|
||
template <class T>
|
||
static bool
|
||
validNumericRange(std::optional<T> value, T max, T min = T{});
|
||
|
||
/** Validate an optional strong-unit numeric field within `[min, max]`.
|
||
*
|
||
* Overload for `unit::ValueUnit<Unit, T>` bounds to maintain type
|
||
* safety across unit systems. Delegates to the plain-value overload.
|
||
*
|
||
* @tparam T Underlying numeric type.
|
||
* @tparam Unit Unit tag.
|
||
* @param value Optional field value (raw numeric).
|
||
* @param max Inclusive upper bound (unit-typed).
|
||
* @param min Inclusive lower bound (unit-typed, default zero).
|
||
* @return `true` if absent or within `[min, max]`.
|
||
*/
|
||
template <class T, class Unit>
|
||
static bool
|
||
validNumericRange(
|
||
std::optional<T> value,
|
||
unit::ValueUnit<Unit, T> max,
|
||
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
|
||
|
||
/** Validate that an optional numeric field is at least `min`.
|
||
*
|
||
* An absent optional is treated as valid.
|
||
*
|
||
* @tparam T Numeric type.
|
||
* @param value Optional field value.
|
||
* @param min Inclusive lower bound (default-constructed, usually 0).
|
||
* @return `true` if absent or `>= min`.
|
||
*/
|
||
template <class T>
|
||
static bool
|
||
validNumericMinimum(std::optional<T> value, T min = T{});
|
||
|
||
/** Validate an optional strong-unit numeric field against a minimum.
|
||
*
|
||
* Overload for `unit::ValueUnit<Unit, T>` bounds. Delegates to the
|
||
* plain-value overload.
|
||
*
|
||
* @tparam T Underlying numeric type.
|
||
* @tparam Unit Unit tag.
|
||
* @param value Optional field value.
|
||
* @param min Inclusive lower bound (unit-typed, default zero).
|
||
* @return `true` if absent or `>= min`.
|
||
*/
|
||
template <class T, class Unit>
|
||
static bool
|
||
validNumericMinimum(
|
||
std::optional<T> value,
|
||
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
|
||
|
||
private:
|
||
/** Roll back all doApply mutations and re-apply fee deduction only.
|
||
*
|
||
* Calls `ctx_.discard()` to discard all ledger changes, then
|
||
* re-deducts the fee from the fee payer's balance (clamped to the
|
||
* available balance), and re-consumes the sequence/ticket. Used for
|
||
* fee-claiming `tec*` results and after invariant failures.
|
||
*
|
||
* @param fee Requested fee in drops; clamped to available balance.
|
||
* @return `{tesSUCCESS, actualFee}` on success, or
|
||
* `{tefINTERNAL, 0}` if the account SLE is missing (ledger
|
||
* corruption).
|
||
*/
|
||
std::pair<TER, XRPAmount>
|
||
reset(XRPAmount fee);
|
||
|
||
/** Advance `sfSequence` or consume the Ticket for this transaction.
|
||
*
|
||
* For sequence-based transactions, increments `sfSequence` by one.
|
||
* For ticket-based transactions, delegates to `ticketDelete`.
|
||
*
|
||
* @param sleAccount Mutable SLE for the submitting account.
|
||
* @return `tesSUCCESS` or `tefBAD_LEDGER` if the ticket is missing.
|
||
*/
|
||
TER
|
||
consumeSeqProxy(SLE::pointer const& sleAccount);
|
||
|
||
/** Deduct the transaction fee from the fee payer's balance.
|
||
*
|
||
* Reads `sfFee` from the transaction and subtracts it from the fee
|
||
* payer's `sfBalance`. The caller is responsible for calling
|
||
* `view().update(sle)` to commit the change.
|
||
*
|
||
* @return `tesSUCCESS` or `tefINTERNAL` if the payer account is absent.
|
||
*/
|
||
TER
|
||
payFee();
|
||
|
||
/** Verify a single-signature transaction against the account root.
|
||
*
|
||
* Checks, in priority order: regular key → enabled master key →
|
||
* disabled master key (`tefMASTER_DISABLED`) → unknown key
|
||
* (`tefBAD_AUTH`).
|
||
*
|
||
* @param view Read-only ledger view.
|
||
* @param idSigner AccountID derived from the signing public key.
|
||
* @param idAccount AccountID from `sfAccount` (the authorizing account).
|
||
* @param sleAccount AccountRoot SLE for `idAccount`.
|
||
* @param j Journal for trace logging.
|
||
* @return `tesSUCCESS`, `tefMASTER_DISABLED`, or `tefBAD_AUTH`.
|
||
*/
|
||
static NotTEC
|
||
checkSingleSign(
|
||
ReadView const& view,
|
||
AccountID const& idSigner,
|
||
AccountID const& idAccount,
|
||
std::shared_ptr<SLE const> sleAccount,
|
||
beast::Journal const j);
|
||
|
||
/** Verify a multi-signature against the account's SignerList.
|
||
*
|
||
* Performs an O(n) linear merge of the sorted `sfSigners` array from
|
||
* the transaction against the sorted `SignerEntry` list from the
|
||
* account's signer list SLE. Every signer in the transaction must
|
||
* appear in the account's signer list and pass key verification.
|
||
* Returns `tefBAD_QUORUM` if the accumulated weight is below
|
||
* `sfSignerQuorum`.
|
||
*
|
||
* @param view Read-only ledger view.
|
||
* @param flags Apply flags (used for dry-run simulation handling).
|
||
* @param id The account whose signer list governs authorization.
|
||
* @param sigObject The STObject containing `sfSigners`.
|
||
* @param j Journal for trace logging.
|
||
* @return `tesSUCCESS`, `tefNOT_MULTI_SIGNING`, `tefBAD_SIGNATURE`,
|
||
* `tefMASTER_DISABLED`, or `tefBAD_QUORUM`.
|
||
*/
|
||
static NotTEC
|
||
checkMultiSign(
|
||
ReadView const& view,
|
||
ApplyFlags flags,
|
||
AccountID const& id,
|
||
STObject const& sigObject,
|
||
beast::Journal const j);
|
||
|
||
/** Named breakpoint for replaying specific transactions under a debugger.
|
||
*
|
||
* Does nothing except log at debug level. Set a breakpoint here to
|
||
* pause execution when a specific transaction (identified by its hash
|
||
* in the service registry's trap configuration) is being applied.
|
||
*/
|
||
void trapTransaction(uint256) const;
|
||
|
||
/** Early sanity checks on the account field, fee field, and flags.
|
||
*
|
||
* Called as step 3 of `invokePreflight<T>` (after
|
||
* `checkExtraFeatures`, before `T::preflight`). Validates:
|
||
* - `sfDelegate` presence (requires `featurePermissionDelegationV1_1`)
|
||
* - `preflight0` (network ID, txid, flags via `flagMask`)
|
||
* - `sfAccount` is non-zero
|
||
* - `sfFee` is native XRP and non-negative
|
||
* - signing key format
|
||
* - ticket / AccountTxnID mutual exclusivity
|
||
* - `tfInnerBatchTxn` requires `featureBatch`
|
||
*
|
||
* @note Do not call this from `preflight()` in derived classes. It is
|
||
* invoked automatically by `invokePreflight<T>`.
|
||
*
|
||
* @param ctx Preflight context.
|
||
* @param flagMask Bitmask of valid flags from `T::getFlagsMask()`.
|
||
* @return `tesSUCCESS` or a `tem*` / `tel*` error.
|
||
*/
|
||
static NotTEC
|
||
preflight1(PreflightContext const& ctx, std::uint32_t flagMask);
|
||
|
||
/** Validate the cryptographic signature via the hash-router cache.
|
||
*
|
||
* Called as step 5 of `invokePreflight<T>` (after `T::preflight`,
|
||
* before `T::preflightSigValidated`). Skips the check entirely for
|
||
* batch inner transactions (`tfInnerBatchTxn` + `featureBatch`) since
|
||
* they are authorized by the outer batch's signature. For simulation
|
||
* (`TapDryRun`), validates key/signer consistency but skips
|
||
* cryptographic verification.
|
||
*
|
||
* @note Do not call this from `preflight()` in derived classes. It is
|
||
* invoked automatically by `invokePreflight<T>`.
|
||
*
|
||
* @param ctx Preflight context.
|
||
* @return `tesSUCCESS` or `temINVALID`.
|
||
*/
|
||
static NotTEC
|
||
preflight2(PreflightContext const& ctx);
|
||
|
||
/** Check transaction-specific invariants only.
|
||
*
|
||
* Walks every modified ledger entry via visitInvariantEntry, then
|
||
* calls finalizeInvariants on the derived transactor. Returns
|
||
* tecINVARIANT_FAILED if any transaction invariant is violated.
|
||
*
|
||
* @param result the tentative TER from transaction processing.
|
||
* @param fee the fee consumed by the transaction.
|
||
*
|
||
* @return the original result if all invariants pass, or
|
||
* tecINVARIANT_FAILED otherwise.
|
||
*/
|
||
[[nodiscard]] TER
|
||
checkTransactionInvariants(TER result, XRPAmount fee);
|
||
};
|
||
|
||
inline bool
|
||
Transactor::checkExtraFeatures(PreflightContext const& ctx)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
/** Early sanity checks on the transaction ID, network ID, and flag bits.
|
||
*
|
||
* The very first check in the preflight pipeline, called from `preflight1`.
|
||
* Validates:
|
||
* - Pseudo-transactions may not carry `tfInnerBatchTxn`.
|
||
* - `sfNetworkID` presence/absence rules: legacy networks (ID ≤ 1024) must
|
||
* not include `sfNetworkID`; newer networks must include it and it must
|
||
* match the local node.
|
||
* - Transaction ID must not be all-zeros.
|
||
* - No flag bits outside `flagMask` may be set.
|
||
*
|
||
* @param ctx Preflight context.
|
||
* @param flagMask Bitmask of valid flags for this transaction type.
|
||
* @return `tesSUCCESS` or a `tel*` / `tem*` error.
|
||
*/
|
||
NotTEC
|
||
preflight0(PreflightContext const& ctx, std::uint32_t flagMask);
|
||
|
||
namespace detail {
|
||
|
||
/** Validate the format of the signing public key in a transaction or signer.
|
||
*
|
||
* Returns `temBAD_SIGNATURE` if the `sfSigningPubKey` field is non-empty
|
||
* but not a recognized key type (secp256k1 or Ed25519). An empty key is
|
||
* valid (indicates multi-signing or batch inner transaction).
|
||
*
|
||
* Called from `preflight1` with the transaction object.
|
||
*
|
||
* @param sigObject The STObject containing `sfSigningPubKey`.
|
||
* @param j Journal for debug logging.
|
||
* @return `tesSUCCESS` or `temBAD_SIGNATURE`.
|
||
*/
|
||
NotTEC
|
||
preflightCheckSigningKey(STObject const& sigObject, beast::Journal j);
|
||
|
||
/** Validate signing-key state for dry-run simulation transactions.
|
||
*
|
||
* Called from `preflight2` when `TapDryRun` is set. A simulation
|
||
* transaction is valid if it has neither a signature nor a multi-signer
|
||
* list, or if it uses multi-signers with empty individual signatures.
|
||
* Returns `std::nullopt` when `TapDryRun` is not set (the caller should
|
||
* proceed to normal signature verification).
|
||
*
|
||
* @param flags Apply flags; must have `TapDryRun` set to take effect.
|
||
* @param sigObject The transaction's STObject.
|
||
* @param j Journal for debug logging.
|
||
* @return `tesSUCCESS` or `temINVALID` if the simulation keys are
|
||
* inconsistent; `std::nullopt` if not in simulation mode.
|
||
*/
|
||
std::optional<NotTEC>
|
||
preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::Journal j);
|
||
} // namespace detail
|
||
|
||
/** Explicit preflight specialization for `Change` pseudo-transactions.
|
||
*
|
||
* `Change` is a validator-generated pseudo-transaction with no real sender;
|
||
* its preflight logic is entirely different from normal transactions.
|
||
* Defined in `Change.cpp`.
|
||
*/
|
||
template <>
|
||
NotTEC
|
||
Transactor::invokePreflight<Change>(PreflightContext const& ctx);
|
||
|
||
template <class T>
|
||
NotTEC
|
||
Transactor::invokePreflight(PreflightContext const& ctx)
|
||
{
|
||
// Using this lookup does NOT require checking the fixDelegateV1_1. The data
|
||
// exists regardless of whether it is enabled.
|
||
auto const feature = Permission::getInstance().getTxFeature(ctx.tx.getTxnType());
|
||
|
||
if (feature && !ctx.rules.enabled(*feature))
|
||
return temDISABLED;
|
||
|
||
if (!T::checkExtraFeatures(ctx))
|
||
return temDISABLED;
|
||
|
||
if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx)))
|
||
return ret;
|
||
|
||
if (auto const ret = T::preflight(ctx))
|
||
return ret;
|
||
|
||
if (auto const ret = preflight2(ctx))
|
||
return ret;
|
||
|
||
return T::preflightSigValidated(ctx);
|
||
}
|
||
|
||
template <class T>
|
||
bool
|
||
Transactor::validNumericRange(std::optional<T> value, T max, T min)
|
||
{
|
||
if (!value)
|
||
return true;
|
||
return value >= min && value <= max;
|
||
}
|
||
|
||
template <class T, class Unit>
|
||
bool
|
||
Transactor::validNumericRange(
|
||
std::optional<T> value,
|
||
unit::ValueUnit<Unit, T> max,
|
||
unit::ValueUnit<Unit, T> min)
|
||
{
|
||
return validNumericRange(value, max.value(), min.value());
|
||
}
|
||
|
||
template <class T>
|
||
bool
|
||
Transactor::validNumericMinimum(std::optional<T> value, T min)
|
||
{
|
||
if (!value)
|
||
return true;
|
||
return value >= min;
|
||
}
|
||
|
||
template <class T, class Unit>
|
||
bool
|
||
Transactor::validNumericMinimum(std::optional<T> value, unit::ValueUnit<Unit, T> min)
|
||
{
|
||
return validNumericMinimum(value, min.value());
|
||
}
|
||
|
||
} // namespace xrpl
|