diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h index 3af81eff16..d8dac4caaf 100644 --- a/include/xrpl/ledger/helpers/NFTokenHelpers.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -20,6 +20,10 @@ removeTokenOffersWithLimit( Keylet const& directory, std::size_t maxDeletableOffers); +/** Returns tesSUCCESS if NFToken has few enough offers that it can be burned */ +TER +notTooManyOffers(ReadView const& view, uint256 const& nftokenID); + /** Finds the specified token in the owner's token directory. */ std::optional findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID); diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 494b3fa6cd..b7dd3760da 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(DefragDirectories, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(MPTokensV2, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (Security3_1_3, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo) diff --git a/src/libxrpl/ledger/ApplyView.cpp b/src/libxrpl/ledger/ApplyView.cpp index 476b635511..3297b1b681 100644 --- a/src/libxrpl/ledger/ApplyView.cpp +++ b/src/libxrpl/ledger/ApplyView.cpp @@ -10,6 +10,14 @@ namespace xrpl { namespace directory { +struct Gap +{ + uint64_t const page; + SLE::pointer node; + uint64_t const nextPage; + SLE::pointer next; +}; + std::uint64_t createRoot( ApplyView& view, @@ -111,7 +119,9 @@ insertPage( if (page == 0) return std::nullopt; if (!view.rules().enabled(fixDirectoryLimit) && page >= dirNodeMaxPages) // Old pages limit + { return std::nullopt; + } // We are about to create a new node; we'll link it to // the chain first: @@ -133,12 +143,8 @@ insertPage( // it's the default. if (page != 1) node->setFieldU64(sfIndexPrevious, page - 1); - XRPL_ASSERT_PARTS(!nextPage, "xrpl::directory::insertPage", "nextPage has default value"); - /* Reserved for future use when directory pages may be inserted in - * between two other pages instead of only at the end of the chain. if (nextPage) node->setFieldU64(sfIndexNext, nextPage); - */ describe(node); view.insert(node); @@ -154,7 +160,7 @@ ApplyView::dirAdd( uint256 const& key, std::function const&)> const& describe) { - auto root = peek(directory); + auto const root = peek(directory); if (!root) { @@ -164,6 +170,43 @@ ApplyView::dirAdd( auto [page, node, indexes] = directory::findPreviousPage(*this, directory, root); + if (rules().enabled(featureDefragDirectories)) + { + // If there are more nodes than just the root, and there's no space in + // the last one, walk backwards to find one with space, or to find one + // missing. + std::optional gapPages; + while (page && indexes.size() >= dirNodeMaxEntries) + { + // Find a page with space, or a gap in pages. + auto [prevPage, prevNode, prevIndexes] = + directory::findPreviousPage(*this, directory, node); + if (!gapPages && prevPage != page - 1) + gapPages.emplace(prevPage, prevNode, page, node); + page = prevPage; + node = prevNode; + indexes = prevIndexes; + } + // We looped through all the pages back to the root. + if (!page) + { + // If we found a gap, use it. + if (gapPages) + { + return directory::insertPage( + *this, + gapPages->page, + gapPages->node, + gapPages->nextPage, + gapPages->next, + key, + directory, + describe); + } + std::tie(page, node, indexes) = directory::findPreviousPage(*this, directory, root); + } + } + // If there's space, we use it: if (indexes.size() < dirNodeMaxEntries) { diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 7e7335232f..4652bccca8 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -607,6 +607,33 @@ removeTokenOffersWithLimit(ApplyView& view, Keylet const& directory, std::size_t return deletedOffersCount; } +TER +notTooManyOffers(ReadView const& view, uint256 const& nftokenID) +{ + std::size_t totalOffers = 0; + + { + Dir const buys(view, keylet::nft_buys(nftokenID)); + for (auto iter = buys.begin(); iter != buys.end(); iter.next_page()) + { + totalOffers += iter.page_size(); + if (totalOffers > maxDeletableTokenOfferEntries) + return tefTOO_BIG; + } + } + + { + Dir const sells(view, keylet::nft_sells(nftokenID)); + for (auto iter = sells.begin(); iter != sells.end(); iter.next_page()) + { + totalOffers += iter.page_size(); + if (totalOffers > maxDeletableTokenOfferEntries) + return tefTOO_BIG; + } + } + return tesSUCCESS; +} + bool deleteTokenOffer(ApplyView& view, std::shared_ptr const& offer) {