Optimize MPT freeze checks to reduce redundant state reads

This commit is contained in:
Gregory Tsipenyuk
2026-06-05 06:51:37 -04:00
parent 6c543426c3
commit df0ed95383
7 changed files with 181 additions and 17 deletions

View File

@@ -58,6 +58,13 @@ isVaultPseudoAccountFrozen(
MPTIssue const& mptShare,
std::uint8_t depth);
[[nodiscard]] bool
isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
SLE const& issuanceSle,
std::uint8_t depth);
[[nodiscard]] bool
isLPTokenFrozen(
ReadView const& view,

View File

@@ -23,9 +23,15 @@ namespace xrpl {
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
[[nodiscard]] bool
isGlobalFrozen(SLE const& issuanceSle);
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
[[nodiscard]] bool
isIndividualFrozen(SLE const& mptSle);
[[nodiscard]] bool
isFrozen(
ReadView const& view,
@@ -33,6 +39,9 @@ isFrozen(
MPTIssue const& mptIssue,
std::uint8_t depth = 0);
[[nodiscard]] bool
isFrozen(ReadView const& view, AccountID const& account, SLE const& mptSle, std::uint8_t depth = 0);
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,

View File

@@ -52,12 +52,10 @@ hasExpired(ReadView const& view, std::optional<std::uint32_t> const& exp)
return exp && (view.parentCloseTime() >= tp{d{*exp}});
}
bool
isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptShare,
std::uint8_t depth)
namespace {
std::optional<bool>
checkVaultPseudoAccountFrozenPreconditions(ReadView const& view, std::uint8_t depth)
{
if (!view.rules().enabled(featureSingleAssetVault))
return false;
@@ -70,21 +68,31 @@ isVaultPseudoAccountFrozen(
// LCOV_EXCL_STOP
}
auto const mptIssuance = view.read(keylet::mptIssuance(mptShare.getMptID()));
if (mptIssuance == nullptr)
return false; // zero MPToken won't block deletion of MPTokenIssuance
return std::nullopt;
}
auto const issuer = mptIssuance->getAccountID(sfIssuer);
bool
isVaultPseudoAccountFrozenForIssuance(
ReadView const& view,
AccountID const& account,
SLE const& issuanceSle,
std::uint8_t depth)
{
XRPL_ASSERT(
issuanceSle.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isVaultPseudoAccountFrozenForIssuance : MPTokenIssuance SLE");
auto const issuer = issuanceSle.getAccountID(sfIssuer);
// Post-fixCleanup3_2_0: vault shares carry sfReferenceHolding pointing
// to the vault pseudo's MPToken or RippleState for the underlying.
// Read it to derive the underlying asset and recurse, skipping the
// issuer-account-then-vault chain. Pre-amendment shares (no field)
// fall back to the chain lookup below.
if (mptIssuance->isFieldPresent(sfReferenceHolding))
if (issuanceSle.isFieldPresent(sfReferenceHolding))
{
auto const sleHolding =
view.read(keylet::unchecked(mptIssuance->getFieldH256(sfReferenceHolding)));
view.read(keylet::unchecked(issuanceSle.getFieldH256(sfReferenceHolding)));
if (!sleHolding)
{
// LCOV_EXCL_START
@@ -93,7 +101,7 @@ isVaultPseudoAccountFrozen(
// LCOV_EXCL_STOP
}
return isAnyFrozen(
view, {issuer, account}, assetOfHolding(*mptIssuance, *sleHolding), depth + 1);
view, {issuer, account}, assetOfHolding(issuanceSle, *sleHolding), depth + 1);
}
auto const mptIssuer = view.read(keylet::account(issuer));
@@ -119,6 +127,38 @@ isVaultPseudoAccountFrozen(
return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1);
}
} // namespace
bool
isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
SLE const& issuanceSle,
std::uint8_t depth)
{
if (auto const result = checkVaultPseudoAccountFrozenPreconditions(view, depth))
return *result;
return isVaultPseudoAccountFrozenForIssuance(view, account, issuanceSle, depth);
}
bool
isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptShare,
std::uint8_t depth)
{
if (auto const result = checkVaultPseudoAccountFrozenPreconditions(view, depth))
return *result;
auto const issuanceSle = view.read(keylet::mptIssuance(mptShare.getMptID()));
if (issuanceSle == nullptr)
return false; // zero MPToken won't block deletion of MPTokenIssuance
return isVaultPseudoAccountFrozenForIssuance(view, account, *issuanceSle, depth);
}
bool
isLPTokenFrozen(
ReadView const& view,

View File

@@ -12,6 +12,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AMMCore.h>
@@ -598,7 +599,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
return asset.visit(
[&](MPTIssue const& issue) {
if (auto const sle = view.read(keylet::mptoken(issue, ammAccountID));
sle && !isFrozen(view, ammAccountID, issue))
sle && !isFrozen(view, ammAccountID, *sle))
return STAmount{issue, (*sle)[sfMPTAmount]};
return STAmount{asset};
},

View File

@@ -37,22 +37,53 @@
namespace xrpl {
namespace {
bool
isMPTLocked(SLE const& sle)
{
XRPL_ASSERT(
sle.getType() == ltMPTOKEN || sle.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isMPTLocked : MPToken or MPTokenIssuance SLE");
return sle.isFlag(lsfMPTLocked);
}
} // namespace
bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue)
{
if (auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID())))
return sle->isFlag(lsfMPTLocked);
return isGlobalFrozen(*sle);
return false;
}
bool
isGlobalFrozen(SLE const& issuance)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE, "xrpl::isGlobalFrozen : MPTokenIssuance SLE");
return isMPTLocked(issuance);
}
bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue)
{
if (auto const sle = view.read(keylet::mptoken(mptIssue.getMptID(), account)))
return sle->isFlag(lsfMPTLocked);
return isIndividualFrozen(*sle);
return false;
}
bool
isIndividualFrozen(SLE const& mptSle)
{
XRPL_ASSERT(mptSle.getType() == ltMPTOKEN, "xrpl::isIndividualFrozen : MPToken SLE");
return isMPTLocked(mptSle);
}
bool
isFrozen(
ReadView const& view,
@@ -64,6 +95,23 @@ isFrozen(
isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
}
bool
isFrozen(ReadView const& view, AccountID const& account, SLE const& mptSle, std::uint8_t depth)
{
XRPL_ASSERT(mptSle.getType() == ltMPTOKEN, "xrpl::isFrozen : MPToken SLE");
MPTID const mptID = mptSle[sfMPTokenIssuanceID];
auto const issuanceSle = view.read(keylet::mptIssuance(mptID));
if ((issuanceSle && isGlobalFrozen(*issuanceSle)) || isIndividualFrozen(mptSle))
return true;
if (issuanceSle)
return isVaultPseudoAccountFrozen(view, account, *issuanceSle, depth);
return isVaultPseudoAccountFrozen(view, account, MPTIssue{mptID}, depth);
}
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,

View File

@@ -338,7 +338,7 @@ accountHolds(
{
amount.clear(mptIssue);
}
else if (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue))
else if (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, *sleMpt))
{
amount.clear(mptIssue);
}

View File

@@ -25,6 +25,7 @@
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -7075,6 +7076,63 @@ private:
}
}
void
testDanglingAMMMPTokenFreezeCheck()
{
testcase("Dangling AMM MPToken freeze check");
using namespace jtx;
FeatureBitset const all{testableAmendments()};
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->fees.referenceFee = XRPAmount(1);
return cfg;
}),
all);
env.fund(XRP(1'000), gw_, alice_);
MPTTester usd({.env = env, .issuer = gw_});
MPTTester const btc({.env = env, .issuer = gw_});
AMM amm(env, gw_, usd(10'000), btc(10'000));
for (auto i = 0; i < kMaxDeletableAmmTrustLines + 10; ++i)
{
Account const a{std::to_string(i)};
env.fund(XRP(1'000), a);
env(trust(a, STAmount{amm.lptIssue(), 10'000}));
env.close();
}
// With too many LP-token trust lines to delete in one pass, the AMM
// remains in an empty state with zero-balance MPToken objects.
amm.withdrawAll(gw_);
BEAST_EXPECT(amm.ammExists());
BEAST_EXPECT(amm.expectBalances(usd(0), btc(0), IOUAmount{0}));
auto const ammToken = env.le(keylet::mptoken(usd.issuanceID(), amm.ammAccount()));
if (!BEAST_EXPECT(ammToken))
return;
BEAST_EXPECT((*ammToken)[sfMPTAmount] == 0);
usd.destroy();
BEAST_EXPECT(env.le(keylet::mptIssuance(usd.issuanceID())) == nullptr);
// A Payment cannot cross this empty AMM because BookStep skips AMMs
// with zero LPTokenBalance. Probe the same ZeroIfFrozen balance read
// used by AMM accounting.
auto const balance = accountHolds(
*env.current(),
amm.ammAccount(),
MPTIssue{usd.issuanceID()},
FreezeHandling::ZeroIfFrozen,
AuthHandling::IgnoreAuth,
env.journal);
BEAST_EXPECT(balance == usd(0));
}
void
run() override
{
@@ -7110,6 +7168,7 @@ private:
testLPTokenBalance(all - fixAMMv1_3);
testAMMDepositWithFrozenAssets();
testAutoDelete();
testDanglingAMMMPTokenFreezeCheck();
}
};