refactor: Extract invariant invocation into free checkInvariants runner (#7404)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vito Tumas
2026-08-19 14:10:11 +00:00
committed by GitHub
parent 5639863715
commit d1dc7a6ccf
8 changed files with 465 additions and 175 deletions

View File

@@ -17,7 +17,6 @@
#include <cstddef>
#include <functional>
#include <optional>
#include <utility>
namespace xrpl {
@@ -130,16 +129,6 @@ public:
view_->rawDestroyXRP(fee);
}
/**
* Applies all invariant checkers one by one.
*
* @param result the result generated by processing this transaction.
* @param fee the fee charged for this transaction
* @return the result code that should be returned for this transaction.
*/
TER
checkInvariants(TER const result, XRPAmount const fee);
ApplyViewContext
getApplyViewContext()
{
@@ -150,13 +139,6 @@ public:
}
private:
static TER
failInvariantCheck(TER const result);
template <std::size_t... Is>
TER
checkInvariantsHelper(TER const result, XRPAmount const fee, std::index_sequence<Is...>);
OpenView& base_;
ApplyFlags flags_;
std::optional<ApplyViewImpl> view_;

View File

@@ -20,6 +20,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <cstddef>
#include <cstdint>
@@ -147,7 +148,7 @@ struct FeePayer
FeePayerType type{FeePayerType::Account};
};
class Transactor
class Transactor : public TxInvariantCheck
{
protected:
ApplyContext& ctx_;
@@ -158,7 +159,7 @@ protected:
XRPAmount preFeeBalance_{}; // Balance before fees.
public:
virtual ~Transactor() = default;
~Transactor() override = default;
Transactor(Transactor const&) = delete;
Transactor&
operator=(Transactor const&) = delete;
@@ -183,20 +184,50 @@ public:
return ctx_.view();
}
/**
* Which invariant layers to check.
*
* Full runs the protocol invariants plus the transaction-specific
* check. This is always the scope of the initial pass, even when the
* tentative TER is a tec: a bug or exploit could still mutate ledger
* state, so transaction-specific invariants must run for failed
* transactions too.
*
* ProtocolOnly runs only the protocol invariants and is used
* exclusively for the second invariant pass that follows a
* fee-claim reset — specifically, the reset that
* Transactor::operator() performs when the initial invariant pass
* returns tecINVARIANT_FAILED, rolling the transaction's effects back
* to a fee-claim-only state. In that reduced state the
* transaction-specific post-conditions no longer apply, but the
* protocol invariants must still hold against the fee claim itself.
* ProtocolOnly is not intended for other context discards (e.g. the
* reset used to handle tecOVERSIZE/tecKILLED/etc. in
* processPersistentChanges, or the ctx_.discard() done under
* TapFailHard); those paths do not re-run invariants at all.
*/
enum class InvariantScope { Full, ProtocolOnly };
/**
* 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.
* Delegates to the free xrpl::checkInvariants runner. When @p scope is
* InvariantScope::Full, this transactor is passed so both layers
* share a single walk of the modified ledger entries. A failure in
* either layer fails the transaction the same way: tecINVARIANT_FAILED
* on the first pass, which the caller may respond to by rolling the
* transaction back to a fee-claim state and re-invoking this with
* InvariantScope::ProtocolOnly; a failure on that post-reset pass
* escalates to tefINVARIANT_FAILED.
*
* @param result the tentative TER from transaction processing.
* @param fee the fee consumed by the transaction.
* @param scope which invariant layers to check.
*
* @return the final TER after all invariant checks.
*/
[[nodiscard]] TER
checkInvariants(TER result, XRPAmount fee);
checkInvariants(TER result, XRPAmount fee, InvariantScope scope);
/////////////////////////////////////////////////////
/*
@@ -538,20 +569,30 @@ private:
preflightUniversal(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.
* Bridges the two-phase TxInvariantCheck interface to this transactor's
* visitInvariantEntry/finalizeInvariants hooks. Declared private (rather
* than protected, like the hooks they forward to) so that neither this
* transactor nor any subclass can call them directly through a
* Transactor& — only through the TxInvariantCheck& that the free
* xrpl::checkInvariants runner holds, which is where the two-phase
* ordering is enforced.
*/
[[nodiscard]] TER
checkTransactionInvariants(TER result, XRPAmount fee);
void
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) final
{
visitInvariantEntry(isDelete, before, after);
}
[[nodiscard]] bool
finalize(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) final
{
return finalizeInvariants(tx, result, fee, view, j);
}
};
inline bool

View File

@@ -0,0 +1,140 @@
#pragma once
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <functional>
#include <optional>
namespace xrpl {
/**
* @brief Runtime interface for a transaction-specific invariant check.
*
* The free checkInvariants runner drives two layers of checks over a single
* walk of the modified ledger entries:
*
* - Protocol checks are the concrete types in InvariantChecks, held in a
* std::tuple and dispatched statically by a compile-time fold (no
* virtual calls). They are duck-typed against the two-phase contract
* described below; see InvariantChecker_PROTOTYPE in InvariantCheck.h.
* - The transaction-specific check is injected at runtime through this
* interface, so the runner can call it without depending on the concrete
* transactor type. Transactor implements this interface directly (see
* Transactor.h) so that the interface's access can stay narrower than
* Transactor's own public surface: calling through a TxInvariantCheck&
* (all the runner ever holds) is public, but calling through a
* Transactor& is not, since Transactor overrides these as private
* (forwarding to its own protected visitInvariantEntry/finalizeInvariants).
*
* Both layers honour the same two-phase protocol:
*
* Phase 1 — state collection (visitEntry). Called once for each ledger
* entry created, modified, or deleted by the transaction. Implementations
* accumulate whatever state they need to evaluate their post-conditions.
* Must not throw.
*
* Phase 2 — condition evaluation (finalize). Called once after every
* modified entry has been visited. Returns true if all post-conditions
* hold, false to fail the transaction.
*
* Rule: invariants must run regardless of transaction result. finalize
* MUST perform meaningful checks even when the transaction has failed
* (when result is not tesSUCCESS). A bug or exploit could cause a failed
* transaction to mutate ledger state in unexpected ways; invariants are the
* last line of defense.
*
* The typical pattern: an invariant that expects a domain-specific state
* change (e.g. a Vault being created) should expect that change only when
* the transaction succeeded. A failed VaultCreate must not have created a
* Vault.
*
* Rule: privilege-gated checks apply to failed transactions too. Failed
* transactions carry no privileges. Any privilege-gated assertion must
* therefore also be enforced for failed transactions.
*/
class TxInvariantCheck
{
public:
virtual ~TxInvariantCheck() = default;
/**
* @brief Called for each ledger entry modified by the transaction.
*
* @param isDelete true if the SLE is being deleted.
* @param before the entry's state before the transaction (nullptr for
* newly created entries).
* @param after the entry's state after the transaction. For deletions
* this is the SLE being erased; use @p isDelete rather than
* a null @p after to detect deletions. @p after is
* never null.
*/
virtual void
visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) = 0;
/**
* @brief Called after all entries have been visited.
*
* @param tx the transaction being applied.
* @param result the tentative TER result of the transaction.
* @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 hold; false to fail with
* tecINVARIANT_FAILED / tefINVARIANT_FAILED.
*/
[[nodiscard]] virtual bool
finalize(
STTx const& tx,
TER result,
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) = 0;
};
/**
* @brief Run all protocol invariant checks plus the transaction-specific check
* in a single pass over the modified entries.
*
* Both layers share one walk of the modified-entry set: @p txCheck's
* visitEntry accumulates state on the same traversal that drives the
* protocol checkers, then both layers' finalize run on the complete state.
*
* Any failure (a finalize returning false or an exception anywhere in the
* check) returns failInvariantCheck(result). On the first pass that yields
* tecINVARIANT_FAILED, which the transactor treats as a signal to roll the
* transaction's effects back to a fee-claim-only state and re-run this
* runner against the reduced state (see Transactor::InvariantScope). If
* that second pass also fails, the result escalates to tefINVARIANT_FAILED,
* which excludes the transaction from the ledger entirely.
*
* The whole traversal — both layers' visitEntry calls and both layers'
* finalize calls — runs under a single try/catch. There is no per-layer
* isolation: an exception anywhere aborts the remaining traversal and
* finalize calls and fails the transaction.
*
* @param ctx the apply context for the current transaction.
* @param result the tentative TER from transaction processing.
* @param fee the fee consumed by the transaction.
* @param txCheck the transaction-specific invariant check.
* @return the final TER after all invariant checks.
*/
[[nodiscard]] TER
checkInvariants(
ApplyContext& ctx,
TER result,
XRPAmount fee,
std::optional<std::reference_wrapper<TxInvariantCheck>> txCheck);
[[nodiscard]] inline TER
checkInvariants(ApplyContext& ctx, TER result, XRPAmount fee)
{
return checkInvariants(ctx, result, fee, std::nullopt);
}
} // namespace xrpl