mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-02 16:26:48 +00:00
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.
This commit is contained in:
@@ -263,10 +263,7 @@ protected:
|
||||
* to detect deletions.
|
||||
*/
|
||||
virtual void
|
||||
visitInvariantEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after) = 0;
|
||||
visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) = 0;
|
||||
|
||||
/** Check transaction-specific post-conditions after all entries have
|
||||
* been visited.
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -37,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).
|
||||
* @returns 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.
|
||||
* @returns 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.
|
||||
* @returns 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.
|
||||
* @returns 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.
|
||||
* @returns @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, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
|
||||
private:
|
||||
VaultInvariantData data_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -41,6 +42,9 @@ public:
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
|
||||
private:
|
||||
VaultInvariantData invariantData_;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
127
include/xrpl/tx/transactors/vault/VaultInvariantData.h
Normal file
127
include/xrpl/tx/transactors/vault/VaultInvariantData.h
Normal file
@@ -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
|
||||
@@ -5,280 +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,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
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::line(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;
|
||||
|
||||
if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != 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::mptIssuance(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
|
||||
@@ -294,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))
|
||||
{
|
||||
@@ -318,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";
|
||||
@@ -330,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)
|
||||
{
|
||||
@@ -345,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)
|
||||
{
|
||||
@@ -398,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::mptIssuance(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)
|
||||
{
|
||||
@@ -498,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";
|
||||
@@ -506,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()) << //
|
||||
@@ -516,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 &&
|
||||
@@ -549,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()) << //
|
||||
@@ -658,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()) << //
|
||||
@@ -670,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);
|
||||
@@ -701,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);
|
||||
@@ -742,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";
|
||||
@@ -756,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";
|
||||
@@ -795,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";
|
||||
@@ -807,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);
|
||||
@@ -829,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;
|
||||
}();
|
||||
|
||||
@@ -849,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 =
|
||||
@@ -900,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";
|
||||
@@ -915,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";
|
||||
@@ -955,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 "
|
||||
@@ -972,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)
|
||||
@@ -1005,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";
|
||||
@@ -1013,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()) << //
|
||||
@@ -1028,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()) << //
|
||||
@@ -1072,25 +762,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>
|
||||
@@ -263,18 +266,106 @@ VaultCreate::doApply()
|
||||
}
|
||||
|
||||
void
|
||||
VaultCreate::visitInvariantEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&)
|
||||
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::mptIssuance(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;
|
||||
}
|
||||
|
||||
|
||||
329
src/libxrpl/tx/transactors/vault/VaultInvariantData.cpp
Normal file
329
src/libxrpl/tx/transactors/vault/VaultInvariantData.cpp
Normal file
@@ -0,0 +1,329 @@
|
||||
#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::mptIssuance(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::mptIssuance(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::line(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;
|
||||
|
||||
if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != 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::mptIssuance(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
|
||||
@@ -52,7 +52,7 @@
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
#include <xrpl/tx/applySteps.h>
|
||||
#include <xrpl/tx/invariants/DirectoryInvariant.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariant.h>
|
||||
#include <xrpl/tx/transactors/vault/VaultInvariantData.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -4764,12 +4764,12 @@ 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;
|
||||
};
|
||||
|
||||
NumberMantissaScaleGuard const g{MantissaRange::MantissaScale::Large};
|
||||
|
||||
auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
|
||||
auto makeDelta = [&vaultAsset](Number const& n) -> VaultInvariantData::DeltaInfo {
|
||||
return {.delta = n, .scale = scale(n, vaultAsset.raw())};
|
||||
};
|
||||
|
||||
@@ -4811,7 +4811,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,
|
||||
@@ -4849,7 +4849,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,
|
||||
|
||||
Reference in New Issue
Block a user