From df0ed95383968d5656102a33cfcbcbfea254aff4 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 5 Jun 2026 06:51:37 -0400 Subject: [PATCH] Optimize MPT freeze checks to reduce redundant state reads --- include/xrpl/ledger/View.h | 7 ++ include/xrpl/ledger/helpers/MPTokenHelpers.h | 9 +++ src/libxrpl/ledger/View.cpp | 66 +++++++++++++++---- src/libxrpl/ledger/helpers/AMMHelpers.cpp | 3 +- src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 52 ++++++++++++++- src/libxrpl/ledger/helpers/TokenHelpers.cpp | 2 +- src/test/app/AMMMPT_test.cpp | 59 +++++++++++++++++ 7 files changed, 181 insertions(+), 17 deletions(-) diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 255413e459..87b56a1905 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -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, diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index c709badab8..8336f27dd9 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -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, diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index fdd7998609..73f286d259 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -52,12 +52,10 @@ hasExpired(ReadView const& view, std::optional 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 +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, diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp index fe6d022490..e149e0660b 100644 --- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -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}; }, diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 387116d820..fa043ac4e0 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -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, diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 7191456868..f9876ab009 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -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); } diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 31b54ceee0..88ebcc3a97 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -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 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(); } };