perf: Optimize MPT freeze checks to reduce redundant state reads (#7411)

Co-authored-by: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Gregory Tsipenyuk
2026-08-17 21:15:16 +00:00
committed by GitHub
parent ca6121c5b3
commit 1b226c8b2e
10 changed files with 216 additions and 26 deletions

View File

@@ -61,12 +61,10 @@ hasExpired(
: view.parentCloseTime() > boundary;
}
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;
@@ -74,26 +72,37 @@ isVaultPseudoAccountFrozen(
if (depth >= kMaxAssetCheckDepth)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::View::isVaultPseudoAccountFrozen : reached asset check depth");
UNREACHABLE(
"xrpl::View::checkVaultPseudoAccountFrozenPreconditions : reached asset check depth");
return true;
// LCOV_EXCL_STOP
}
auto const mptIssuance = view.read(keylet::mptokenIssuance(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
@@ -102,7 +111,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));
@@ -128,6 +137,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::mptokenIssuance(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

@@ -11,6 +11,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>
@@ -633,7 +634,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

@@ -42,18 +42,35 @@ bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue)
{
if (auto const sle = view.read(keylet::mptokenIssuance(mptIssue.getMptID())))
return sle->isFlag(lsfMPTLocked);
return isGlobalFrozen(*sle);
return false;
}
bool
isGlobalFrozen(SLE const& issuanceSle)
{
XRPL_ASSERT(
issuanceSle.getType() == ltMPTOKEN_ISSUANCE, "xrpl::isGlobalFrozen : MPTokenIssuance SLE");
return issuanceSle.isFlag(lsfMPTLocked);
}
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 mptSle.isFlag(lsfMPTLocked);
}
bool
isFrozen(
ReadView const& view,
@@ -65,6 +82,34 @@ isFrozen(
isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
}
bool
isFrozen(ReadView const& view, AccountID const& account, SLE const& sle, std::uint8_t depth)
{
XRPL_ASSERT(
sle.getType() == ltMPTOKEN || sle.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isFrozen : MPToken or MPTokenIssuance SLE");
if (sle.getType() == ltMPTOKEN)
{
XRPL_ASSERT(sle[sfAccount] == account, "xrpl::isFrozen : valid MPToken holder");
MPTID const mptID = sle[sfMPTokenIssuanceID];
auto const issuanceSle = view.read(keylet::mptokenIssuance(mptID));
if ((issuanceSle && isGlobalFrozen(*issuanceSle)) || isIndividualFrozen(sle))
return true;
if (issuanceSle)
return isVaultPseudoAccountFrozen(view, account, *issuanceSle, depth);
return isVaultPseudoAccountFrozen(view, account, MPTIssue{mptID}, depth);
}
MPTIssue const mptIssue{sle[sfSequence], sle[sfIssuer]};
return isGlobalFrozen(sle) || isIndividualFrozen(view, account, mptIssue) ||
isVaultPseudoAccountFrozen(view, account, sle, depth);
}
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
@@ -72,7 +117,8 @@ isAnyFrozen(
MPTIssue const& mptIssue,
std::uint8_t depth)
{
if (isGlobalFrozen(view, mptIssue))
auto const issuanceSle = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
if (issuanceSle && isGlobalFrozen(*issuanceSle))
return true;
for (auto const& account : accounts)
@@ -81,9 +127,15 @@ isAnyFrozen(
return true;
}
return std::ranges::any_of(accounts, [&](auto const& account) {
return isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
});
// Pass the issuance SLE when we have it to avoid re-reading it per account;
// otherwise defer to the MPTIssue overload, which handles a missing issuance.
auto const anyVaultFrozen = [&](auto const& shareOrIssuance) {
return std::ranges::any_of(accounts, [&](auto const& account) {
return isVaultPseudoAccountFrozen(view, account, shareOrIssuance, depth);
});
};
return issuanceSle ? anyVaultFrozen(*issuanceSle) : anyVaultFrozen(mptIssue);
}
Rate

View File

@@ -439,7 +439,7 @@ accountHolds(
auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account));
if (!sleMpt ||
(zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue)))
(zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, *sleMpt)))
{
amount.clear(mptIssue);
}

View File

@@ -900,7 +900,7 @@ ValidMPTTransfer::finalize(
// Check once: if any involved account is frozen, the whole issuance transfer is
// considered frozen. Only need to check for frozen if there is a transfer of funds.
if (!invalidTransfer &&
(isFrozen(view, account, MPTIssue{mptID}) ||
(isFrozen(view, account, *sleIssuance) ||
!isAuthorized(view, mptID, account, reqAuth)))
{
invalidTransfer = true;

View File

@@ -304,11 +304,11 @@ escrowCreatePreclaimHelper<MPTIssue>(
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(ctx.view, account, mptIssue))
if (isFrozen(ctx.view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
if (isFrozen(ctx.view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH

View File

@@ -186,7 +186,7 @@ escrowFinishPreclaimHelper<MPTIssue>(
return ter;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
if (isFrozen(ctx.view, dest, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;

View File

@@ -27,6 +27,8 @@
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -7421,6 +7423,57 @@ private:
}
}
void
testDanglingAMMMPTokenFreezeCheck()
{
testcase("Dangling AMM MPToken freeze check");
using namespace jtx;
FeatureBitset const all{testableAmendments()};
Env env(*this, 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::mptokenIssuance(usd.issuanceID())) == nullptr);
BEAST_EXPECT(!isFrozen(*env.current(), amm.ammAccount(), *ammToken));
// 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
{
@@ -7461,6 +7514,7 @@ private:
testDepositIntegralOverflowMPT(all);
testDepositIntegralOverflowMPT(all - fixCleanup3_4_0);
testWithdrawIntegralNoOverflowMPT();
testDanglingAMMMPTokenFreezeCheck();
}
};