Compare commits

...
Author SHA1 Message Date
Vito 358e468b8b feat: Move VaultCreate per-transactor invariants out of ValidVault
Introduces VaultInvariantData, a shared data-collection class that
captures vault and share-issuance snapshots from visitEntry deltas.
ValidVault::finalize delegates all data collection to an instance of
this class and skips the ttVAULT_CREATE switch case (now handled by
VaultCreate::finalizeInvariants).

VaultCreate gains visitInvariantEntry / finalizeInvariants / invariantData_
members. finalizeInvariants checks create-specific invariants (vault must be
a fresh insert, must be empty, share issuance must exist and point to the
correct pseudo-account) before the universal ValidVault checks run.

Share resolution is now key-based (SLE key stored in Shares::sleKey) so
that tampered sfIssuer fields do not prevent finding the issuance object
the vault points to via sfShareMPTID.
2026-07-29 12:53:35 +02:00
Vito 71381b7852 refactor: Have Transactor implement TxInvariantCheck directly
Restores a runtime interface (renamed InvariantCheck -> TxInvariantCheck
per review feedback) that Transactor now inherits from, rather than the
free checkInvariants runner depending on Transactor directly. Transactor
implements visitEntry/finalize as private final overrides forwarding to
its own protected visitInvariantEntry/finalizeInvariants, so the two-phase
hooks stay unreachable through a bare Transactor&, only through the
TxInvariantCheck& the runner holds. Adds a test exercising the runner's
exception path via a throwing TxInvariantCheck, and fixes two inaccurate
doc comments (checkInvariants' stale escalation description, and
getInvariantChecks() construction moved back inside the try block).
2026-07-29 12:40:21 +02:00
Vito fd914e1182 refactor: Drop InvariantCheckAdapter, dispatch tx invariants directly
Address open PR #7404 review feedback:
- Rename CheckInvariants.h/.cpp to InvariantRunner.h/.cpp; the old name
  read as invariants for the Checks (ttCHECK*) feature.
- Remove the InvariantCheck interface and Transactor::InvariantCheckAdapter
  wrapper. The free checkInvariants runner now takes an optional
  Transactor& directly and calls visitInvariantEntry/finalizeInvariants on
  it, since the adapter only existed to bridge to a single concrete type.
2026-07-29 11:30:11 +02:00
Vito Tumas 1135dcc1e7 Merge branch 'develop' into tapanito/invariant-improvement 2026-07-29 11:30:07 +02:00
Vito 0c8701c43f clang-tidy 2026-07-23 15:04:06 +02:00
Vito c8a366f8da Merge remote-tracking branch 'origin/develop' into tapanito/invariant-improvement 2026-07-23 14:55:37 +02:00
Vito 070545b5f2 simplifies invariant invocation 2026-06-30 14:20:16 +02:00
Vito e8dc999a82 refactor: Address review feedback on CheckInvariants (#7404)
- Document that `after` is never null in InvariantCheck::visitEntry
- Clarify tec/tef escalation in checkInvariants doc: both protocol and
  transaction faults return tecINVARIANT_FAILED on the first pass;
  tefINVARIANT_FAILED only occurs when the incoming result is already
  tecINVARIANT_FAILED (i.e. the fee-claim reset path checks again)
- Rename SkipTxInvariants -> CheckTxInvariants with Yes/No semantics
  to eliminate the double-negative at call sites
2026-06-24 14:34:44 +02:00
Vito f1145a4bfe Merge remote-tracking branch 'origin/develop' into tapanito/invariant-improvement 2026-06-24 14:27:39 +02:00
Vito bdb2c5f096 clang-tidy 2026-06-09 13:56:01 +02:00
Vito Tumas 17a7d64900 Merge branch 'develop' into tapanito/invariant-improvement 2026-06-09 13:54:48 +02:00
Vito 67037782af refactor: Extract invariant invocation into free checkInvariants runner
Move the invariant-check orchestration out of ApplyContext and Transactor
into a free function xrpl::checkInvariants(ApplyContext&, TER, XRPAmount,
optional<reference_wrapper<InvariantCheck>>).

The two previously separate traversals (one in ApplyContext driving the
protocol tuple fold, one in Transactor driving the tx-specific check) are
merged into a single ctx.visit walk.  Per-layer try/catch inside the
lambda isolates collection faults: a throw in one layer stops only that
layer from visiting further entries while the other continues.  A layer
whose collection faulted skips its finalize phase.

ApplyContext loses checkInvariants/checkInvariantsHelper/failInvariantCheck.
Transactor delegates to the free runner via a private InvariantCheckAdapter
that bridges visitInvariantEntry+finalizeInvariants into the InvariantCheck
interface.  A SkipTxInvariants::Yes/No enum makes the fee-claim-reset call
site explicit about omitting the tx-specific check.

Protocol checks remain duck-typed in the InvariantChecks tuple (static
dispatch, no vtable on the hot path).  InvariantCheck is the runtime
interface used only by InvariantCheckAdapter.
2026-06-09 13:16:37 +02:00
14 changed files with 1000 additions and 701 deletions

No files matched your search

-18
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_;
+43 -19
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,33 @@ public:
return ctx_.view();
}
/**
* Whether to run the transaction-specific invariant check.
*
* After a fee-claim reset the transaction's effects have been rolled back,
* so only the protocol invariants are meaningful; pass @c No to omit the
* transaction-specific check in that case.
*/
enum class CheckTxInvariants : bool { No = false, Yes = true };
/**
* 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 @c xrpl::checkInvariants runner. When @p check is
* @c CheckTxInvariants::Yes, 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, escalating to tefINVARIANT_FAILED if invariants are
* checked again after a fee-claim reset.
*
* @param result the tentative TER from transaction processing.
* @param fee the fee consumed by the transaction.
* @param check whether to include the transaction-specific invariant check.
*
* @return the final TER after all invariant checks.
*/
[[nodiscard]] TER
checkInvariants(TER result, XRPAmount fee);
checkInvariants(TER result, XRPAmount fee, CheckTxInvariants check);
/////////////////////////////////////////////////////
/*
@@ -538,20 +552,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
* @c Transactor& — only through the @c TxInvariantCheck& that the free
* @c 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
@@ -0,0 +1,142 @@
#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 @c checkInvariants runner drives two layers of checks over a single
* walk of the modified ledger entries:
*
* - **Protocol checks** are the concrete types in @c InvariantChecks, held in
* a @c 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 @c 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. @c 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 @c TxInvariantCheck&
* (all the runner ever holds) is public, but calling through a
* @c 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.
*
* ## Rules for implementing `finalize`
*
* ### Invariants must run regardless of transaction result
*
* `finalize` MUST perform meaningful checks even when the transaction has
* failed (`!isTesSuccess(result)`). 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.
*
* ### 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
* `after == nullptr` 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 @c failInvariantCheck(result). On the first pass that yields
* @c tecINVARIANT_FAILED. If that triggers a fee-claim reset and invariants
* are checked again, a second failure escalates to @c 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
+4 -126
View File
@@ -1,23 +1,13 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <optional>
#include <unordered_map>
#include <vector>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
namespace xrpl {
@@ -42,127 +32,15 @@ namespace xrpl {
*/
class ValidVault
{
static constexpr Number kZero{};
struct Vault final
{
uint256 key = beast::kZero;
Asset asset;
AccountID pseudoId;
AccountID owner;
uint192 shareMPTID = beast::kZero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
Vault static make(SLE const&);
};
struct Shares final
{
MPTIssue share;
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
Shares static make(SLE const&);
};
public:
struct DeltaInfo final
{
Number delta = kNumZero;
std::optional<int> scale;
// Compute the delta between two Numbers, taking the coarsest scale
[[nodiscard]] static DeltaInfo
makeDelta(Number const& before, Number const& after, Asset const& asset);
};
private:
std::vector<Vault> afterVault_;
std::vector<Shares> afterMPTs_;
std::vector<Vault> beforeVault_;
std::vector<Shares> beforeMPTs_;
std::unordered_map<uint256, DeltaInfo> deltas_;
/**
* @brief Compute the minimum STAmount scale for rounding invariant
* calculations.
*
* Post-amendment (@c fixCleanup3_2_0) this is simply the posterior
* @c assetsTotal scale. Pre-amendment it is the coarsest scale across
* @p vaultDelta and both asset-field deltas.
*
* @param vaultDelta Delta of the vault's asset balance for this transaction.
* @param rules Active ledger rules (used to check the amendment).
* @return The minimum scale to apply when rounding vault-related amounts.
*/
[[nodiscard]] std::int32_t
computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const;
/**
* @brief Return the vault-asset balance-change delta for an account.
*
* Looks up the ledger-entry delta recorded during @c visitEntry for the
* account entry (XRP), trust line (IOU), or MPToken (MPT) that corresponds
* to the vault asset held by @p id.
*
* @param id Account whose asset delta is requested.
* @return The delta, or @c std::nullopt if the entry was not touched.
*/
[[nodiscard]] std::optional<DeltaInfo>
deltaAssets(AccountID const& id) const;
/**
* @brief Return the vault-asset delta for the transaction's sending
* account, adjusted for the fee.
*
* Calls @c deltaAssets for @c tx[sfAccount] and, for non-delegated XRP
* transactions, adds the consumed fee back so the invariant sees the net
* asset movement rather than the fee-reduced balance change.
*
* @param tx The transaction being applied.
* @param fee Fee charged by this transaction.
* @return The fee-adjusted delta, or @c std::nullopt if the net delta is
* zero or the account entry was not touched.
*/
[[nodiscard]] std::optional<DeltaInfo>
deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const;
/**
* @brief Return the vault-share balance-change delta for an account.
*
* For the vault's pseudo-account the @c MPTokenIssuance outstanding-amount
* delta is returned; for all other accounts the @c MPToken delta is
* returned.
*
* @param id Account whose share delta is requested.
* @return The delta, or @c std::nullopt if the entry was not touched.
*/
[[nodiscard]] std::optional<DeltaInfo>
deltaShares(AccountID const& id) const;
/**
* @brief Check whether a vault holds no assets.
*
* @param vault Snapshot of the vault to test.
* @return @c true when both @c assetsAvailable and @c assetsTotal are
* zero.
*/
[[nodiscard]] static bool
isVaultEmpty(Vault const& vault);
public:
// Compute the coarsest scale required to represent all numbers
[[nodiscard]] static std::int32_t
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
VaultInvariantData data_;
};
} // namespace xrpl
@@ -8,6 +8,7 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
#include <cstdint>
@@ -47,6 +48,9 @@ public:
XRPAmount fee,
ReadView const& view,
beast::Journal const& j) override;
private:
VaultInvariantData invariantData_;
};
} // namespace xrpl
@@ -0,0 +1,127 @@
#pragma once
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/XRPAmount.h>
#include <optional>
#include <unordered_map>
#include <vector>
namespace xrpl {
/**
* @brief Collects vault-related ledger-entry snapshots and balance-change
* deltas for use in per-transactor invariant checks.
*
* Each vault transactor that performs per-transactor invariant checking holds
* one instance of this class as a private member. During
* @c visitInvariantEntry the transactor calls @c visitEntry for every touched
* SLE; during @c finalizeInvariants it queries the collected data via the
* accessor and helper methods below.
*/
class VaultInvariantData
{
public:
struct Vault final
{
uint256 key = beast::kZero;
Asset asset;
AccountID pseudoId;
AccountID owner;
uint192 shareMPTID = beast::kZero;
Number assetsTotal = 0;
Number assetsAvailable = 0;
Number assetsMaximum = 0;
Number lossUnrealized = 0;
[[nodiscard]] static Vault
make(SLE const&);
};
struct Shares final
{
uint256 sleKey = beast::kZero;
MPTIssue share;
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
[[nodiscard]] static Shares
make(SLE const&);
};
struct DeltaInfo final
{
Number delta = kNumZero;
std::optional<int> scale;
[[nodiscard]] static DeltaInfo
makeDelta(Number const& before, Number const& after, Asset const& asset);
};
// Feed a single SLE change into the collected data.
void
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
// Snapshot accessors -------------------------------------------------------
[[nodiscard]] std::vector<Vault> const&
afterVault() const
{
return afterVault_;
}
[[nodiscard]] std::vector<Vault> const&
beforeVault() const
{
return beforeVault_;
}
// Search afterMPTs_ for the share issuance matching @p afterVault.
[[nodiscard]] std::optional<Shares>
resolveUpdatedShares(Vault const& afterVault) const;
// Search beforeMPTs_ for the share issuance matching @p beforeVault.
[[nodiscard]] std::optional<Shares>
resolveBeforeShares(Vault const& beforeVault) const;
// Delta helpers ------------------------------------------------------------
[[nodiscard]] std::optional<DeltaInfo>
deltaAssets(AccountID const& id) const;
[[nodiscard]] std::optional<DeltaInfo>
deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const;
[[nodiscard]] std::optional<DeltaInfo>
deltaShares(AccountID const& id) const;
// Utilities ----------------------------------------------------------------
[[nodiscard]] static bool
isVaultEmpty(Vault const& vault);
[[nodiscard]] static std::int32_t
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
[[nodiscard]] std::int32_t
computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const;
private:
std::vector<Vault> afterVault_;
std::vector<Shares> afterMPTs_;
std::vector<Vault> beforeVault_;
std::vector<Shares> beforeMPTs_;
std::unordered_map<uint256, DeltaInfo> deltas_;
};
} // namespace xrpl
-79
View File
@@ -1,27 +1,19 @@
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxMeta.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheck.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <exception>
#include <functional>
#include <optional>
#include <tuple>
#include <utility>
namespace xrpl {
@@ -75,75 +67,4 @@ ApplyContext::visit(
view_->visit(base_, func); // NOLINT(bugprone-unchecked-optional-access)
}
TER
ApplyContext::failInvariantCheck(TER const result)
{
// If we already failed invariant checks before and we are now attempting to
// only charge a fee, and even that fails the invariant checks something is
// very wrong. We switch to tefINVARIANT_FAILED, which does NOT get included
// in a ledger.
return (result == tecINVARIANT_FAILED || result == tefINVARIANT_FAILED)
? TER{tefINVARIANT_FAILED}
: TER{tecINVARIANT_FAILED};
}
template <std::size_t... Is>
TER
ApplyContext::checkInvariantsHelper(
TER const result,
XRPAmount const fee,
std::index_sequence<Is...>)
{
try
{
auto checkers = getInvariantChecks();
// call each check's per-entry method
visit(
[&checkers](
uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
(..., std::get<Is>(checkers).visitEntry(isDelete, before, after));
});
// Note: do not replace this logic with a `...&&` fold expression.
// The fold expression will only run until the first check fails (it
// short-circuits). While the logic is still correct, the log
// message won't be. Every failed invariant should write to the log,
// not just the first one.
std::array<bool, sizeof...(Is)> const finalizers{{std::get<Is>(checkers).finalize(
tx, result, fee, *view_, journal)...}}; // NOLINT(bugprone-unchecked-optional-access)
// call each check's finalizer to see that it passes
if (!std::ranges::all_of(finalizers, [](auto const& b) { return b; }))
{
JLOG(journal.fatal()) << "Transaction has failed one or more global invariants: "
<< to_string(tx.getJson(JsonOptions::Values::None));
return failInvariantCheck(result);
}
}
catch (std::exception const& ex)
{
JLOG(journal.fatal()) << "Transaction caused an exception in a global invariant"
<< ", ex: " << ex.what()
<< ", tx: " << to_string(tx.getJson(JsonOptions::Values::None));
return failInvariantCheck(result);
}
return result;
}
TER
ApplyContext::checkInvariants(TER const result, XRPAmount const fee)
{
XRPL_ASSERT(
isTesSuccess(result) || isTecClaim(result),
"xrpl::ApplyContext::checkInvariants : is tesSUCCESS or tecCLAIM");
return checkInvariantsHelper(
result, fee, std::make_index_sequence<std::tuple_size_v<InvariantChecks>>{});
}
} // namespace xrpl
+10 -51
View File
@@ -41,11 +41,11 @@
#include <xrpl/tx/SignerEntries.h>
#include <xrpl/tx/apply.h>
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <map>
#include <optional>
#include <stdexcept>
@@ -1538,53 +1538,12 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
}
[[nodiscard]] TER
Transactor::checkTransactionInvariants(TER result, XRPAmount fee)
Transactor::checkInvariants(TER result, XRPAmount fee, CheckTxInvariants check)
{
try
{
// Phase 1: visit modified entries
ctx_.visit(
[this](uint256 const&, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
this->visitInvariantEntry(isDelete, before, after);
});
if (check == CheckTxInvariants::No)
return xrpl::checkInvariants(ctx_, result, fee);
// Phase 2: finalize
if (!this->finalizeInvariants(ctx_.tx, result, fee, ctx_.view(), ctx_.journal))
{
JLOG(ctx_.journal.fatal()) << //
"Transaction has failed one or more transaction invariants, tx: " << //
to_string(ctx_.tx.getJson(JsonOptions::Values::None));
return tecINVARIANT_FAILED;
}
}
catch (std::exception const& ex)
{
JLOG(ctx_.journal.fatal()) << //
"Exception while checking transaction invariants: " << //
ex.what() << //
", tx: " << //
to_string(ctx_.tx.getJson(JsonOptions::Values::None));
return tecINVARIANT_FAILED;
}
return result;
}
[[nodiscard]] TER
Transactor::checkInvariants(TER result, XRPAmount fee)
{
/*
* DISABLED for 3.2.0 — Must be re-introduced for 3.3.0
*
* Transaction invariants are disabled due to a performance regression:
* the two-pass design (transaction-specific invariants + protocol invariants)
* iterates over modified ledger entries twice per transaction.
*
* Until resolved, only protocol invariants are checked (delegated to ctx_).
* This is safe because all transaction invariants in 3.2.0 are no-ops.
*/
return ctx_.checkInvariants(result, fee);
return xrpl::checkInvariants(ctx_, result, fee, *this);
}
//------------------------------------------------------------------------------
@@ -1660,7 +1619,7 @@ Transactor::operator()()
{
// Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
// proceed to apply the tx
result = checkInvariants(result, fee);
result = checkInvariants(result, fee, CheckTxInvariants::Yes);
if (result == tecINVARIANT_FAILED)
{
// Reset to fee-claim only
@@ -1671,11 +1630,11 @@ Transactor::operator()()
fee = resetResult.second;
// Check invariants again to ensure the fee claiming doesn't violate
// invariants. After reset, only protocol invariants are re-checked.
// Transaction invariants are not meaningful here — the transaction's
// effects have been rolled back.
// invariants. After reset, only protocol invariants are re-checked;
// the transaction's effects have been rolled back, so the
// transaction-specific invariants are no longer meaningful.
if (isTesSuccess(result) || isTecClaim(result))
result = ctx_.checkInvariants(result, fee);
result = checkInvariants(result, fee, CheckTxInvariants::No);
}
// We ran through the invariant checker, which can, in some cases,
@@ -0,0 +1,110 @@
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/json/to_string.h> // IWYU pragma: keep
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/invariants/InvariantCheck.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <exception>
#include <functional>
#include <optional>
#include <tuple>
#include <utility>
namespace xrpl {
namespace {
TER
failInvariantCheck(TER const result)
{
return (result == tecINVARIANT_FAILED || result == tefINVARIANT_FAILED)
? TER{tefINVARIANT_FAILED}
: TER{tecINVARIANT_FAILED};
}
template <std::size_t... Is>
TER
checkInvariantsHelper(
ApplyContext& ctx,
TER const result,
XRPAmount const fee,
std::optional<std::reference_wrapper<TxInvariantCheck>> txCheck,
std::index_sequence<Is...>)
{
bool allOk = true;
try
{
auto checkers = getInvariantChecks();
ctx.visit([&](uint256 const&, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
if (txCheck)
txCheck->get().visitEntry(isDelete, before, after);
(..., std::get<Is>(checkers).visitEntry(isDelete, before, after));
});
if (txCheck)
{
if (!txCheck->get().finalize(ctx.tx, result, fee, ctx.view(), ctx.journal))
{
JLOG(ctx.journal.fatal())
<< "Transaction has failed one or more transaction invariants: "
<< to_string(ctx.tx.getJson(JsonOptions::Values::None));
allOk = false;
}
}
// Note: do not replace this logic with a `...&&` fold expression.
// The fold expression will only run until the first check fails (it
// short-circuits). While the logic is still correct, the log
// message won't be. Every failed invariant should write to the log,
// not just the first one.
std::array<bool, sizeof...(Is)> const finalizers{
{std::get<Is>(checkers).finalize(ctx.tx, result, fee, ctx.view(), ctx.journal)...}};
if (!std::all_of(finalizers.cbegin(), finalizers.cend(), [](auto const& b) { return b; }))
{
JLOG(ctx.journal.fatal()) << "Transaction has failed one or more global invariants: "
<< to_string(ctx.tx.getJson(JsonOptions::Values::None));
allOk = false;
}
}
catch (std::exception const& ex)
{
JLOG(ctx.journal.fatal()) << "Transaction caused an exception during invariant checks"
<< ", ex: " << ex.what() << ", tx: "
<< to_string(ctx.tx.getJson(JsonOptions::Values::None));
return failInvariantCheck(result);
}
return allOk ? result : failInvariantCheck(result);
}
} // namespace
TER
checkInvariants(
ApplyContext& ctx,
TER const result,
XRPAmount const fee,
std::optional<std::reference_wrapper<TxInvariantCheck>> txCheck)
{
XRPL_ASSERT(
isTesSuccess(result) || isTecClaim(result),
"xrpl::checkInvariants : is tesSUCCESS or tecCLAIM");
return checkInvariantsHelper(
ctx, result, fee, txCheck, std::make_index_sequence<std::tuple_size_v<InvariantChecks>>{});
}
} // namespace xrpl
+66 -396
View File
@@ -5,279 +5,34 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
#include <algorithm>
#include <cstdint>
#include <memory>
#include <optional>
#include <variant>
#include <vector>
namespace xrpl {
ValidVault::Vault
ValidVault::Vault::make(SLE const& from)
{
XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object");
static constexpr Number kZero{};
ValidVault::Vault self;
self.key = from.key();
self.asset = from.at(sfAsset);
self.pseudoId = from.getAccountID(sfAccount);
self.owner = from.at(sfOwner);
self.shareMPTID = from.getFieldH192(sfShareMPTID);
self.assetsTotal = from.at(sfAssetsTotal);
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
return self;
}
ValidVault::Shares
ValidVault::Shares::make(SLE const& from)
{
XRPL_ASSERT(
from.getType() == ltMPTOKEN_ISSUANCE,
"ValidVault::Shares::make : from MPTokenIssuance object");
ValidVault::Shares self;
self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
self.sharesTotal = from.at(sfOutstandingAmount);
self.sharesMaximum = from[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
return self;
}
using Vault = VaultInvariantData::Vault;
using Shares = VaultInvariantData::Shares;
using DeltaInfo = VaultInvariantData::DeltaInfo;
void
ValidVault::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
// If `before` is empty, this means an object is being created, in which
// case `isDelete` must be false. Otherwise `before` and `after` are set and
// `isDelete` indicates whether an object is being deleted or modified.
XRPL_ASSERT(
after != nullptr && (before != nullptr || !isDelete),
"xrpl::ValidVault::visitEntry : some object is available");
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), and
// preserves value scale (exponent) to round values to the same scale during
// validation. It is used to validate that the change in account
// balances matches the change in vault balances, stored to deltas_ at the
// end of this function.
DeltaInfo balanceDelta{.delta = kNumZero, .scale = std::nullopt};
std::int8_t sign = 0;
if (before)
{
switch (before->getType())
{
case ltVAULT:
beforeVault_.push_back(Vault::make(*before));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balanceDelta.delta =
static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta = before->getFieldAmount(sfBalance);
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = before->getFieldAmount(sfBalance);
balanceDelta.delta = amount;
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
if (!isDelete && after)
{
switch (after->getType())
{
case ltVAULT:
afterVault_.push_back(Vault::make(*after));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after));
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = after->getFieldAmount(sfBalance);
balanceDelta.delta -= Number(amount);
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
if (amount.exponent() > balanceDelta.scale)
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
uint256 const key = (before ? before->key() : after->key());
// Append to deltas if sign is non-zero, i.e. an object of an interesting
// type has been updated. A transaction may update an object even when
// its balance has not changed, e.g. transaction fee equals the amount
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
{
XRPL_ASSERT_PARTS(balanceDelta.scale, "xrpl::ValidVault::visitEntry", "scale initialized");
balanceDelta.delta *= sign;
deltas_[key] = balanceDelta;
}
}
std::optional<ValidVault::DeltaInfo>
ValidVault::deltaAssets(AccountID const& id) const
{
auto const& vaultAsset = afterVault_[0].asset;
auto const lookup = [&](uint256 const& key) -> std::optional<DeltaInfo> {
auto const it = deltas_.find(key);
if (it == deltas_.end())
return std::nullopt;
return it->second;
};
return std::visit(
[&]<typename TIss>(TIss const& issue) -> std::optional<DeltaInfo> {
if constexpr (std::is_same_v<TIss, Issue>)
{
if (isXRP(issue))
return lookup(keylet::account(id).key);
auto result = lookup(keylet::trustLine(id, issue).key);
// Trust-line balance is stored from the low-account's perspective;
// negate if id is the high account so the delta is in id's terms.
if (result && id > issue.getIssuer())
result->delta = -result->delta;
return result;
}
else if constexpr (std::is_same_v<TIss, MPTIssue>)
{
return lookup(keylet::mptoken(issue.getMptID(), id).key);
}
},
vaultAsset.value());
}
std::optional<ValidVault::DeltaInfo>
ValidVault::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const
{
auto const& vaultAsset = afterVault_[0].asset;
auto ret = deltaAssets(tx[sfAccount]);
if (!ret.has_value() || !vaultAsset.native())
return ret;
// Only add the fee back if tx[sfAccount] actually paid it. When the fee is
// paid by someone else (a delegate or a fee sponsor), the
// account's XRP balance moved only by the vault amount.
if (tx.getFeePayerID() != tx[sfAccount])
return ret;
ret->delta += fee.drops();
if (ret->delta == kZero)
return std::nullopt;
return ret;
}
std::optional<ValidVault::DeltaInfo>
ValidVault::deltaShares(AccountID const& id) const
{
auto const& afterVault = afterVault_[0];
auto const it = [&]() {
if (id == afterVault.pseudoId)
return deltas_.find(keylet::mptokenIssuance(afterVault.shareMPTID).key);
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
}();
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
}
bool
ValidVault::isVaultEmpty(Vault const& vault)
{
return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
}
std::int32_t
ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const
{
// Returns the posterior `assetsTotal` scale.
//
// 1. Because STAmounts are normalized, `assetsTotal` (being >= `assetsAvailable`)
// safely represents the coarsest exponent needed for both fields.
//
// 2. The scale may decrease (withdraw/clawback) or increase (deposit). In both cases
// we ensure the vault is in a legitimate state in the post-transaction scale.
auto const& afterVault = afterVault_[0];
auto const& vaultAsset = afterVault.asset;
if (rules.enabled(fixCleanup3_2_0))
{
NumberRoundModeGuard const roundGuard(Number::RoundingMode::ToNearest);
return scale(afterVault.assetsTotal, vaultAsset);
}
auto const& beforeVault = beforeVault_[0];
auto const totalDelta =
DeltaInfo::makeDelta(beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
auto const availableDelta =
DeltaInfo::makeDelta(beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
data_.visitEntry(isDelete, before, after);
}
bool
@@ -293,7 +48,7 @@ ValidVault::finalize(
if (!isTesSuccess(ret))
return true; // Do not perform checks
if (afterVault_.empty() && beforeVault_.empty())
if (data_.afterVault().empty() && data_.beforeVault().empty())
{
if (hasPrivilege(tx, MustModifyVault))
{
@@ -317,7 +72,7 @@ ValidVault::finalize(
return !enforce; // Also not a vault operation
}
if (beforeVault_.size() > 1 || afterVault_.size() > 1)
if (data_.beforeVault().size() > 1 || data_.afterVault().size() > 1)
{
JLOG(j.fatal()) << //
"Invariant failed: vault operation updated more than single vault";
@@ -329,7 +84,7 @@ ValidVault::finalize(
// We do special handling for ttVAULT_DELETE first, because it's the only
// vault-modifying transaction without an "after" state of the vault
if (afterVault_.empty())
if (data_.afterVault().empty())
{
if (txnType != ttVAULT_DELETE)
{
@@ -344,19 +99,9 @@ ValidVault::finalize(
// Note, if afterVault_ is empty then we know that beforeVault_ is not
// empty, as enforced at the top of this function
auto const& beforeVault = beforeVault_[0];
auto const& beforeVault = data_.beforeVault()[0];
// At this moment we only know a vault is being deleted and there
// might be some MPTokenIssuance objects which are deleted in the
// same transaction. Find the one matching this vault.
auto const deletedShares = [&]() -> std::optional<Shares> {
for (auto const& e : beforeMPTs_)
{
if (e.share.getMptID() == beforeVault.shareMPTID)
return e;
}
return std::nullopt;
}();
auto const deletedShares = data_.resolveBeforeShares(beforeVault);
if (!deletedShares)
{
@@ -397,34 +142,29 @@ ValidVault::finalize(
}
// Note, `afterVault_.empty()` is handled above
auto const& afterVault = afterVault_[0];
auto const& afterVault = data_.afterVault()[0];
XRPL_ASSERT(
beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
data_.beforeVault().empty() || data_.beforeVault()[0].key == afterVault.key,
"xrpl::ValidVault::finalize : single vault operation");
auto const updatedShares = [&]() -> std::optional<Shares> {
// At this moment we only know that a vault is being updated and there
// might be some MPTokenIssuance objects which are also updated in the
// same transaction. Find the one matching the shares to this vault.
// Note, we expect updatedMPTs collection to be extremely small. For
// such collections linear search is faster than lookup.
for (auto const& e : afterMPTs_)
{
if (e.share.getMptID() == afterVault.shareMPTID)
return e;
}
// Check the in-memory collection first (covers the common case where
// the issuance was touched by this transaction).
if (auto found = data_.resolveUpdatedShares(afterVault))
return found;
// Fall back to reading from the view for transactions that do not
// modify the issuance object itself (e.g. VaultSet).
auto const sleShares = view.read(keylet::mptokenIssuance(afterVault.shareMPTID));
return sleShares ? std::optional<Shares>(Shares::make(*sleShares)) : std::nullopt;
}();
bool result = true;
// Universal transaction checks
if (!beforeVault_.empty())
if (!data_.beforeVault().empty())
{
auto const& beforeVault = beforeVault_[0];
auto const& beforeVault = data_.beforeVault()[0];
if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
afterVault.shareMPTID != beforeVault.shareMPTID)
{
@@ -497,7 +237,7 @@ ValidVault::finalize(
// Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
// enforcing invariants on transaction types other than ttVAULT_CREATE
if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
if (data_.beforeVault().empty() && txnType != ttVAULT_CREATE)
{
JLOG(j.fatal()) << //
"Invariant failed: vault created by a wrong transaction type";
@@ -505,7 +245,8 @@ ValidVault::finalize(
return !enforce; // That's all we can do here
}
if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
if (!data_.beforeVault().empty() &&
afterVault.lossUnrealized != data_.beforeVault()[0].lossUnrealized &&
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
{
JLOG(j.fatal()) << //
@@ -515,16 +256,9 @@ ValidVault::finalize(
}
auto const beforeShares = [&]() -> std::optional<Shares> {
if (beforeVault_.empty())
if (data_.beforeVault().empty())
return std::nullopt;
auto const& beforeVault = beforeVault_[0];
for (auto const& e : beforeMPTs_)
{
if (e.share.getMptID() == beforeVault.shareMPTID)
return e;
}
return std::nullopt;
return data_.resolveBeforeShares(data_.beforeVault()[0]);
}();
if (!beforeShares &&
@@ -548,68 +282,19 @@ ValidVault::finalize(
switch (txnType)
{
case ttVAULT_CREATE: {
bool result = true;
if (!beforeVault_.empty())
{
JLOG(j.fatal()) //
<< "Invariant failed: create operation must not have "
"updated a vault";
result = false;
}
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
{
JLOG(j.fatal()) //
<< "Invariant failed: created vault must be empty";
result = false;
}
if (afterVault.pseudoId != updatedShares->share.getIssuer())
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer and vault "
"pseudo-account must be the same";
result = false;
}
auto const sleSharesIssuer =
view.read(keylet::account(updatedShares->share.getIssuer()));
if (!sleSharesIssuer)
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer must exist";
return false;
}
if (!isPseudoAccount(sleSharesIssuer))
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer must be a "
"pseudo-account";
result = false;
}
if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID];
!vaultId || *vaultId != afterVault.key)
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer pseudo-account "
"must point back to the vault";
result = false;
}
return result;
// Per-transactor invariants for create are checked in
// VaultCreate::finalizeInvariants before this runs.
return true;
}
case ttVAULT_SET: {
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault");
auto const& beforeVault = beforeVault_[0];
!data_.beforeVault().empty(),
"xrpl::ValidVault::finalize : set updated a vault");
auto const& beforeVault = data_.beforeVault()[0];
auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const vaultDeltaAssets = data_.deltaAssets(afterVault.pseudoId);
if (vaultDeltaAssets)
{
JLOG(j.fatal()) << //
@@ -657,10 +342,11 @@ ValidVault::finalize(
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = beforeVault_[0];
!data_.beforeVault().empty(),
"xrpl::ValidVault::finalize : deposit updated a vault");
auto const& beforeVault = data_.beforeVault()[0];
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const maybeVaultDeltaAssets = data_.deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << //
@@ -669,7 +355,8 @@ ValidVault::finalize(
}
// Get the posterior scale to round calculations to
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const minScale =
data_.computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
@@ -700,15 +387,15 @@ ValidVault::finalize(
if (!issuerDeposit)
{
auto const maybeAccDeltaAssets = deltaAssetsTxAccount(tx, fee);
auto const maybeAccDeltaAssets = data_.deltaAssetsTxAccount(tx, fee);
if (!maybeAccDeltaAssets)
{
JLOG(j.fatal())
<< "Invariant failed: deposit must change depositor balance";
return false;
}
auto const localMinScale =
std::max(minScale, computeCoarsestScale({*maybeAccDeltaAssets}));
auto const localMinScale = std::max(
minScale, VaultInvariantData::computeCoarsestScale({*maybeAccDeltaAssets}));
auto const accountDeltaAssets =
roundToAsset(vaultAsset, maybeAccDeltaAssets->delta, localMinScale);
@@ -741,7 +428,7 @@ ValidVault::finalize(
result = false;
}
auto const maybeAccDeltaShares = deltaShares(tx[sfAccount]);
auto const maybeAccDeltaShares = data_.deltaShares(tx[sfAccount]);
if (!maybeAccDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change depositor shares";
@@ -755,7 +442,7 @@ ValidVault::finalize(
result = false;
}
auto const maybeVaultDeltaShares = deltaShares(afterVault.pseudoId);
auto const maybeVaultDeltaShares = data_.deltaShares(afterVault.pseudoId);
if (!maybeVaultDeltaShares || maybeVaultDeltaShares->delta == kZero)
{
JLOG(j.fatal()) << "Invariant failed: deposit must change vault shares";
@@ -794,11 +481,11 @@ ValidVault::finalize(
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(),
!data_.beforeVault().empty(),
"xrpl::ValidVault::finalize : withdrawal updated a vault");
auto const& beforeVault = beforeVault_[0];
auto const& beforeVault = data_.beforeVault()[0];
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const maybeVaultDeltaAssets = data_.deltaAssets(afterVault.pseudoId);
if (!maybeVaultDeltaAssets)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
@@ -806,7 +493,8 @@ ValidVault::finalize(
}
// Get the posterior scale to round calculations to
auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const minScale =
data_.computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const vaultPseudoDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
@@ -828,11 +516,11 @@ ValidVault::finalize(
if (!issuerWithdrawal)
{
auto const maybeAccDelta = deltaAssetsTxAccount(tx, fee);
auto const maybeAccDelta = data_.deltaAssetsTxAccount(tx, fee);
auto const maybeOtherAccDelta = [&]() -> std::optional<DeltaInfo> {
if (auto const destination = tx[~sfDestination];
destination && *destination != tx[sfAccount])
return deltaAssets(*destination);
return data_.deltaAssets(*destination);
return std::nullopt;
}();
@@ -848,7 +536,8 @@ ValidVault::finalize(
// the scale of destinationDelta can be coarser than
// minScale, so we take that into account when rounding
auto const destinationScale = computeCoarsestScale({destinationDelta});
auto const destinationScale =
VaultInvariantData::computeCoarsestScale({destinationDelta});
auto const localMinScale = std::max(minScale, destinationScale);
auto const roundedDestinationDelta =
@@ -899,7 +588,7 @@ ValidVault::finalize(
}
// We don't round shares, they are integral MPT
auto const accountDeltaShares = deltaShares(tx[sfAccount]);
auto const accountDeltaShares = data_.deltaShares(tx[sfAccount]);
if (!accountDeltaShares)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change depositor shares";
@@ -914,7 +603,7 @@ ValidVault::finalize(
}
// We don't round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
auto const vaultDeltaShares = data_.deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || vaultDeltaShares->delta == kZero)
{
JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault shares";
@@ -954,15 +643,17 @@ ValidVault::finalize(
bool result = true;
XRPL_ASSERT(
!beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault");
auto const& beforeVault = beforeVault_[0];
!data_.beforeVault().empty(),
"xrpl::ValidVault::finalize : clawback updated a vault");
auto const& beforeVault = data_.beforeVault()[0];
if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount])
{
// The owner can use clawback to force-burn shares when the
// vault is empty but there are outstanding shares
if (!(beforeShares && beforeShares->sharesTotal > 0 &&
isVaultEmpty(beforeVault) && beforeVault.owner == tx[sfAccount]))
VaultInvariantData::isVaultEmpty(beforeVault) &&
beforeVault.owner == tx[sfAccount]))
{
JLOG(j.fatal()) << "Invariant failed: " << //
"clawback may only be performed by the asset issuer, or by the vault "
@@ -971,11 +662,11 @@ ValidVault::finalize(
}
}
auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
auto const maybeVaultDeltaAssets = data_.deltaAssets(afterVault.pseudoId);
if (maybeVaultDeltaAssets)
{
auto const minScale =
computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
data_.computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
auto const vaultDeltaAssets =
roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
if (vaultDeltaAssets >= kZero)
@@ -1004,7 +695,7 @@ ValidVault::finalize(
result = false;
}
}
else if (!isVaultEmpty(beforeVault))
else if (!VaultInvariantData::isVaultEmpty(beforeVault))
{
JLOG(j.fatal()) << //
"Invariant failed: clawback must change vault balance";
@@ -1012,7 +703,7 @@ ValidVault::finalize(
}
// We don't need to round shares, they are integral MPT
auto const maybeAccountDeltaShares = deltaShares(tx[sfHolder]);
auto const maybeAccountDeltaShares = data_.deltaShares(tx[sfHolder]);
if (!maybeAccountDeltaShares)
{
JLOG(j.fatal()) << //
@@ -1027,7 +718,7 @@ ValidVault::finalize(
}
// We don't need to round shares, they are integral MPT
auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
auto const vaultDeltaShares = data_.deltaShares(afterVault.pseudoId);
if (!vaultDeltaShares || vaultDeltaShares->delta == kZero)
{
JLOG(j.fatal()) << //
@@ -1069,25 +760,4 @@ ValidVault::finalize(
return true;
}
[[nodiscard]] ValidVault::DeltaInfo
ValidVault::DeltaInfo::makeDelta(Number const& before, Number const& after, Asset const& asset)
{
return {
.delta = after - before,
.scale = std::max(xrpl::scale(after, asset), xrpl::scale(before, asset))};
}
[[nodiscard]] std::int32_t
ValidVault::computeCoarsestScale(std::vector<DeltaInfo> const& numbers)
{
if (numbers.empty())
return 0;
auto const max = std::ranges::max_element(
numbers, [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
XRPL_ASSERT_PARTS(
max->scale, "xrpl::ValidVault::computeCoarsestScale", "scale set for destinationDelta");
return max->scale.value_or(STAmount::kMaxOffset);
}
} // namespace xrpl
@@ -1,9 +1,12 @@
#include <xrpl/tx/transactors/vault/VaultCreate.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
@@ -270,15 +273,106 @@ VaultCreate::doApply()
}
void
VaultCreate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
VaultCreate::visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
// No transaction-specific invariants yet (future work).
invariantData_.visitEntry(isDelete, before, after);
}
bool
VaultCreate::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
VaultCreate::finalizeInvariants(
STTx const& tx,
TER result,
XRPAmount,
ReadView const& view,
beast::Journal const& j)
{
// No transaction-specific invariants yet (future work).
if (!isTesSuccess(result))
return true;
bool const enforce = view.rules().enabled(featureSingleAssetVault);
auto const& afterVaults = invariantData_.afterVault();
auto const& beforeVaults = invariantData_.beforeVault();
if (afterVaults.empty())
{
JLOG(j.fatal()) << "Invariant failed: VaultCreate must create a vault";
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault created");
return !enforce;
}
auto const& afterVault = afterVaults[0];
bool ok = true;
if (!beforeVaults.empty())
{
JLOG(j.fatal()) << //
"Invariant failed: create operation must not have updated a vault";
ok = false;
}
auto const updatedShares = [&]() -> std::optional<VaultInvariantData::Shares> {
if (auto found = invariantData_.resolveUpdatedShares(afterVault))
return found;
auto const sleShares = view.read(keylet::mptokenIssuance(afterVault.shareMPTID));
if (!sleShares)
return std::nullopt;
return VaultInvariantData::Shares::make(*sleShares);
}();
if (!updatedShares)
{
JLOG(j.fatal()) << "Invariant failed: VaultCreate must create share issuance";
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : shares created");
return !enforce;
}
static constexpr Number kZero{};
if (afterVault.assetsAvailable != kZero || afterVault.assetsTotal != kZero ||
afterVault.lossUnrealized != kZero || updatedShares->sharesTotal != 0)
{
JLOG(j.fatal()) << "Invariant failed: created vault must be empty";
ok = false;
}
if (afterVault.pseudoId != updatedShares->share.getIssuer())
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer and vault "
"pseudo-account must be the same";
ok = false;
}
auto const sleSharesIssuer = view.read(keylet::account(updatedShares->share.getIssuer()));
if (!sleSharesIssuer)
{
JLOG(j.fatal()) << "Invariant failed: shares issuer must exist";
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : shares issuer exists");
return !enforce;
}
if (!isPseudoAccount(sleSharesIssuer))
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer must be a "
"pseudo-account";
ok = false;
}
if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; !vaultId || *vaultId != afterVault.key)
{
JLOG(j.fatal()) //
<< "Invariant failed: shares issuer pseudo-account "
"must point back to the vault";
ok = false;
}
if (!ok)
{
XRPL_ASSERT(enforce, "xrpl::VaultCreate::finalizeInvariants : vault invariants");
return !enforce;
}
return true;
}
@@ -0,0 +1,332 @@
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <cstdint>
#include <memory>
#include <optional>
#include <variant>
#include <vector>
namespace xrpl {
static constexpr Number kZero{};
VaultInvariantData::Vault
VaultInvariantData::Vault::make(SLE const& from)
{
XRPL_ASSERT(from.getType() == ltVAULT, "VaultInvariantData::Vault::make : from Vault object");
VaultInvariantData::Vault self;
self.key = from.key();
self.asset = from.at(sfAsset);
self.pseudoId = from.getAccountID(sfAccount);
self.owner = from.at(sfOwner);
self.shareMPTID = from.getFieldH192(sfShareMPTID);
self.assetsTotal = from.at(sfAssetsTotal);
self.assetsAvailable = from.at(sfAssetsAvailable);
self.assetsMaximum = from.at(sfAssetsMaximum);
self.lossUnrealized = from.at(sfLossUnrealized);
return self;
}
VaultInvariantData::Shares
VaultInvariantData::Shares::make(SLE const& from)
{
XRPL_ASSERT(
from.getType() == ltMPTOKEN_ISSUANCE,
"VaultInvariantData::Shares::make : from MPTokenIssuance object");
VaultInvariantData::Shares self;
self.sleKey = from.key();
self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
self.sharesTotal = from.at(sfOutstandingAmount);
self.sharesMaximum = from[~sfMaximumAmount].value_or(kMaxMpTokenAmount);
return self;
}
void
VaultInvariantData::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
// If `before` is empty, this means an object is being created, in which
// case `isDelete` must be false. Otherwise `before` and `after` are set and
// `isDelete` indicates whether an object is being deleted or modified.
XRPL_ASSERT(
after != nullptr && (before != nullptr || !isDelete),
"xrpl::VaultInvariantData::visitEntry : some object is available");
// Number balanceDelta will capture the difference (delta) between "before"
// state (zero if created) and "after" state (zero if destroyed), and
// preserves value scale (exponent) to round values to the same scale during
// validation. It is used to validate that the change in account
// balances matches the change in vault balances, stored to deltas_ at the
// end of this function.
DeltaInfo balanceDelta{.delta = kNumZero, .scale = std::nullopt};
std::int8_t sign = 0;
if (before)
{
switch (before->getType())
{
case ltVAULT:
beforeVault_.push_back(Vault::make(*before));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
beforeMPTs_.push_back(Shares::make(*before));
balanceDelta.delta =
static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta = before->getFieldAmount(sfBalance);
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = before->getFieldAmount(sfBalance);
balanceDelta.delta = amount;
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
if (!isDelete && after)
{
switch (after->getType())
{
case ltVAULT:
afterVault_.push_back(Vault::make(*after));
break;
case ltMPTOKEN_ISSUANCE:
// At this moment we have no way of telling if this object holds
// vault shares or something else. Save it for finalize.
afterMPTs_.push_back(Shares::make(*after));
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = 1;
break;
case ltMPTOKEN:
balanceDelta.delta -=
Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
// MPTs are ints, so the scale is always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltACCOUNT_ROOT:
balanceDelta.delta -= Number(after->getFieldAmount(sfBalance));
// Account balance is XRP, which is an int, so the scale is
// always 0.
balanceDelta.scale = 0;
sign = -1;
break;
case ltRIPPLE_STATE: {
auto const amount = after->getFieldAmount(sfBalance);
balanceDelta.delta -= Number(amount);
// Trust Line balances are STAmounts, so we can use the exponent
// directly to get the scale.
if (amount.exponent() > balanceDelta.scale)
balanceDelta.scale = amount.exponent();
sign = -1;
break;
}
default:;
}
}
uint256 const key = (before ? before->key() : after->key());
// Append to deltas if sign is non-zero, i.e. an object of an interesting
// type has been updated. A transaction may update an object even when
// its balance has not changed, e.g. transaction fee equals the amount
// transferred to the account. We intentionally do not compare balanceDelta
// against zero, to avoid missing such updates.
if (sign != 0)
{
XRPL_ASSERT_PARTS(
balanceDelta.scale, "xrpl::VaultInvariantData::visitEntry", "scale initialized");
balanceDelta.delta *= sign;
deltas_[key] = balanceDelta;
}
}
std::optional<VaultInvariantData::Shares>
VaultInvariantData::resolveUpdatedShares(Vault const& afterVault) const
{
auto const targetKey = keylet::mptokenIssuance(afterVault.shareMPTID).key;
for (auto const& e : afterMPTs_)
{
if (e.sleKey == targetKey)
return e;
}
return std::nullopt;
}
std::optional<VaultInvariantData::Shares>
VaultInvariantData::resolveBeforeShares(Vault const& beforeVault) const
{
auto const targetKey = keylet::mptokenIssuance(beforeVault.shareMPTID).key;
for (auto const& e : beforeMPTs_)
{
if (e.sleKey == targetKey)
return e;
}
return std::nullopt;
}
std::optional<VaultInvariantData::DeltaInfo>
VaultInvariantData::deltaAssets(AccountID const& id) const
{
auto const& vaultAsset = afterVault_[0].asset;
auto const lookup = [&](uint256 const& key) -> std::optional<DeltaInfo> {
auto const it = deltas_.find(key);
if (it == deltas_.end())
return std::nullopt;
return it->second;
};
return std::visit(
[&]<typename TIss>(TIss const& issue) -> std::optional<DeltaInfo> {
if constexpr (std::is_same_v<TIss, Issue>)
{
if (isXRP(issue))
return lookup(keylet::account(id).key);
auto result = lookup(keylet::trustLine(id, issue).key);
// Trust-line balance is stored from the low-account's perspective;
// negate if id is the high account so the delta is in id's terms.
if (result && id > issue.getIssuer())
result->delta = -result->delta;
return result;
}
else if constexpr (std::is_same_v<TIss, MPTIssue>)
{
return lookup(keylet::mptoken(issue.getMptID(), id).key);
}
},
vaultAsset.value());
}
std::optional<VaultInvariantData::DeltaInfo>
VaultInvariantData::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const
{
auto const& vaultAsset = afterVault_[0].asset;
auto ret = deltaAssets(tx[sfAccount]);
if (!ret.has_value() || !vaultAsset.native())
return ret;
// Only add the fee back if tx[sfAccount] actually paid it. When the fee is
// paid by someone else (a delegate or a fee sponsor), the
// account's XRP balance moved only by the vault amount.
if (tx.getFeePayerID() != tx[sfAccount])
return ret;
ret->delta += fee.drops();
if (ret->delta == kZero)
return std::nullopt;
return ret;
}
std::optional<VaultInvariantData::DeltaInfo>
VaultInvariantData::deltaShares(AccountID const& id) const
{
auto const& afterVault = afterVault_[0];
auto const it = [&]() {
if (id == afterVault.pseudoId)
return deltas_.find(keylet::mptokenIssuance(afterVault.shareMPTID).key);
return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
}();
return it != deltas_.end() ? std::optional<DeltaInfo>(it->second) : std::nullopt;
}
bool
VaultInvariantData::isVaultEmpty(Vault const& vault)
{
return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
}
std::int32_t
VaultInvariantData::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const
{
// Returns the posterior `assetsTotal` scale.
//
// 1. Because STAmounts are normalized, `assetsTotal` (being >= `assetsAvailable`)
// safely represents the coarsest exponent needed for both fields.
//
// 2. The scale may decrease (withdraw/clawback) or increase (deposit). In both cases
// we ensure the vault is in a legitimate state in the post-transaction scale.
auto const& afterVault = afterVault_[0];
auto const& vaultAsset = afterVault.asset;
if (rules.enabled(fixCleanup3_2_0))
{
NumberRoundModeGuard const roundGuard(Number::RoundingMode::ToNearest);
return scale(afterVault.assetsTotal, vaultAsset);
}
auto const& beforeVault = beforeVault_[0];
auto const totalDelta =
DeltaInfo::makeDelta(beforeVault.assetsTotal, afterVault.assetsTotal, vaultAsset);
auto const availableDelta =
DeltaInfo::makeDelta(beforeVault.assetsAvailable, afterVault.assetsAvailable, vaultAsset);
return computeCoarsestScale({vaultDelta, totalDelta, availableDelta});
}
[[nodiscard]] VaultInvariantData::DeltaInfo
VaultInvariantData::DeltaInfo::makeDelta(
Number const& before,
Number const& after,
Asset const& asset)
{
return {
.delta = after - before,
.scale = std::max(xrpl::scale(after, asset), xrpl::scale(before, asset))};
}
[[nodiscard]] std::int32_t
VaultInvariantData::computeCoarsestScale(std::vector<DeltaInfo> const& numbers)
{
if (numbers.empty())
return 0;
auto const max = std::ranges::max_element(
numbers, [](auto const& a, auto const& b) -> bool { return a.scale < b.scale; });
XRPL_ASSERT_PARTS(
max->scale,
"xrpl::VaultInvariantData::computeCoarsestScale",
"scale set for destinationDelta");
return max->scale.value_or(STAmount::kMaxOffset);
}
} // namespace xrpl
+61 -6
View File
@@ -21,6 +21,7 @@
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
@@ -54,7 +55,9 @@
#include <xrpl/tx/applySteps.h>
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/DirectoryInvariant.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <xrpl/tx/invariants/VaultInvariant.h>
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
#include <algorithm>
#include <array>
@@ -64,6 +67,7 @@
#include <initializer_list>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <utility>
@@ -209,7 +213,8 @@ class Invariants_test : public beast::unit_test::Suite
TER terActual = tesSUCCESS;
for (TER const& terExpect : ters)
{
terActual = transactor->checkInvariants(terActual, fee);
terActual =
transactor->checkInvariants(terActual, fee, Transactor::CheckTxInvariants::Yes);
BEAST_EXPECTS(
terExpect == terActual,
"expected: " + transToken(terExpect) + " got: " + transToken(terActual));
@@ -5571,7 +5576,7 @@ class Invariants_test : public beast::unit_test::Suite
{
std::string name;
std::int32_t expectedMinScale;
std::vector<ValidVault::DeltaInfo> values;
std::vector<VaultInvariantData::DeltaInfo> values;
};
for (auto const mantissaScale : MantissaRange::getAllScales())
@@ -5580,7 +5585,7 @@ class Invariants_test : public beast::unit_test::Suite
continue;
NumberMantissaScaleGuard const g{mantissaScale};
auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
auto makeDelta = [&vaultAsset](Number const& n) -> VaultInvariantData::DeltaInfo {
return {.delta = n, .scale = scale(n, vaultAsset.raw())};
};
@@ -5626,7 +5631,7 @@ class Invariants_test : public beast::unit_test::Suite
{
testcase("vault computeCoarsestScale: " + tc.name);
auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
auto const actualScale = VaultInvariantData::computeCoarsestScale(tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
@@ -5664,7 +5669,7 @@ class Invariants_test : public beast::unit_test::Suite
{
testcase("vault computeCoarsestScale: " + tc.name);
auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
auto const actualScale = VaultInvariantData::computeCoarsestScale(tc.values);
BEAST_EXPECTS(
actualScale == tc.expectedMinScale,
@@ -5908,12 +5913,61 @@ class Invariants_test : public beast::unit_test::Suite
auto transactor = makeTransactor(ac);
if (!BEAST_EXPECT(transactor))
return;
TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
TER const result = transactor->checkInvariants(
tesSUCCESS, XRPAmount{}, Transactor::CheckTxInvariants::Yes);
BEAST_EXPECT(result == tecINVARIANT_FAILED);
BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
}
}
void
testTxCheckException()
{
testcase << "txCheck exception";
using namespace jtx;
// A TxInvariantCheck that always throws from finalize, so we can
// exercise checkInvariantsHelper's catch block via the
// transaction-specific layer (as opposed to the protocol layer,
// which testObjectHasPseudoAccount's last case already covers via a
// real Transactor's finalizeInvariants).
struct ThrowingTxInvariantCheck : TxInvariantCheck
{
void
visitEntry(bool, SLE::const_ref, SLE::const_ref) override
{
}
[[nodiscard]] bool
finalize(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
{
throw std::runtime_error("test-injected txCheck exception");
}
};
Env env{*this};
Account const alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
OpenView ov{*env.current()};
STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
test::StreamSink sink{beast::Severity::Warning};
beast::Journal const jlog{sink};
ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
CurrentTransactionRulesGuard const rulesGuard(ov.rules());
ThrowingTxInvariantCheck throwing;
TER terActual = tesSUCCESS;
for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
{
terActual = checkInvariants(ac, terActual, XRPAmount{}, throwing);
BEAST_EXPECT(terExpect == terActual);
BEAST_EXPECT(sink.messages().str().contains(
"Transaction caused an exception during invariant checks"));
}
}
void
testConfidentialMPTTransfer()
{
@@ -6198,6 +6252,7 @@ public:
testAMM();
testObjectHasPseudoAccount();
testSponsorship();
testTxCheckException();
}
};
+3 -2
View File
@@ -31,6 +31,7 @@
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nft.h>
#include <xrpl/tx/ApplyContext.h>
#include <xrpl/tx/invariants/InvariantRunner.h>
#include <algorithm>
#include <cstddef>
@@ -791,7 +792,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
TER terActual = tesSUCCESS;
for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
{
terActual = ac.checkInvariants(terActual, XRPAmount{});
terActual = xrpl::checkInvariants(ac, terActual, XRPAmount{});
BEAST_EXPECT(terExpect == terActual);
BEAST_EXPECT(sink.messages().str().starts_with("Invariant failed:"));
// uncomment to log the invariant failure message
@@ -827,7 +828,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
TER terActual = tesSUCCESS;
for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
{
terActual = ac.checkInvariants(terActual, XRPAmount{});
terActual = xrpl::checkInvariants(ac, terActual, XRPAmount{});
BEAST_EXPECT(terExpect == terActual);
BEAST_EXPECT(sink.messages().str().starts_with("Invariant failed:"));
// uncomment to log the invariant failure message