From 283bc3ea39d5beb39678d4bff11035b9b86cd139 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Thu, 30 Oct 2025 21:31:03 +0000 Subject: [PATCH] Remove directory size limit (#5935) This change introduces the `fixDirectoryLimit` amendment to remove the directory pages limit. We found that the directory size limit is easier to hit than originally assumed, and there is no good reason to keep this limit, since the object reserve provides the necessary incentive to avoid creating unnecessary objects on the ledger. --- include/xrpl/protocol/Protocol.h | 5 +- include/xrpl/protocol/detail/features.macro | 3 +- src/test/app/Credentials_test.cpp | 34 +++++ src/test/jtx.h | 1 + src/test/jtx/directory.h | 81 +++++++++++ src/test/jtx/impl/directory.cpp | 145 ++++++++++++++++++++ src/test/ledger/Directory_test.cpp | 88 ++++++++++++ src/xrpld/ledger/detail/ApplyView.cpp | 18 ++- 8 files changed, 371 insertions(+), 4 deletions(-) create mode 100644 src/test/jtx/directory.h create mode 100644 src/test/jtx/impl/directory.cpp diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 898fd06fbd..3d8869a0ec 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -56,7 +56,10 @@ std::size_t constexpr oversizeMetaDataCap = 5200; /** The maximum number of entries per directory page */ std::size_t constexpr dirNodeMaxEntries = 32; -/** The maximum number of pages allowed in a directory */ +/** The maximum number of pages allowed in a directory + + Made obsolete by fixDirectoryLimit amendment. +*/ std::uint64_t constexpr dirNodeMaxPages = 262144; /** The maximum number of items in an NFT page */ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 679c55b96c..995279d845 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,9 +29,8 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. -// If you add an amendment here, then do not forget to increment `numFeatures` -// in include/xrpl/protocol/Feature.h. +XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PriceOracleOrder, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (MPTDeliveredAmount, Supported::no, VoteBehavior::DefaultNo) XRPL_FIX (AMMClawbackRounding, Supported::no, VoteBehavior::DefaultNo) diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 54826cbb12..cbcf8ce0ac 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -568,6 +568,39 @@ struct Credentials_test : public beast::unit_test::suite jle[jss::result][jss::node]["CredentialType"] == strHex(std::string_view(credType))); } + + { + testcase("Credentials fail, directory full"); + std::uint32_t const issuerSeq{env.seq(issuer) + 1}; + env(ticket::create(issuer, 63)); + env.close(); + + // Everything below can only be tested on open ledger. + auto const res1 = directory::bumpLastPage( + env, + directory::maximumPageIndex(env), + keylet::ownerDir(issuer.id()), + directory::adjustOwnerNode); + BEAST_EXPECT(res1); + + auto const jv = credentials::create(issuer, subject, credType); + env(jv, ter(tecDIR_FULL)); + // Free one directory entry by using a ticket + env(noop(issuer), ticket::use(issuerSeq + 40)); + + // Fill subject directory + env(ticket::create(subject, 63)); + auto const res2 = directory::bumpLastPage( + env, + directory::maximumPageIndex(env), + keylet::ownerDir(subject.id()), + directory::adjustOwnerNode); + BEAST_EXPECT(res2); + env(jv, ter(tecDIR_FULL)); + + // End test + env.close(); + } } { @@ -1094,6 +1127,7 @@ struct Credentials_test : public beast::unit_test::suite testSuccessful(all); testCredentialsDelete(all); testCreateFailed(all); + testCreateFailed(all - fixDirectoryLimit); testAcceptFailed(all); testDeleteFailed(all); testFeatureFailed(all - featureCredentials); diff --git a/src/test/jtx.h b/src/test/jtx.h index 6347b9dcf9..3d3a4f41f8 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/directory.h b/src/test/jtx/directory.h new file mode 100644 index 0000000000..67e9900c6c --- /dev/null +++ b/src/test/jtx/directory.h @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_DIRECTORY_H_INCLUDED +#define RIPPLE_TEST_JTX_DIRECTORY_H_INCLUDED + +#include + +#include +#include +#include + +#include +#include + +namespace ripple::test::jtx { + +/** Directory operations. */ +namespace directory { + +enum Error { + DirectoryRootNotFound, + DirectoryTooSmall, + DirectoryPageDuplicate, + DirectoryPageNotFound, + InvalidLastPage, + AdjustmentError +}; + +/// Move the position of the last page in the user's directory on open ledger to +/// newLastPage. Requirements: +/// - directory must have at least two pages (root and one more) +/// - adjust should be used to update owner nodes of the objects affected +/// - newLastPage must be greater than index of the last page in the directory +/// +/// Use this to test tecDIR_FULL errors in open ledger. +/// NOTE: effects will be DISCARDED on env.close() +auto +bumpLastPage( + Env& env, + std::uint64_t newLastPage, + Keylet directory, + std::function adjust) + -> Expected; + +/// Implementation of adjust for the most common ledger entry, i.e. one where +/// page index is stored in sfOwnerNode (and only there). Pass this function +/// to bumpLastPage if the last page of directory has only objects +/// of this kind (e.g. ticket, DID, offer, deposit preauth, MPToken etc.) +bool +adjustOwnerNode(ApplyView& view, uint256 key, std::uint64_t page); + +inline auto +maximumPageIndex(Env const& env) -> std::uint64_t +{ + if (env.enabled(fixDirectoryLimit)) + return std::numeric_limits::max(); + return dirNodeMaxPages - 1; +} + +} // namespace directory + +} // namespace ripple::test::jtx + +#endif diff --git a/src/test/jtx/impl/directory.cpp b/src/test/jtx/impl/directory.cpp new file mode 100644 index 0000000000..f279ce2bb7 --- /dev/null +++ b/src/test/jtx/impl/directory.cpp @@ -0,0 +1,145 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace ripple::test::jtx { + +/** Directory operations. */ +namespace directory { + +auto +bumpLastPage( + Env& env, + std::uint64_t newLastPage, + Keylet directory, + std::function adjust) + -> Expected +{ + Expected res{}; + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) -> bool { + Sandbox sb(&view, tapNONE); + + // Find the root page + auto sleRoot = sb.peek(directory); + if (!sleRoot) + { + res = Unexpected(DirectoryRootNotFound); + return false; + } + + // Find last page + auto const lastIndex = sleRoot->getFieldU64(sfIndexPrevious); + if (lastIndex == 0) + { + res = Unexpected(DirectoryTooSmall); + return false; + } + + if (sb.exists(keylet::page(directory, newLastPage))) + { + res = Unexpected(DirectoryPageDuplicate); + return false; + } + + if (lastIndex >= newLastPage) + { + res = Unexpected(InvalidLastPage); + return false; + } + + auto slePage = sb.peek(keylet::page(directory, lastIndex)); + if (!slePage) + { + res = Unexpected(DirectoryPageNotFound); + return false; + } + + // Copy its data and delete the page + auto indexes = slePage->getFieldV256(sfIndexes); + auto prevIndex = slePage->at(~sfIndexPrevious); + auto owner = slePage->at(~sfOwner); + sb.erase(slePage); + + // Create new page to replace slePage + auto sleNew = + std::make_shared(keylet::page(directory, newLastPage)); + sleNew->setFieldH256(sfRootIndex, directory.key); + sleNew->setFieldV256(sfIndexes, indexes); + if (owner) + sleNew->setAccountID(sfOwner, *owner); + if (prevIndex) + sleNew->setFieldU64(sfIndexPrevious, *prevIndex); + sb.insert(sleNew); + + // Adjust root previous and previous node's next + sleRoot->setFieldU64(sfIndexPrevious, newLastPage); + if (prevIndex.value_or(0) == 0) + sleRoot->setFieldU64(sfIndexNext, newLastPage); + else + { + auto slePrev = sb.peek(keylet::page(directory, *prevIndex)); + if (!slePrev) + { + res = Unexpected(DirectoryPageNotFound); + return false; + } + slePrev->setFieldU64(sfIndexNext, newLastPage); + sb.update(slePrev); + } + sb.update(sleRoot); + + // Fixup page numbers in the objects referred by indexes + if (adjust) + for (auto const key : indexes) + { + if (!adjust(sb, key, newLastPage)) + { + res = Unexpected(AdjustmentError); + return false; + } + } + + sb.apply(view); + return true; + }); + + return res; +} + +bool +adjustOwnerNode(ApplyView& view, uint256 key, std::uint64_t page) +{ + auto sle = view.peek({ltANY, key}); + if (sle && sle->isFieldPresent(sfOwnerNode)) + { + sle->setFieldU64(sfOwnerNode, page); + view.update(sle); + return true; + } + + return false; +} + +} // namespace directory + +} // namespace ripple::test::jtx diff --git a/src/test/ledger/Directory_test.cpp b/src/test/ledger/Directory_test.cpp index 9e8d40e0cc..07e42c1508 100644 --- a/src/test/ledger/Directory_test.cpp +++ b/src/test/ledger/Directory_test.cpp @@ -23,9 +23,11 @@ #include #include #include +#include #include #include +#include namespace ripple { namespace test { @@ -490,6 +492,91 @@ struct Directory_test : public beast::unit_test::suite } } + void + testDirectoryFull() + { + using namespace test::jtx; + Account alice("alice"); + + auto const testCase = [&, this](FeatureBitset features, auto setup) { + using namespace test::jtx; + + Env env(*this, features); + env.fund(XRP(20000), alice); + env.close(); + + auto const [lastPage, full] = setup(env); + + // Populate root page and last page + for (int i = 0; i < 63; ++i) + env(credentials::create(alice, alice, std::to_string(i))); + env.close(); + + // NOTE, everything below can only be tested on open ledger because + // there is no transaction type to express what bumpLastPage does. + + // Bump position of last page from 1 to highest possible + auto const res = directory::bumpLastPage( + env, + lastPage, + keylet::ownerDir(alice.id()), + [lastPage, this]( + ApplyView& view, uint256 key, std::uint64_t page) { + auto sle = view.peek({ltCREDENTIAL, key}); + if (!BEAST_EXPECT(sle)) + return false; + + BEAST_EXPECT(page == lastPage); + sle->setFieldU64(sfIssuerNode, page); + // sfSubjectNode is not set in self-issued credentials + view.update(sle); + return true; + }); + BEAST_EXPECT(res); + + // Create one more credential + env(credentials::create(alice, alice, std::to_string(63))); + + // Not enough space for another object if full + auto const expected = full ? ter{tecDIR_FULL} : ter{tesSUCCESS}; + env(credentials::create(alice, alice, "foo"), expected); + + // Destroy all objects in directory + for (int i = 0; i < 64; ++i) + env(credentials::deleteCred( + alice, alice, alice, std::to_string(i))); + + if (!full) + env(credentials::deleteCred(alice, alice, alice, "foo")); + + // Verify directory is empty. + auto const sle = env.le(keylet::ownerDir(alice.id())); + BEAST_EXPECT(sle == nullptr); + + // Test completed + env.close(); + }; + + testCase( + testable_amendments() - fixDirectoryLimit, + [this](Env&) -> std::tuple { + testcase("directory full without fixDirectoryLimit"); + return {dirNodeMaxPages - 1, true}; + }); + testCase( + testable_amendments(), // + [this](Env&) -> std::tuple { + testcase("directory not full with fixDirectoryLimit"); + return {dirNodeMaxPages - 1, false}; + }); + testCase( + testable_amendments(), // + [this](Env&) -> std::tuple { + testcase("directory full with fixDirectoryLimit"); + return {std::numeric_limits::max(), true}; + }); + } + void run() override { @@ -498,6 +585,7 @@ struct Directory_test : public beast::unit_test::suite testRipd1353(); testEmptyChain(); testPreviousTxnID(); + testDirectoryFull(); } }; diff --git a/src/xrpld/ledger/detail/ApplyView.cpp b/src/xrpld/ledger/detail/ApplyView.cpp index 3191b47cbb..9d9273cb0d 100644 --- a/src/xrpld/ledger/detail/ApplyView.cpp +++ b/src/xrpld/ledger/detail/ApplyView.cpp @@ -23,6 +23,9 @@ #include #include +#include +#include + namespace ripple { std::optional @@ -92,8 +95,21 @@ ApplyView::dirAdd( return page; } + // We rely on modulo arithmetic of unsigned integers (guaranteed in + // [basic.fundamental] paragraph 2) to detect page representation overflow. + // For signed integers this would be UB, hence static_assert here. + static_assert(std::is_unsigned_v); + // Defensive check against breaking changes in compiler. + static_assert([](std::type_identity) constexpr -> T { + T tmp = std::numeric_limits::max(); + return ++tmp; + }(std::type_identity{}) == 0); + ++page; // Check whether we're out of pages. - if (++page >= dirNodeMaxPages) + if (page == 0) + return std::nullopt; + if (!rules().enabled(fixDirectoryLimit) && + page >= dirNodeMaxPages) // Old pages limit return std::nullopt; // We are about to create a new node; we'll link it to