From a12ab0496cb450fa6ea235a612cd15d2dc4937bc Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 16 Jul 2026 11:18:26 -0400 Subject: [PATCH 01/68] fix: Add missing pseudo-account checks --- .../tx/transactors/account/AccountDelete.cpp | 2 ++ .../tx/transactors/payment/DepositPreauth.cpp | 9 +++++- src/test/app/AccountDelete_test.cpp | 24 ++++++++++++++-- src/test/app/DepositAuth_test.cpp | 28 +++++++++++++++++-- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 0055fce403..ce027f4cad 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -241,6 +241,8 @@ AccountDelete::preclaim(PreclaimContext const& ctx) if (!ctx.tx.isFieldPresent(sfCredentialIDs)) { // Check whether the destination account requires deposit authorization. + // This also checks if destination is a pseudo-account, since pseudo-accounts have the + // lsfDepositAuth flag set by default if (sleDst->isFlag(lsfDepositAuth)) { if (!ctx.view.exists(keylet::depositPreauth(dst, account))) diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index d3e2af86ef..aa2c6e42bd 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -103,9 +103,16 @@ DepositPreauth::preclaim(PreclaimContext const& ctx) { // Verify that the Authorize account is present in the ledger. AccountID const auth{ctx.tx[sfAuthorize]}; - if (!ctx.view.exists(keylet::account(auth))) + auto const sleAuth = ctx.view.read(keylet::account(auth)); + if (!sleAuth) return tecNO_TARGET; + if (ctx.view.rules().enabled(fixCleanup3_3_0) && isPseudoAccount(sleAuth)) + { + JLOG(ctx.j.debug()) << "Authorized account is a pseudo-account."; + return tecNO_PERMISSION; + } + // Verify that the Preauth entry they asked to add is not already // in the ledger. if (ctx.view.exists(keylet::depositPreauth(account, auth))) diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 399696ec0d..8fbb786caf 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -687,7 +689,7 @@ public: } void - testDest() + testDest(FeatureBitset features) { testcase("Destination Constraints"); @@ -698,7 +700,7 @@ public: Account const carol{"carol"}; Account const daria{"daria"}; - Env env{*this}; + Env env{*this, features}; env.fund(XRP(100000), alice, becky, carol); env.close(); @@ -711,6 +713,16 @@ public: env(fset(carol, asfRequireDest)); env.close(); + // Need to create a pseudo-account + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(tx); + env.close(); + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + Account const vaultPseudo{"vaultPseudo", sleVault->at(sfAccount)}; + // Close enough ledgers to be able to delete becky's account. incLgrSeqForAccDel(env, becky); @@ -730,6 +742,10 @@ public: env(acctdelete(becky, alice), Fee(acctDelFee), Ter(tecNO_PERMISSION)); env.close(); + // becky attempts to delete her account using a pseudo-account as the + // destination, which fails since pseudo-accounts have deposit auth enabled. + env(acctdelete(becky, vaultPseudo), Fee(acctDelFee), Ter(tecNO_PERMISSION)); + // alice preauthorizes deposits from becky. Now becky can delete her // account and forward the leftovers to alice. env(deposit::auth(alice, becky)); @@ -1076,6 +1092,7 @@ public: void run() override { + auto const all{jtx::testableAmendments()}; testBasics(); testDirectories(); testOwnedTypes(); @@ -1083,7 +1100,8 @@ public: testImplicitlyCreatedTrustline(); testBalanceTooSmallForFee(); testWithTickets(); - testDest(); + testDest(all); + testDest(all - fixCleanup3_3_0); testDestinationDepositAuthCredentials(); testDeleteCredentialsOwner(); } diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index c75bdeaf3a..5943f5ca95 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -444,7 +446,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite } void - testInvalid() + testInvalid(FeatureBitset features) { testcase("Invalid"); @@ -453,7 +455,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite Account const becky{"becky"}; Account const carol{"carol"}; - Env env(*this); + Env env(*this, features); // Tell env about alice, becky and carol since they are not yet funded. env.memoize(alice); @@ -559,6 +561,25 @@ struct DepositPreauth_test : public beast::unit_test::Suite env.close(); env.require(Owners(alice, 0)); env.require(Owners(becky, 0)); + + { + // alice attempts to authorize a pseudo-account. + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = becky, .asset = xrpIssue()}); + env(tx); + env.close(); + + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + Account const vaultPseudo{"vault", sleVault->at(sfAccount)}; + + auto const expectedResult = + features[fixCleanup3_3_0] ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS); + env(deposit::auth(alice, vaultPseudo), expectedResult); + env.close(); + env.require(Owners(alice, features[fixCleanup3_3_0] ? 0 : 1)); + } } void @@ -1419,8 +1440,9 @@ struct DepositPreauth_test : public beast::unit_test::Suite run() override { testEnable(); - testInvalid(); auto const supported{jtx::testableAmendments()}; + testInvalid(supported); + testInvalid(supported - fixCleanup3_3_0); testPayment(supported - featureCredentials); testPayment(supported); testCredentialsPayment(); From d569f7db9e5869df2cfa7d365275ac25ef2852b2 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 16 Jul 2026 11:42:27 -0400 Subject: [PATCH 02/68] fix: Prevent credentials from being created for pseudo-accounts --- .../credentials/CredentialCreate.cpp | 10 +++++++- src/test/app/Credentials_test.cpp | 24 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index e902ee73a6..29e262d9ec 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -84,7 +84,9 @@ CredentialCreate::preclaim(PreclaimContext const& ctx) auto const credType(ctx.tx[sfCredentialType]); auto const subject = ctx.tx[sfSubject]; - if (!ctx.view.exists(keylet::account(subject))) + auto const subjectSle = ctx.view.read(keylet::account(subject)); + + if (!subjectSle) { JLOG(ctx.j.trace()) << "Subject doesn't exist."; return tecNO_TARGET; @@ -96,6 +98,12 @@ CredentialCreate::preclaim(PreclaimContext const& ctx) return tecDUPLICATE; } + if (ctx.view.rules().enabled(fixCleanup3_3_0) && isPseudoAccount(subjectSle)) + { + JLOG(ctx.j.trace()) << "Subject is a pseudo-account."; + return tecNO_PERMISSION; + } + return tesSUCCESS; } diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index 1f6ec012c8..a26ceb261c 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -425,7 +427,6 @@ struct Credentials_test : public beast::unit_test::Suite Account const subject{"subject"}; { - using namespace jtx; Env env{*this, features}; env.fund(XRP(5000), subject, issuer); @@ -566,10 +567,27 @@ struct Credentials_test : public beast::unit_test::Suite // End test env.close(); } + + { + testcase("Credentials fail, subject is a pseudo-account."); + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = subject, .asset = xrpIssue()}); + env(tx); + env.close(); + + auto const sleVault = env.le(keylet); + if (!BEAST_EXPECT(sleVault)) + return; + Account const vaultPseudo{"vault", sleVault->at(sfAccount)}; + auto const expectedResult = + features[fixCleanup3_3_0] ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS); + + env(credentials::create(vaultPseudo, issuer, credType), expectedResult); + env.close(); + } } { - using namespace jtx; Env env{*this, features}; env.fund(XRP(5000), issuer); @@ -583,7 +601,6 @@ struct Credentials_test : public beast::unit_test::Suite } { - using namespace jtx; Env env{*this, features}; auto const reserve = drops(env.current()->fees().reserve); @@ -1157,6 +1174,7 @@ struct Credentials_test : public beast::unit_test::Suite testCredentialsDelete(all); testCreateFailed(all); testCreateFailed(all - fixDirectoryLimit); + testCreateFailed(all - fixCleanup3_3_0); testAcceptFailed(all); testDeleteFailed(all); testFeatureFailed(all - featureCredentials); From 846369cbe7935ff172777a91a8e5c68e3ee1cc27 Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Thu, 16 Jul 2026 17:16:09 +0100 Subject: [PATCH 03/68] fix: Use hashmap for quicker lookup in assembleAdd --- include/xrpl/protocol/STPathSet.h | 35 +++++++--- src/libxrpl/protocol/STPathSet.cpp | 24 +++---- src/test/app/Path_test.cpp | 99 +++++++++++++++++++++++++++++ src/xrpld/rpc/detail/Pathfinder.cpp | 8 +-- 4 files changed, 139 insertions(+), 27 deletions(-) diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index 23f4e653c4..d527e2479f 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -108,6 +109,9 @@ public: [[nodiscard]] bool isType(Type const& pe) const; + [[nodiscard]] size_t + getHash() const; + bool operator==(STPathElement const& t) const; @@ -171,12 +175,23 @@ public: reserve(size_t s); }; +template +void +hash_append(Hasher& h, STPath const& p) noexcept +{ + for (auto const& e : p) + { + beast::hash_append(h, e.getHash()); + } +} + //------------------------------------------------------------------------------ // A set of zero or more payment paths class STPathSet final : public STBase, public CountedObject { std::vector value_; + xrpl::hardened_hash_set seenHashes_; public: STPathSet() = default; @@ -205,9 +220,6 @@ public: std::vector::const_reference operator[](std::vector::size_type n) const; - std::vector::reference - operator[](std::vector::size_type n); - [[nodiscard]] std::vector::const_iterator begin() const; @@ -227,6 +239,9 @@ public: void emplaceBack(Args&&... args); + [[nodiscard]] bool + contains(STPath const& path) const; + private: STBase* copy(std::size_t n, void* buf) const override; @@ -515,12 +530,6 @@ STPathSet::operator[](std::vector::size_type n) const return value_[n]; } -inline std::vector::reference -STPathSet::operator[](std::vector::size_type n) -{ - return value_[n]; -} - inline std::vector::const_iterator STPathSet::begin() const { @@ -549,6 +558,7 @@ inline void STPathSet::pushBack(STPath const& e) { value_.push_back(e); + seenHashes_.emplace(value_.back()); } template @@ -556,6 +566,13 @@ inline void STPathSet::emplaceBack(Args&&... args) { value_.emplace_back(std::forward(args)...); + seenHashes_.emplace(value_.back()); +} + +inline bool +STPathSet::contains(STPath const& path) const +{ + return seenHashes_.contains(path); } } // namespace xrpl diff --git a/src/libxrpl/protocol/STPathSet.cpp b/src/libxrpl/protocol/STPathSet.cpp index 8987d05f1e..658aaa65dd 100644 --- a/src/libxrpl/protocol/STPathSet.cpp +++ b/src/libxrpl/protocol/STPathSet.cpp @@ -51,6 +51,12 @@ STPathElement::getHash(STPathElement const& element) return (hashAccount ^ hashCurrency ^ hashIssuer); } +[[nodiscard]] size_t +STPathElement::getHash() const +{ + return STPathElement::getHash(*this); +} + STPathSet::STPathSet(SerialIter& sit, SField const& name) : STBase(name) { std::vector path; @@ -126,21 +132,15 @@ STPathSet::move(std::size_t n, void* buf) bool STPathSet::assembleAdd(STPath const& base, STPathElement const& tail) { // assemble base+tail and add it to the set if it's not a duplicate - value_.push_back(base); + STPath combined = base; + combined.pushBack(tail); - auto it = value_.rbegin(); - - STPath& newPath = *it; - newPath.pushBack(tail); - - while (++it != value_.rend()) + if (!seenHashes_.insert(combined).second) { - if (*it == newPath) - { - value_.pop_back(); - return false; - } + return false; } + + value_.push_back(std::move(combined)); return true; } diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 8f19a419a0..d6e4fec60b 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include @@ -1866,6 +1867,103 @@ public: BEAST_EXPECT(same(st, stpath(gw_, ipe(xrpIssue())))); } + void + testAssembleAddDeduplication() + { + testcase("STPathSet::assembleAdd deduplication — O(N^2) regression"); + + static constexpr std::string_view kAccount1 = "A3F19C7B2E5D08146FB93A7C0E2D5184BC6F3A09"; + static constexpr std::string_view kAccount2 = "1D7E4B90C2A6F3851E0B9D47A2C5F8136E0A4B7D"; + static constexpr std::string_view kAccount3 = "F08C36A1D95E27B40CA1F63E8D204B7950E1C3A6"; + static constexpr std::string_view kAccount4 = "4B6209E7F1A3C85D0E94B27Af3D6018C5A7E92B4"; + static constexpr std::string_view kAccount5 = "9E2D7041BCA3F6589D013E7B2A4C6F80159D3E7A"; + static constexpr std::string_view kAccount6 = "7C5A91E384F2D06BA19C4E73D820F516B3A9C0E4"; + static constexpr std::string_view kAccount7 = "2F8B043C6A1E9D75B0C38E14F6A2D509731BC4E8"; + static constexpr std::string_view kAccount8 = "E61D9A30F47C285BA0D31E96C7B4F802513A8D6F"; + + static constexpr AccountID kAccountID1{kAccount1}; + static constexpr AccountID kAccountID2{kAccount2}; + static constexpr AccountID kAccountID3{kAccount3}; + static constexpr AccountID kAccountID4{kAccount4}; + static constexpr AccountID kAccountID5{kAccount5}; + static constexpr AccountID kAccountID6{kAccount6}; + static constexpr AccountID kAccountID7{kAccount7}; + static constexpr AccountID kAccountID8{kAccount8}; + + auto ps = STPathSet{}; + + auto createPathElements = [](auto const& account1, auto const& account2) { + auto base = STPath{}; + base.pushBack( + STPathElement{STPathElement::TypeAccount, account1, xrpCurrency(), account1}); + auto tail = + STPathElement{STPathElement::TypeAccount, account2, xrpCurrency(), account2}; + return std::make_pair(base, tail); + }; + + { + auto [base, tail] = createPathElements(kAccountID1, kAccountID2); + + for (auto i = 0uz; i < 10000; ++i) + { + ps.assembleAdd(base, tail); + } + + BEAST_EXPECT(ps.size() == 1); + } + + { + auto [base, tail] = createPathElements(kAccountID3, kAccountID4); + ps.assembleAdd(base, tail); + } + + { + auto [base, tail] = createPathElements(kAccountID5, kAccountID6); + ps.assembleAdd(base, tail); + } + + { + auto [base, tail] = createPathElements(kAccountID7, kAccountID8); + + auto before = ps.size(); + + for (auto i = 0uz; i < 10000; ++i) + { + ps.assembleAdd(base, tail); + } + + BEAST_EXPECT(ps.size() - before == 1); + } + + { + auto [base, tail] = createPathElements(kAccountID1, kAccountID3); + auto copy = base; + copy.pushBack(tail); + + auto before = ps.size(); + + ps.pushBack(copy); + ps.assembleAdd(base, tail); + + BEAST_EXPECT(ps.size() - before == 1); + } + + { + auto [base, tail] = createPathElements(kAccountID2, kAccountID4); + auto copy = base; + copy.pushBack(tail); + + auto before = ps.size(); + + ps.emplaceBack(copy); + ps.assembleAdd(base, tail); + + BEAST_EXPECT(ps.size() - before == 1); + } + + BEAST_EXPECT(ps.size() == 6); + } + void run() override { @@ -1878,6 +1976,7 @@ public: issuesPathNegativeRippleClientIssue23Smaller(); issuesPathNegativeRippleClientIssue23Larger(); qualityPathsQualitySetAndTest(); + testAssembleAddDeduplication(); trustAutoClearTrustNormalClear(); trustAutoClearTrustAutoClear(); norippleCombinations(); diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index 5b7f1415a2..642b5c4253 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -962,14 +962,10 @@ Pathfinder::isNoRippleOut(STPath const& currentPath) void addUniquePath(STPathSet& pathSet, STPath const& path) { - // TODO(tom): building an STPathSet this way is quadratic in the size - // of the STPathSet! - for (auto const& p : pathSet) + if (!pathSet.contains(path)) { - if (p == path) - return; + pathSet.pushBack(path); } - pathSet.pushBack(path); } void From d60955e2fc444225161665ae3881d6215ad393ab Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Thu, 16 Jul 2026 17:41:34 +0100 Subject: [PATCH 04/68] fix: Acquire lock on getClosedLedgerHash --- src/test/app/LedgerReplay_test.cpp | 2 +- src/test/overlay/reduce_relay_test.cpp | 2 +- src/xrpld/overlay/Peer.h | 2 +- src/xrpld/overlay/detail/PeerImp.h | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 4cc83608d6..f08d6548e0 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -333,7 +333,7 @@ public: setPublisherListSequence(PublicKey const&, std::size_t const) override { } - [[nodiscard]] uint256 const& + [[nodiscard]] uint256 getClosedLedgerHash() const override { static uint256 const kHash{}; diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 2f42313037..6b991836d1 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -140,7 +140,7 @@ public: setPublisherListSequence(PublicKey const&, std::size_t const) override { } - [[nodiscard]] uint256 const& + [[nodiscard]] uint256 getClosedLedgerHash() const override { static uint256 const kHash{}; diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h index 23a45dc512..328b9075d6 100644 --- a/src/xrpld/overlay/Peer.h +++ b/src/xrpld/overlay/Peer.h @@ -117,7 +117,7 @@ public: // Ledger // - [[nodiscard]] virtual uint256 const& + [[nodiscard]] virtual uint256 getClosedLedgerHash() const = 0; [[nodiscard]] virtual bool hasLedger(uint256 const& hash, std::uint32_t seq) const = 0; diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index ea6eccd656..0085927550 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -426,9 +426,10 @@ public: // Ledger // - uint256 const& + uint256 getClosedLedgerHash() const override { + std::scoped_lock const sl{recentLock_}; return closedLedgerHash_; } From 6c793edf72c8d64934633b66591d384e93b6dd6f Mon Sep 17 00:00:00 2001 From: Timothy Banks Date: Thu, 16 Jul 2026 20:07:36 +0100 Subject: [PATCH 05/68] fix: Reject oversized TMPing messages --- src/xrpld/overlay/Message.h | 3 +++ src/xrpld/overlay/detail/ProtocolMessage.h | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h index 2e187a2a4d..e942ed244a 100644 --- a/src/xrpld/overlay/Message.h +++ b/src/xrpld/overlay/Message.h @@ -19,6 +19,9 @@ namespace xrpl { constexpr std::size_t kMaximumMessageSize = megabytes(64); +// Ping messages should be much smaller than the maximum message size, +// so we define a separate limit for them. +constexpr std::size_t kMaximumPingMessageSize = kilobytes(1); // VFALCO NOTE If we forward declare Message and write out shared_ptr // instead of using the in-class type alias, we can remove the diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h index ef1bc8cb2b..7c22a8e84c 100644 --- a/src/xrpld/overlay/detail/ProtocolMessage.h +++ b/src/xrpld/overlay/detail/ProtocolMessage.h @@ -359,6 +359,13 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin return result; } + if (header->messageType == protocol::mtPING && + header->uncompressedSize + header->headerSize > kMaximumPingMessageSize) + { + result.second = make_error_code(boost::system::errc::message_size); + return result; + } + // We don't have the whole message yet. This isn't an error but we have // nothing to do. if (header->totalWireSize > size) From e1dae5f775fcb75ecc6b77b3915276f04ab25b41 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:32:54 -0400 Subject: [PATCH 06/68] refactor: Use `tecPSEUDO_ACCOUNT` instead of `tecNO_PERMISSION` where relevant --- .../tx/transactors/credentials/CredentialCreate.cpp | 2 +- src/libxrpl/tx/transactors/delegate/DelegateSet.cpp | 2 +- src/libxrpl/tx/transactors/payment/DepositPreauth.cpp | 2 +- src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp | 2 +- src/test/app/Credentials_test.cpp | 2 +- src/test/app/Delegate_test.cpp | 4 ++-- src/test/app/DepositAuth_test.cpp | 2 +- src/test/app/Sponsor_test.cpp | 8 ++++---- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index 29e262d9ec..5cce1a7de8 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -101,7 +101,7 @@ CredentialCreate::preclaim(PreclaimContext const& ctx) if (ctx.view.rules().enabled(fixCleanup3_3_0) && isPseudoAccount(subjectSle)) { JLOG(ctx.j.trace()) << "Subject is a pseudo-account."; - return tecNO_PERMISSION; + return tecPSEUDO_ACCOUNT; } return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp index 96e6c9e443..12edb43bff 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp @@ -57,7 +57,7 @@ DelegateSet::preclaim(PreclaimContext const& ctx) return tecNO_TARGET; if (isPseudoAccount(sleAuthorize)) - return tecNO_PERMISSION; + return tecPSEUDO_ACCOUNT; // Deleting the delegate object is invalid if it doesn’t exist. if (ctx.tx.getFieldArray(sfPermissions).empty() && diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp index aa2c6e42bd..c11c0ed916 100644 --- a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp @@ -110,7 +110,7 @@ DepositPreauth::preclaim(PreclaimContext const& ctx) if (ctx.view.rules().enabled(fixCleanup3_3_0) && isPseudoAccount(sleAuth)) { JLOG(ctx.j.debug()) << "Authorized account is a pseudo-account."; - return tecNO_PERMISSION; + return tecPSEUDO_ACCOUNT; } // Verify that the Preauth entry they asked to add is not already diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp index 2b6ab8cf15..24bfaad2f8 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp @@ -146,7 +146,7 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx) // Pseudo-accounts cannot participate in sponsorship. if (isPseudoAccount(sponsorAccSle) || isPseudoAccount(sponseeSle)) - return tecNO_PERMISSION; + return tecPSEUDO_ACCOUNT; auto const sponsorshipSle = ctx.view.read(keylet::sponsorship(sponsorID, sponseeID)); diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp index a26ceb261c..ff3489884e 100644 --- a/src/test/app/Credentials_test.cpp +++ b/src/test/app/Credentials_test.cpp @@ -580,7 +580,7 @@ struct Credentials_test : public beast::unit_test::Suite return; Account const vaultPseudo{"vault", sleVault->at(sfAccount)}; auto const expectedResult = - features[fixCleanup3_3_0] ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS); + features[fixCleanup3_3_0] ? Ter(tecPSEUDO_ACCOUNT) : Ter(tesSUCCESS); env(credentials::create(vaultPseudo, issuer, credType), expectedResult); env.close(); diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 257ed33619..a8ddc27e6f 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -236,7 +236,7 @@ class Delegate_test : public beast::unit_test::Suite env(delegate::set(gw, Account("unknown"), {"Payment"}), Ter(tecNO_TARGET)); } - // Delegating to a pseudo-account is not allowed, should return tecNO_PERMISSION + // Delegating to a pseudo-account is not allowed, should return tecPSEUDO_ACCOUNT { Vault const vault{env}; auto [tx, keylet] = vault.create({.owner = gw, .asset = xrpIssue()}); @@ -246,7 +246,7 @@ class Delegate_test : public beast::unit_test::Suite auto const sleVault = env.le(keylet); BEAST_EXPECT(sleVault); Account const vaultPseudo{"vault", sleVault->at(sfAccount)}; - env(delegate::set(gw, vaultPseudo, {"Payment"}), Ter(tecNO_PERMISSION)); + env(delegate::set(gw, vaultPseudo, {"Payment"}), Ter(tecPSEUDO_ACCOUNT)); } // non-delegable transaction diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 5943f5ca95..881441e0f9 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -575,7 +575,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite Account const vaultPseudo{"vault", sleVault->at(sfAccount)}; auto const expectedResult = - features[fixCleanup3_3_0] ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS); + features[fixCleanup3_3_0] ? Ter(tecPSEUDO_ACCOUNT) : Ter(tesSUCCESS); env(deposit::auth(alice, vaultPseudo), expectedResult); env.close(); env.require(Owners(alice, features[fixCleanup3_3_0] ? 0 : 1)); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index f20aac68f9..1012a7d0b6 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -353,17 +353,17 @@ public: Account const pseudoAcc("vault", vaultSle->getAccountID(sfAccount)); env.memoize(pseudoAcc); - // Sponsee is a pseudo account -> tecNO_PERMISSION + // Sponsee is a pseudo account -> tecPSEUDO_ACCOUNT env(sponsor::set(sp, 0, 100, XRP(100)), sponsor::SponseeAcc(pseudoAcc), - Ter(tecNO_PERMISSION)); + Ter(tecPSEUDO_ACCOUNT)); env.close(); - // Sponsor is a pseudo account -> tecNO_PERMISSION + // Sponsor is a pseudo account -> tecPSEUDO_ACCOUNT // (submitted by bob with counterpartySponsor pointing to pseudo account) env(sponsor::set(bob, tfDeleteObject), sponsor::CounterpartySponsor(pseudoAcc), - Ter(tecNO_PERMISSION)); + Ter(tecPSEUDO_ACCOUNT)); env.close(); } From a5af6b4e4aa4035cc7d6b3c23602fd9fb287919e Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:31:34 -0400 Subject: [PATCH 07/68] fix: Charge kFeeHeavyBurdenRpc in doChannelVerify --- src/test/app/PayChan_test.cpp | 155 +++++++++++++++++- src/xrpld/rpc/handlers/ChannelVerify.cpp | 3 + .../admin/signing/ChannelAuthorize.cpp | 3 + 3 files changed, 159 insertions(+), 2 deletions(-) diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 5068472135..703bcf51a7 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -13,13 +13,18 @@ #include #include +#include +#include +#include + #include #include #include #include #include #include -#include // IWYU pragma: keep +#include +#include #include #include #include @@ -39,6 +44,9 @@ #include #include #include +#include +#include +#include #include #include @@ -48,6 +56,7 @@ #include #include #include +#include #include #include @@ -495,7 +504,7 @@ struct PayChan_test : public beast::unit_test::Suite // Owner closes, will close after settleDelay env(claim(alice, chan), Txflags(tfClose)); BEAST_EXPECT(channelExists(*env.current(), chan)); - env.close(settleTimepoint - settleDelay / 2); + env.close(settleTimepoint - (settleDelay / 2)); { // receiver can still claim auto const chanBal = channelBalance(*env.current(), chan); @@ -1587,6 +1596,146 @@ struct PayChan_test : public beast::unit_test::Suite } } + void + testChannelVerifyLoadType(FeatureBitset features) + { + testcase("channel_verify sets kFEE_HEAVY_BURDEN_RPC load type"); + + using namespace jtx; + using namespace std::literals::chrono_literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const channelFunds = XRP(1000); + auto const chanStr = to_string(channel(alice, bob, env.seq(alice))); + + env(create(alice, bob, channelFunds, settleDelay, pk)); + env.close(); + + // Step 1: get a valid signature from channel_authorize + auto const authResult = env.rpc("channel_authorize", "alice", chanStr, "1000"); + auto const sig = authResult[jss::result][jss::signature].asString(); + BEAST_EXPECT(!sig.empty()); + auto const pkHex = strHex(pk.slice()); + + // Step 2: build RPC::JsonContext directly so we can inspect loadType + auto& app = env.app(); + Resource::Charge loadType = Resource::kFeeReferenceRpc; + Resource::Consumer c; + RPC::JsonContext context{ + {.j = env.journal, + .app = app, + .loadType = loadType, + .netOps = app.getOPs(), + .ledgerMaster = app.getLedgerMaster(), + .consumer = c, + .role = Role::USER, + .coro = {}, + .infoSub = {}, + .apiVersion = RPC::kApiVersionIfUnspecified}, + {}, + {}}; + json::Value params; + params[jss::public_key] = pkHex; + params[jss::channel_id] = chanStr; + params[jss::amount] = "1000"; + params[jss::signature] = sig; + context.params = std::move(params); + + // Confirm default before calling handler + BEAST_EXPECT(context.loadType == Resource::kFeeReferenceRpc); + json::Value result; + Gate g; + app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { + context.coro = coro; + result = doChannelVerify(context); + g.signal(); + }); + + using namespace std::chrono_literals; + BEAST_EXPECT(g.waitFor(5s)); + // Signature must verify correctly + BEAST_EXPECT(result[jss::signature_verified].asBool()); + // KEY ASSERTION: loadType must be kFEE_HEAVY_BURDEN_RPC after the fix + // Before fix: this will FAIL because loadType stays kFEE_REFERENCE_RPC (20) + // After fix: this will PASS because loadType is kFEE_HEAVY_BURDEN_RPC (3000) + BEAST_EXPECT(context.loadType == Resource::kFeeHeavyBurdenRpc); + // Confirm the charge is 150x heavier than the current (broken) default + BEAST_EXPECT(context.loadType.cost() == Resource::kFeeHeavyBurdenRpc.cost()); // 3000 + BEAST_EXPECT(context.loadType.cost() != Resource::kFeeReferenceRpc.cost()); // not 20 + } + + void + testChannelAuthorizeLoadType(FeatureBitset features) + { + testcase("channel_authorize sets kFEE_HEAVY_BURDEN_RPC load type"); + + using namespace jtx; + using namespace std::literals::chrono_literals; + + Env env{*this, features}; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + + auto const pk = alice.pk(); + auto const settleDelay = 3600s; + auto const chanStr = to_string(channel(alice, bob, env.seq(alice))); + + env(create(alice, bob, XRP(1000), settleDelay, pk)); + env.close(); + + auto& app = env.app(); + Resource::Charge loadType = Resource::kFeeReferenceRpc; + Resource::Consumer c; + RPC::JsonContext context{ + {.j = env.journal, + .app = app, + .loadType = loadType, + .netOps = app.getOPs(), + .ledgerMaster = app.getLedgerMaster(), + .consumer = c, + .role = Role::ADMIN, // channel_authorize requires ADMIN or canSign() + .coro = {}, + .infoSub = {}, + .apiVersion = RPC::kApiVersionIfUnspecified}, + {}, + {}}; + json::Value params; + params[jss::channel_id] = chanStr; + params[jss::amount] = "1000"; + params[jss::secret] = alice.name(); // use account name as seed + context.params = std::move(params); + + // Confirm default before calling handler + BEAST_EXPECT(context.loadType == Resource::kFeeReferenceRpc); + json::Value result; + Gate g; + app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { + context.coro = coro; + result = doChannelAuthorize(context); + g.signal(); + }); + + using namespace std::chrono_literals; + + BEAST_EXPECT(g.waitFor(5s)); + // Must return a valid signature + BEAST_EXPECT(result.isMember(jss::signature)); + BEAST_EXPECT(!result[jss::signature].asString().empty()); + // KEY ASSERTION: loadType must be kFEE_HEAVY_BURDEN_RPC after the fix + // Before fix: FAILS — stays at kFEE_REFERENCE_RPC (charge=20) + // After fix: PASSES — set to kFEE_HEAVY_BURDEN_RPC (charge=3000) + BEAST_EXPECT(context.loadType == Resource::kFeeHeavyBurdenRpc); + } + void testMalformedPK(FeatureBitset features) { @@ -1983,6 +2132,8 @@ struct PayChan_test : public beast::unit_test::Suite testMetaAndOwnership(features); testAccountDelete(features); testUsingTickets(features); + testChannelVerifyLoadType(features); + testChannelAuthorizeLoadType(features); } public: diff --git a/src/xrpld/rpc/handlers/ChannelVerify.cpp b/src/xrpld/rpc/handlers/ChannelVerify.cpp index 64f616e829..b018b70516 100644 --- a/src/xrpld/rpc/handlers/ChannelVerify.cpp +++ b/src/xrpld/rpc/handlers/ChannelVerify.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,8 @@ doChannelVerify(RPC::JsonContext& context) return RPC::missingFieldError(p); } + context.loadType = Resource::kFeeHeavyBurdenRpc; + std::optional pk; { std::string const strPk = params[jss::public_key].asString(); diff --git a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp index be3ce13d45..d304992261 100644 --- a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,8 @@ doChannelAuthorize(RPC::JsonContext& context) return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); } + context.loadType = Resource::kFeeHeavyBurdenRpc; + auto const& params(context.params); for (auto const& p : {jss::channel_id, jss::amount}) { From 981c2569333f25e47fdcecb5f788774351894631 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:30:47 -0400 Subject: [PATCH 08/68] fix: Use weighted median for close-time offset aggregation --- src/xrpld/app/consensus/RCLConsensus.cpp | 17 +----- src/xrpld/consensus/Consensus.h | 9 ++- src/xrpld/consensus/ConsensusTypes.h | 71 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index 4abf77f578..bddd6f3ffb 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -686,28 +686,17 @@ RCLConsensus::Adaptor::doAccept( // close time reports, and update our clock. if ((mode == ConsensusMode::Proposing || mode == ConsensusMode::Observing) && !consensusFail) { - auto closeTime = rawCloseTimes.self; - - JLOG(j_.info()) << "We closed at " << closeTime.time_since_epoch().count(); - using usec64_t = std::chrono::duration; - auto closeTotal = std::chrono::duration_cast(closeTime.time_since_epoch()); + JLOG(j_.info()) << "We closed at " << rawCloseTimes.self.time_since_epoch().count(); int closeCount = 1; - for (auto const& [t, v] : rawCloseTimes.peers) { JLOG(j_.info()) << std::to_string(v) << " time votes for " << std::to_string(t.time_since_epoch().count()); closeCount += v; - closeTotal += std::chrono::duration_cast(t.time_since_epoch()) * v; } - closeTotal += usec64_t(closeCount / 2); // for round to nearest - closeTotal /= closeCount; - - // Use signed times since we are subtracting - using duration = std::chrono::duration; - using time_point = std::chrono::time_point; - auto offset = time_point{closeTotal} - std::chrono::time_point_cast(closeTime); + // Median handles outliers better than mean. + auto const offset = medianCloseOffset(rawCloseTimes); JLOG(j_.info()) << "Our close offset is estimated at " << offset.count() << " (" << closeCount << ")"; diff --git a/src/xrpld/consensus/Consensus.h b/src/xrpld/consensus/Consensus.h index 440191939b..19def76ec0 100644 --- a/src/xrpld/consensus/Consensus.h +++ b/src/xrpld/consensus/Consensus.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1580,7 +1581,13 @@ Consensus::updateOurPositions(std::unique_ptr const& JLOG(j_.info()) << ss.str(); CLOG(clog) << ss.str(); - for (auto const& [t, v] : closeTimeVotes) + // Walk the votes highest-time first so that, among close times tied + // for the most votes, the earliest wins. The smaller value is the + // safer choice: without close-time consensus this round, the winner + // only updates our position for the next proposal, and a too-early + // time is bounded below by the prior ledger's close time. Only the + // tie-break changes; the bin with the most votes still wins. + for (auto const& [t, v] : std::views::reverse(closeTimeVotes)) { JLOG(j_.debug()) << "CCTime: seq " << static_cast(previousLedger_.seq()) + 1 << ": " diff --git a/src/xrpld/consensus/ConsensusTypes.h b/src/xrpld/consensus/ConsensusTypes.h index 4553e46f48..063d18cb63 100644 --- a/src/xrpld/consensus/ConsensusTypes.h +++ b/src/xrpld/consensus/ConsensusTypes.h @@ -9,7 +9,9 @@ #include #include +#include #include +#include #include namespace xrpl { @@ -190,6 +192,75 @@ struct ConsensusCloseTimes NetClock::time_point self; }; +/** + * Offset of the network's close time relative to ours, using a weighted median. + * + * Treats the sample set as `{self x 1}` merged with `{t x w}` for each + * `(t, w)` in `times.peers`, in time order, and returns `(median - self)` + * in whole seconds. Uses the lower weighted median: the median is the + * earliest time at which the running weight reaches half the total, so an + * even total whose halfway point falls between two bins resolves to the + * earlier bin. + * + * @param times Our own close time and the weighted close times of peers. + * @return Weighted median of all close times minus our own, in whole seconds. + */ +inline std::chrono::seconds +medianCloseOffset(ConsensusCloseTimes const& times) +{ + using namespace std::chrono; + using time_point = NetClock::time_point; + + std::int64_t totalWeight = 1; + for (auto const& [_, w] : times.peers) + totalWeight += w; + + std::int64_t const halfWeight = (totalWeight + 1) / 2; + + std::optional median{}; + std::int64_t tally = 0; + bool selfPlaced = false; + + // Accumulate weight in time order; the first bin to reach halfWeight is + // the (lower) weighted median. Returns true once that bin is found. + auto step = [&](time_point t, std::int64_t w) { + XRPL_ASSERT(tally < halfWeight, "xrpl::medianCloseOffset::step : median not yet found"); + tally += w; + if (tally >= halfWeight) + { + median = t; + return true; + } + return false; + }; + + for (auto const& [t, w] : times.peers) + { + if (!selfPlaced && times.self <= t) + { + selfPlaced = true; + if (step(times.self, 1)) + break; + } + if (step(t, w)) + break; + } + if (!selfPlaced && !median) + step(times.self, 1); + + if (!median) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::medianCloseOffset : median not found"); + median = times.self; + // LCOV_EXCL_STOP + } + + return duration_cast( + duration{median->time_since_epoch().count()} - + duration{times.self.time_since_epoch().count()}); +} + /** * Whether we have or don't have a consensus */ From 4a9ee54c88118ff0ab869ec81b8e1066f50f137b Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:29:31 -0400 Subject: [PATCH 09/68] fix: Handle malformed ledger replay responses --- include/xrpl/resource/Fees.h | 1 + src/libxrpl/resource/Fees.cpp | 1 + src/test/app/LedgerReplay_test.cpp | 130 +++++++++++++++--- .../ledger/detail/LedgerReplayMsgHandler.cpp | 112 ++++++++++----- .../ledger/detail/LedgerReplayMsgHandler.h | 21 ++- src/xrpld/overlay/detail/PeerImp.cpp | 22 ++- 6 files changed, 219 insertions(+), 68 deletions(-) diff --git a/include/xrpl/resource/Fees.h b/include/xrpl/resource/Fees.h index 5001b504d6..a7c923dfef 100644 --- a/include/xrpl/resource/Fees.h +++ b/include/xrpl/resource/Fees.h @@ -13,6 +13,7 @@ extern Charge const kFeeRequestNoReply; // A request that we cannot satisfy. extern Charge const kFeeInvalidSignature; // An object whose signature we had to check that failed. extern Charge const kFeeUselessData; // Data we have no use for. extern Charge const kFeeInvalidData; // Data we have to verify before rejecting. +extern Charge const kFeeMalformedData; // Data that no honest peer would send. // RPC loads extern Charge const kFeeMalformedRpc; // An RPC request that we can immediately tell is invalid. diff --git a/src/libxrpl/resource/Fees.cpp b/src/libxrpl/resource/Fees.cpp index bb825fa3c7..6792dd7e36 100644 --- a/src/libxrpl/resource/Fees.cpp +++ b/src/libxrpl/resource/Fees.cpp @@ -9,6 +9,7 @@ Charge const kFeeRequestNoReply(10, "unsatisfiable request"); Charge const kFeeInvalidSignature(2000, "invalid signature"); Charge const kFeeUselessData(150, "useless data"); Charge const kFeeInvalidData(400, "invalid data"); +Charge const kFeeMalformedData(2000, "malformed data"); Charge const kFeeMalformedRpc(100, "malformed RPC"); Charge const kFeeReferenceRpc(20, "reference RPC"); diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index f08d6548e0..27e8a8030b 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -58,6 +58,8 @@ #include #include #include +#include +#include #include #include #include @@ -958,7 +960,8 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processProofPathRequest(request)); BEAST_EXPECT(reply->has_error()); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::BadData); } { // request, wrong hash @@ -982,7 +985,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processProofPathRequest(request)); BEAST_EXPECT(!reply->has_error()); - BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::Ok); { // bad reply: invalid hash/key sizes @@ -990,37 +993,49 @@ struct LedgerReplayer_test : public beast::unit_test::Suite // reply with undersized ledgerhash (31 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(31, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with oversized ledgerhash (33 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(33, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with empty ledgerhash auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string()); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with undersized key (31 bytes) auto bad = std::make_shared(*reply); bad->set_key(std::string(31, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with oversized key (33 bytes) auto bad = std::make_shared(*reply); bad->set_key(std::string(33, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with empty key auto bad = std::make_shared(*reply); bad->set_key(std::string()); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } } @@ -1030,13 +1045,18 @@ struct LedgerReplayer_test : public beast::unit_test::Suite std::string r(reply->ledgerheader()); r.back()--; reply->set_ledgerheader(r); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == + ReplayMsgStatus::Malformed); r.back()++; reply->set_ledgerheader(r); - BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::Ok); // bad proof path reply->mutable_path()->RemoveLast(); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == + ReplayMsgStatus::Malformed); } } } @@ -1054,14 +1074,16 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processReplayDeltaRequest(request)); BEAST_EXPECT(reply->has_error()); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::BadData); // request, wrong hash uint256 hash(1234567); request->set_ledgerhash(hash.data(), hash.size()); reply = std::make_shared( server.msgHandler.processReplayDeltaRequest(request)); BEAST_EXPECT(reply->has_error()); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::BadData); } { @@ -1071,7 +1093,8 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processReplayDeltaRequest(request)); BEAST_EXPECT(!reply->has_error()); - BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::Ok); { // bad reply: invalid hash sizes @@ -1079,19 +1102,25 @@ struct LedgerReplayer_test : public beast::unit_test::Suite // reply with undersized ledgerhash (31 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(31, '\x01')); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with oversized ledgerhash (33 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(33, '\x01')); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with empty ledgerhash auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string()); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(bad) == + ReplayMsgStatus::Malformed); } } @@ -1101,17 +1130,77 @@ struct LedgerReplayer_test : public beast::unit_test::Suite std::string r(reply->ledgerheader()); r.back()--; reply->set_ledgerheader(r); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == + ReplayMsgStatus::Malformed); r.back()++; reply->set_ledgerheader(r); - BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::Ok); // bad txns reply->mutable_transaction()->RemoveLast(); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == + ReplayMsgStatus::Malformed); } } } + void + testTruncatedHeader() + { + testcase("TruncatedLedgerHeader"); + LedgerServer server(*this, {.initLedgers = 1}); + auto const l = server.ledgerMaster.getClosedLedger(); + + auto runNoThrow = [this](auto fn, char const* what) { + try + { + BEAST_EXPECT(fn() == ReplayMsgStatus::Malformed); + } + catch (std::exception const& e) + { + fail( + std::format("processor threw on truncated header ({}): {}", what, e.what()), + __FILE__, + __LINE__); + } + catch (...) + { + fail( + std::format("processor threw unknown exception ({}) on truncated header", what), + __FILE__, + __LINE__); + } + }; + + { + auto request = std::make_shared(); + request->set_ledgerhash(l->header().hash.data(), l->header().hash.size()); + auto reply = std::make_shared( + server.msgHandler.processReplayDeltaRequest(request)); + BEAST_EXPECT(!reply->has_error()); + + reply->set_ledgerheader(std::string(1, '\x00')); + runNoThrow( + [&] { return server.msgHandler.processReplayDeltaResponse(reply); }, "ReplayDelta"); + } + + { + auto request = std::make_shared(); + request->set_ledgerhash(l->header().hash.data(), l->header().hash.size()); + request->set_type(protocol::TMLedgerMapType::lmACCOUNT_STATE); + request->set_key(keylet::skip().key.data(), keylet::skip().key.size()); + auto reply = std::make_shared( + server.msgHandler.processProofPathRequest(request)); + BEAST_EXPECT(!reply->has_error()); + + reply->set_ledgerheader(std::string(1, '\x00')); + runNoThrow( + [&] { return server.msgHandler.processProofPathResponse(reply); }, "ProofPath"); + } + } + void testTaskParameter() { @@ -1514,6 +1603,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite { testProofPath(); testReplayDelta(); + testTruncatedHeader(); testTaskParameter(); testConfig(); testHandshake(); diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp index 07738d99f4..6ed4a296ac 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp @@ -101,42 +101,54 @@ LedgerReplayMsgHandler::processProofPathRequest( return reply; } -bool +ReplayMsgStatus LedgerReplayMsgHandler::processProofPathResponse( std::shared_ptr const& msg) { protocol::TMProofPathResponse const& reply = *msg; - if (reply.has_error() || !reply.has_key() || !reply.has_ledgerhash() || !reply.has_type() || + if (reply.has_error()) + { + JLOG(journal_.debug()) << "ProofPathResponse: peer reported error"; + return ReplayMsgStatus::BadData; + } + if (!reply.has_key() || !reply.has_ledgerhash() || !reply.has_type() || !reply.has_ledgerheader() || reply.path_size() == 0 || reply.ledgerhash().size() != uint256::size() || reply.key().size() != uint256::size()) { - JLOG(journal_.debug()) << "Bad message: Error reply"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (missing or wrong-size fields)"; + return ReplayMsgStatus::Malformed; } if (reply.type() != protocol::lmACCOUNT_STATE) { - JLOG(journal_.debug()) << "Bad message: we only support the state ShaMap for now"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (unsupported map type)"; + return ReplayMsgStatus::Malformed; } // deserialize the header - auto info = deserializeHeader({reply.ledgerheader().data(), reply.ledgerheader().size()}); + LedgerHeader info; + try + { + info = deserializeHeader(makeSlice(reply.ledgerheader())); + } + catch (std::exception const& e) + { + JLOG(journal_.debug()) << "ProofPathResponse: malformed header (" << e.what() << ")"; + return ReplayMsgStatus::Malformed; + } uint256 const replyHash = uint256::fromRaw(reply.ledgerhash()); if (calculateLedgerHash(info) != replyHash) { - JLOG(journal_.debug()) << "Bad message: Hash mismatch"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (hash mismatch)"; + return ReplayMsgStatus::Malformed; } info.hash = replyHash; uint256 const key = uint256::fromRaw(reply.key()); if (key != keylet::skip().key) { - JLOG(journal_.debug()) << "Bad message: we only support the short skip list for now. " - "Key in reply " - << key; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (unexpected key " << key << ")"; + return ReplayMsgStatus::Malformed; } // verify the skip list @@ -149,26 +161,35 @@ LedgerReplayMsgHandler::processProofPathResponse( if (!SHAMap::verifyProofPath(info.accountHash, key, path)) { - JLOG(journal_.debug()) << "Bad message: Proof path verify failed"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (proof path verify failed)"; + return ReplayMsgStatus::Malformed; } // deserialize the SHAMapItem - auto node = SHAMapTreeNode::makeFromWire(makeSlice(path.front())); + SHAMapTreeNodePtr node; + try + { + node = SHAMapTreeNode::makeFromWire(makeSlice(path.front())); + } + catch (std::exception const& e) + { + JLOG(journal_.debug()) << "ProofPathResponse: malformed SHAMap node (" << e.what() << ")"; + return ReplayMsgStatus::Malformed; + } if (!node || !node->isLeaf()) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (not a leaf node)"; + return ReplayMsgStatus::Malformed; } if (auto item = safeDowncast(node.get())->peekItem()) { replayer_.gotSkipList(info, item); - return true; + return ReplayMsgStatus::Ok; } - JLOG(journal_.debug()) << "Bad message: Cannot get ShaMapItem"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (no SHAMapItem)"; + return ReplayMsgStatus::Malformed; } protocol::TMReplayDeltaResponse @@ -210,24 +231,38 @@ LedgerReplayMsgHandler::processReplayDeltaRequest( return reply; } -bool +ReplayMsgStatus LedgerReplayMsgHandler::processReplayDeltaResponse( std::shared_ptr const& msg) { protocol::TMReplayDeltaResponse const& reply = *msg; - if (reply.has_error() || !reply.has_ledgerheader() || !reply.has_ledgerhash() || + if (reply.has_error()) + { + JLOG(journal_.debug()) << "ReplayDeltaResponse: peer reported error"; + return ReplayMsgStatus::BadData; + } + if (!reply.has_ledgerheader() || !reply.has_ledgerhash() || reply.ledgerhash().size() != uint256::size()) { - JLOG(journal_.debug()) << "Bad message: Error reply"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (missing or wrong-size fields)"; + return ReplayMsgStatus::Malformed; } - auto info = deserializeHeader({reply.ledgerheader().data(), reply.ledgerheader().size()}); + LedgerHeader info; + try + { + info = deserializeHeader(makeSlice(reply.ledgerheader())); + } + catch (std::exception const& e) + { + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed header (" << e.what() << ")"; + return ReplayMsgStatus::Malformed; + } uint256 const replyHash = uint256::fromRaw(reply.ledgerhash()); if (calculateLedgerHash(info) != replyHash) { - JLOG(journal_.debug()) << "Bad message: Hash mismatch"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (hash mismatch)"; + return ReplayMsgStatus::Malformed; } info.hash = replyHash; @@ -252,8 +287,8 @@ LedgerReplayMsgHandler::processReplayDeltaResponse( auto tx = std::make_shared(txSit); if (!tx) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (tx deserialize)"; + return ReplayMsgStatus::Malformed; } auto tid = tx->getTransactionID(); STObject meta(metaSit, sfMetadata); @@ -262,25 +297,26 @@ LedgerReplayMsgHandler::processReplayDeltaResponse( if (!txMap.addGiveItem( SHAMapNodeType::TnTransactionMd, makeShamapitem(tid, shaMapItemData.slice()))) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (tx map add)"; + return ReplayMsgStatus::Malformed; } } } - catch (std::exception const&) + catch (std::exception const& e) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed transactions (" << e.what() + << ")"; + return ReplayMsgStatus::Malformed; } if (txMap.getHash().asUInt256() != info.txHash) { - JLOG(journal_.debug()) << "Bad message: Transactions verify failed"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (transactions verify failed)"; + return ReplayMsgStatus::Malformed; } replayer_.gotReplayDelta(info, std::move(orderedTxns)); - return true; + return ReplayMsgStatus::Ok; } } // namespace xrpl diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h index 5a8951fb25..ba989e2586 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h @@ -10,6 +10,15 @@ namespace xrpl { class Application; class LedgerReplayer; +/** + * Outcome of processing an incoming ledger-replay response. + */ +enum class ReplayMsgStatus { + Ok, ///< Accepted. + BadData, ///< Peer reported has_error() (legitimate "cannot fulfill" signal). + Malformed, ///< Protocol-level violation; no honest peer would produce this. +}; + class LedgerReplayMsgHandler final { public: @@ -19,31 +28,31 @@ public: /** * Process TMProofPathRequest and return TMProofPathResponse * @note check has_error() and error() of the response for error + * @return TMProofPathResponse with the proof path, or with error() set if + * the request cannot be fulfilled */ protocol::TMProofPathResponse processProofPathRequest(std::shared_ptr const& msg); /** * Process TMProofPathResponse - * @return false if the response message has bad format or bad data; - * true otherwise */ - bool + ReplayMsgStatus processProofPathResponse(std::shared_ptr const& msg); /** * Process TMReplayDeltaRequest and return TMReplayDeltaResponse * @note check has_error() and error() of the response for error + * @return TMReplayDeltaResponse with the ledger delta, or with error() set + * if the request cannot be fulfilled */ protocol::TMReplayDeltaResponse processReplayDeltaRequest(std::shared_ptr const& msg); /** * Process TMReplayDeltaResponse - * @return false if the response message has bad format or bad data; - * true otherwise */ - bool + ReplayMsgStatus processReplayDeltaResponse(std::shared_ptr const& msg); private: diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 8838970b5f..35eecc6439 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1565,9 +1565,16 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - if (!ledgerReplayMsgHandler_.processProofPathResponse(m)) + switch (ledgerReplayMsgHandler_.processProofPathResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "proof_path_response"); + case ReplayMsgStatus::Ok: + break; + case ReplayMsgStatus::BadData: + fee_.update(Resource::kFeeInvalidData, "proof_path_response"); + break; + case ReplayMsgStatus::Malformed: + fee_.update(Resource::kFeeMalformedData, "proof_path_response malformed"); + break; } } @@ -1615,9 +1622,16 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - if (!ledgerReplayMsgHandler_.processReplayDeltaResponse(m)) + switch (ledgerReplayMsgHandler_.processReplayDeltaResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "replay_delta_response"); + case ReplayMsgStatus::Ok: + break; + case ReplayMsgStatus::BadData: + fee_.update(Resource::kFeeInvalidData, "replay_delta_response"); + break; + case ReplayMsgStatus::Malformed: + fee_.update(Resource::kFeeMalformedData, "replay_delta_response malformed"); + break; } } From 5ab95748d49a8085013a85dfc4a796366c8a68eb Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:25:00 -0400 Subject: [PATCH 10/68] refactor: Clean up pong replies --- include/xrpl/proto/xrpl.proto | 7 ++++--- src/xrpld/overlay/detail/PeerImp.cpp | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/include/xrpl/proto/xrpl.proto b/include/xrpl/proto/xrpl.proto index d49920201e..302120d1be 100644 --- a/include/xrpl/proto/xrpl.proto +++ b/include/xrpl/proto/xrpl.proto @@ -293,14 +293,15 @@ message TMLedgerData { } message TMPing { + // Previously used - don't reuse. + reserved 3, 4; + enum pingType { ptPING = 0; // we want a reply ptPONG = 1; // this is a reply } required pingType type = 1; - optional uint32 seq = 2; // detect stale replies, ensure other side is reading - optional uint64 pingTime = 3; // know when we think we sent the ping - optional uint64 netTime = 4; + optional uint32 seq = 2; // detect stale replies, ensure other side is reading } message TMSquelch { diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 35eecc6439..5662b6d33d 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1113,10 +1113,13 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (m->type() == protocol::TMPing::ptPING) { - // We have received a ping request, reply with a pong + // We have received a ping request, reply with a pong. fee_.update(Resource::kFeeModerateBurdenPeer, "ping request"); - m->set_type(protocol::TMPing::ptPONG); - send(std::make_shared(*m, protocol::mtPING)); + protocol::TMPing pong; + pong.set_type(protocol::TMPing::ptPONG); + if (m->has_seq()) + pong.set_seq(m->seq()); + send(std::make_shared(pong, protocol::mtPING)); return; } From 1dcaf4b54eaf461cb91cc9bc3cc9fc4ae40af11f Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:24:06 -0400 Subject: [PATCH 11/68] fix: Bound and offload per-connection subscription cleanup --- cfg/xrpld-example.cfg | 11 + include/xrpl/config/Constants.h | 1 + include/xrpl/server/InfoSub.h | 142 +++- src/libxrpl/server/InfoSub.cpp | 116 ++- src/test/rpc/Subscribe_test.cpp | 416 +++++++++++ src/tests/libxrpl/CMakeLists.txt | 1 + src/tests/libxrpl/server/InfoSub.cpp | 60 ++ src/xrpld/app/ledger/AcceptedLedger.h | 9 + src/xrpld/app/misc/NetworkOPs.cpp | 667 +++++++++++++----- src/xrpld/core/Config.h | 6 + src/xrpld/core/detail/Config.cpp | 3 + .../rpc/handlers/subscribe/Subscribe.cpp | 83 ++- 12 files changed, 1301 insertions(+), 214 deletions(-) create mode 100644 src/tests/libxrpl/server/InfoSub.cpp diff --git a/cfg/xrpld-example.cfg b/cfg/xrpld-example.cfg index 9e334e6f4f..6a44561c68 100644 --- a/cfg/xrpld-example.cfg +++ b/cfg/xrpld-example.cfg @@ -488,6 +488,17 @@ # Must be a number between 100 and 1000, defaults to 250 # # +# [max_subscriptions_per_connection] +# +# Maximum number of account, real-time account, and account-history +# subscriptions a single client connection may hold at once. Bounds the +# per-connection state torn down when the connection disconnects. Book +# subscriptions are tracked separately and are not counted here. +# +# Defaults to 100000 if not set; large enough for legitimate power users +# such as block explorers. +# +# # [overlay] # # Controls settings related to the peer to peer overlay. diff --git a/include/xrpl/config/Constants.h b/include/xrpl/config/Constants.h index 5514e0e77b..0fe4efee63 100644 --- a/include/xrpl/config/Constants.h +++ b/include/xrpl/config/Constants.h @@ -25,6 +25,7 @@ struct Sections static constexpr auto kLedgerHistory = "ledger_history"; static constexpr auto kLedgerReplay = "ledger_replay"; static constexpr auto kLedgerTxTables = "ledger_tx_tables"; + static constexpr auto kMaxSubscriptionsPerConnection = "max_subscriptions_per_connection"; static constexpr auto kMaxTransactions = "max_transactions"; static constexpr auto kNetworkId = "network_id"; static constexpr auto kNetworkQuorum = "network_quorum"; diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index 2e9bd857c7..1c12d9e520 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -22,6 +23,39 @@ namespace xrpl { // Operations that clients may wish to perform against the network // Master operational handler, server sequencer, network tracker +/** + * Maximum number of subscriptions a single client connection may hold at once. + * + * Applies to the account, real-time account, and account-history subscriptions + * tracked on one InfoSub (the sets counted by totalSubscriptionCount), bounding + * the disconnect-time cleanup of those sets. Book subscriptions are tracked + * separately (OrderBookDB) and are not counted here. Generous enough for + * legitimate power users such as block explorers. + */ +constexpr std::size_t kMaxSubscriptionsPerConnection = 100'000; + +/** + * Whether adding @p additional subscriptions to a connection already holding + * @p current would exceed the cap. + * + * Pure arithmetic split out so it can be unit-tested without a live + * connection. The first term avoids underflow in the subtraction. + * + * @param current Subscriptions already tracked on the connection. + * @param additional Subscriptions a request would add. + * @param cap The effective per-connection cap. Defaults to the + * built-in limit; callers may pass a configured override. + * @return true if the request must be rejected to stay within the cap. + */ +[[nodiscard]] constexpr bool +exceedsSubscriptionCap( + std::size_t current, + std::size_t additional, + std::size_t cap = kMaxSubscriptionsPerConnection) +{ + return additional > cap || current > cap - additional; +} + class InfoSubRequest : public CountedObject { public: @@ -44,12 +78,12 @@ public: * map. * * @note Lifetime contract: every `InfoSub` instance MUST be destroyed - * before the backing `Source`. NetworkOPsImp shutdown drops all - * subscriber strong refs before its own teardown to satisfy this. + * before the backing `Source`. NetworkOPsImp shutdown drops all + * subscriber strong refs before its own teardown to satisfy this. * @note Thread-safety: per-instance state is guarded by `lock_`. The - * destructor reads tracking sets without taking `lock_` because - * the strong-pointer ref-count is zero at destruction time, so - * no other thread can be calling the public mutators. + * destructor reads tracking sets without taking `lock_` because + * the strong-pointer ref-count is zero at destruction time, so + * no other thread can be calling the public mutators. */ class InfoSub : public CountedObject { @@ -117,6 +151,34 @@ public: AccountID const& account, bool historyOnly) = 0; + /** + * Schedule the server-side teardown of a disconnecting connection's + * account subscriptions off the destructor thread. + * + * The implementation posts a low-priority JobQueue task that erases the + * entries in bounded chunks, so `~InfoSub` returns immediately instead + * of running the erase loop inline. The sets are taken by value so the + * job owns its copies and never references the destroyed `InfoSub`. + * Cleanup is keyed on `seq` (unique per connection), so deferring it + * cannot disturb a reconnected client reusing the same accounts. + * + * @param seq The disconnecting connection's unique subscription id. + * @param rtAccounts Real-time account subscriptions to remove. + * @param normalAccounts Normal account subscriptions to remove. + * @param historyAccounts Account-history subscriptions to remove. + * + * @note The implementing `Source` must outlive any job it posts. If the + * JobQueue is already stopping (process shutdown), the job is not + * enqueued; the cleanup is skipped because the server-side maps + * are about to be destroyed and no publishing can run. + */ + virtual void + scheduleAccountCleanup( + std::uint64_t seq, + hash_set rtAccounts, + hash_set normalAccounts, + hash_set historyAccounts) = 0; + // VFALCO TODO Document the bool return value virtual bool subLedger(ref ispListener, json::Value& jvResult) = 0; @@ -153,12 +215,12 @@ public: * @param ispListener The subscriber requesting removal. * @param book The order book to unsubscribe from. * @return true if the entry was present and removed, false if the - * subscriber was not subscribed to @p book. + * subscriber was not subscribed to @p book. * - * @note Thread-safety: acquires subLock_ internally. + * @note Thread-safety: acquires bookLock_ internally. * @note Do NOT call from ~InfoSub(). Use unsubBookInternal instead - * to avoid a redundant write-back to bookSubscriptions_ on a - * partially-destroyed object. + * to avoid a redundant write-back to bookSubscriptions_ on a + * partially-destroyed object. */ virtual bool unsubBook(ref ispListener, Book const&) = 0; @@ -173,9 +235,9 @@ public: * @param uListener The sequence number of the subscriber being torn down. * @param book The order book entry to remove. * @return true if the entry was present and removed, false otherwise - * (e.g., already removed by a concurrent RPC unsubscribe). + * (e.g., already removed by a concurrent RPC unsubscribe). * - * @note Thread-safety: acquires subLock_ internally. + * @note Thread-safety: acquires bookLock_ internally. */ virtual bool unsubBookInternal(std::uint64_t uListener, Book const&) = 0; @@ -221,8 +283,8 @@ public: /** * Journal used by InfoSub for diagnostics that occur after the - * owning subsystem (e.g. application-level Logs) is the only - * surviving sink — primarily destructor-time cleanup failures. + * owning subsystem (e.g. application-level Logs) is the only + * surviving sink — primarily destructor-time cleanup failures. */ [[nodiscard]] virtual beast::Journal const& journal() const = 0; @@ -243,6 +305,56 @@ public: [[nodiscard]] std::uint64_t getSeq() const; + /** + * Return the number of subscriptions currently tracked on this + * connection. + * + * The combined size of the per-connection account, real-time account, and + * account-history subscription sets. `doSubscribe` reads this to enforce + * the per-connection subscription cap before admitting more. + * + * @return The total tracked subscription count for this connection. + * + * @note Thread-safe: takes `lock_` for the read; read-only. + */ + [[nodiscard]] std::size_t + totalSubscriptionCount() const; + + /** + * Enforce the cap and reserve a request's net-new accounts, atomically. + * + * Under one hold of `lock_`: count the net-new entries in the two sets, + * check the total against @p cap, and insert them only if it fits. + * All-or-nothing. Doing check and insert together stops two concurrent + * requests sharing an InfoSub (the admin subscribe-by-url path) from both + * passing the check before either records its accounts. The server-side + * maps are populated afterwards by subAccount, whose re-insert is a no-op. + * + * @param proposedAccounts Real-time (accounts_proposed) ids to reserve. + * @param normalAccounts Normal (accounts) ids to reserve. + * @param cap The effective per-connection cap. + * @return true if reserved; false if the request must be rejected. + * @note Thread-safe: takes `lock_`. + */ + [[nodiscard]] bool + tryReserveAccountSubscriptions( + hash_set const& proposedAccounts, + hash_set const& normalAccounts, + std::size_t cap); + + /** + * Whether this connection already tracks an account-history for @p account. + * + * `doSubscribe` reads this to charge the cap for an account_history_tx_stream + * only when it is net-new, matching the account branches. + * + * @param account The account an account_history_tx_stream would add. + * @return true if @p account is already in the account-history set. + * @note Thread-safe: takes `lock_`; read-only. + */ + [[nodiscard]] bool + hasAccountHistorySubscription(AccountID const& account) const; + void onSendEmpty(); @@ -302,7 +414,9 @@ public: getApiVersion() const noexcept; protected: - std::mutex lock_; + // Mutable so the read-only totalSubscriptionCount() accessor can lock it + // from a const method; locking semantics are otherwise unchanged. + mutable std::mutex lock_; private: Consumer consumer_; diff --git a/src/libxrpl/server/InfoSub.cpp b/src/libxrpl/server/InfoSub.cpp index 353c295856..ceb1027d95 100644 --- a/src/libxrpl/server/InfoSub.cpp +++ b/src/libxrpl/server/InfoSub.cpp @@ -7,10 +7,12 @@ #include #include +#include #include #include #include #include +#include namespace xrpl { @@ -64,6 +66,9 @@ InfoSub::InfoSub(Source& source, Consumer consumer) InfoSub::~InfoSub() { + // Stream unsubscribes are O(1): each erases this connection's single seq_ + // from one stream map, so they are cheap enough to run inline on the + // disconnect thread. // Each Source teardown call below acquires a server-side lock and // can throw. Wrap each independent call so partial failure does not // skip the remaining teardown steps. @@ -79,29 +84,48 @@ InfoSub::~InfoSub() safeUnsub(seq_, [&] { source_.unsubPeerStatus(seq_); }, j); safeUnsub(seq_, [&] { source_.unsubConsensus(seq_); }, j); - // Use the internal unsubscribe so that it won't call - // back to us and modify its own parameter - if (!realTimeSubscriptions_.empty()) - { - safeUnsub( - seq_, [&] { source_.unsubAccountInternal(seq_, realTimeSubscriptions_, true); }, j); - } - - if (!normalSubscriptions_.empty()) - { - safeUnsub( - seq_, [&] { source_.unsubAccountInternal(seq_, normalSubscriptions_, false); }, j); - } - - for (auto const& account : accountHistorySubscriptions_) - { - safeUnsub(seq_, [&] { source_.unsubAccountHistoryInternal(seq_, account, false); }, j); - } - + // Book subscriptions are torn down inline here, keyed on seq_, rather than + // through the chunked account cleanup below. The book set is not capped, so + // it can be large; but each unsubBookInternal takes bookLock_ for a single + // O(1) erase and releases it, so even a large set never holds a lock across + // the whole loop - a competing book publish can interleave between erases. + // The disconnect thread still does O(N) brief acquisitions. Use the internal + // variant so it does not write back to bookSubscriptions_ on this + // partially-destroyed object. for (auto const& book : bookSubscriptions_) { safeUnsub(seq_, [&] { source_.unsubBookInternal(seq_, book); }, j); } + + // Hand the account sets off (by move) to the Source for a chunked, + // off-thread teardown keyed on seq_, instead of erasing them inline here. + // This keeps the destructor from holding the account lock across a large + // erase loop. The job never references this object, which is being + // destroyed. + // + // Moving the sets without holding lock_ is safe: the destructor runs only + // when the last shared_ptr to this InfoSub is released, so by the + // shared_ptr contract no other thread holds a reference. Subscription maps + // store weak_ptrs, so a concurrent publisher must weak_ptr::lock() first; + // that succeeds only while a strong reference exists, which cannot overlap + // with destruction. No other thread can observe the moved-from sets. + // + // Wrapped like the steps above: scheduleAccountCleanup enqueues a JobQueue + // task, which allocates and locks and so can throw. A throw out of this + // noexcept destructor would terminate the process. Skipping the cleanup on + // throw is harmless: the account/rt maps hold weak_ptrs that the next + // publish prunes once this InfoSub is gone, and any history paging job + // self-terminates when its weak sink can no longer be locked. + safeUnsub( + seq_, + [&] { + source_.scheduleAccountCleanup( + seq_, + std::move(realTimeSubscriptions_), + std::move(normalSubscriptions_), + std::move(accountHistorySubscriptions_)); + }, + j); } Resource::Consumer& @@ -121,6 +145,53 @@ InfoSub::onSendEmpty() { } +std::size_t +InfoSub::totalSubscriptionCount() const +{ + // Hold lock_ for the whole read so the three sets cannot be mutated + // mid-count by a concurrent (un)subscribe on this connection. + std::scoped_lock const sl(lock_); + + // Combined tally the per-connection cap is enforced against. + return normalSubscriptions_.size() + realTimeSubscriptions_.size() + + accountHistorySubscriptions_.size(); +} + +bool +InfoSub::tryReserveAccountSubscriptions( + hash_set const& proposedAccounts, + hash_set const& normalAccounts, + std::size_t cap) +{ + // One lock hold covers the count, the check and the insert. + std::scoped_lock const sl(lock_); + + // Entries not already tracked; re-subscribing held accounts is not charged. + auto const countNew = [](hash_set const& requested, + hash_set const& existing) { + std::size_t fresh = 0; + for (auto const& account : requested) + { + if (!existing.contains(account)) + ++fresh; + } + return fresh; + }; + + std::size_t const additional = countNew(proposedAccounts, realTimeSubscriptions_) + + countNew(normalAccounts, normalSubscriptions_); + + std::size_t const current = normalSubscriptions_.size() + realTimeSubscriptions_.size() + + accountHistorySubscriptions_.size(); + + if (exceedsSubscriptionCap(current, additional, cap)) + return false; + + realTimeSubscriptions_.insert(proposedAccounts.begin(), proposedAccounts.end()); + normalSubscriptions_.insert(normalAccounts.begin(), normalAccounts.end()); + return true; +} + void InfoSub::insertSubAccountInfo(AccountID const& account, bool rt) { @@ -165,6 +236,13 @@ InfoSub::deleteSubAccountHistory(AccountID const& account) accountHistorySubscriptions_.erase(account); } +bool +InfoSub::hasAccountHistorySubscription(AccountID const& account) const +{ + std::scoped_lock const sl(lock_); + return accountHistorySubscriptions_.contains(account); +} + void InfoSub::insertBookSubscription(Book const& book) { diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 97c5290947..567f31437a 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1548,6 +1549,413 @@ public: } } + // ----- Subscription limit / teardown verification ---------------------- + // + // The helpers and tests below exercise: + // * the per-connection subscription cap + proportional charge enforced + // in doSubscribe (Subscribe.cpp), and + // * the asynchronous, chunked teardown of a disconnecting connection's + // account subscriptions (~InfoSub -> scheduleAccountCleanup -> JobQueue). + // + // The cap-exceeded error is rpcINVALID_PARAMS with the message "Too many + // subscriptions for this connection."; the tests assert that exactly. + // + // There is no public accessor for the server-side per-connection count, so + // the async cleanup is verified behaviorally: publishing still flows to a + // live subscriber, rather than by reading a count to zero. + + // Build `count` distinct, valid, base58-encoded account strings cheaply by + // incrementing an AccountID. parseAccountIds dedups into a hash_set, so the + // strings MUST be distinct for the cap arithmetic to be exact; incrementing + // guarantees distinctness without deriving `count` keypairs. + static std::vector + makeAccountStrings(std::size_t count, std::uint32_t seed = 1) + { + std::vector out; + out.reserve(count); + // Start at `seed` so separate calls produce non-overlapping ranges, + // letting a test subscribe disjoint batches across requests. + AccountID id{static_cast(seed)}; + for (std::size_t i = 0; i < count; ++i) + { + out.push_back(toBase58(id)); + ++id; + } + return out; + } + + // Append the given account strings as a jss::accounts array onto a fresh + // subscribe request object. + static json::Value + accountsRequest(std::vector const& accts) + { + json::Value jv{json::ValueType::Object}; + jv[jss::accounts] = json::ValueType::Array; + for (auto const& a : accts) + jv[jss::accounts].append(a); + return jv; + } + + // Append the given account strings as a jss::accounts_proposed array onto a + // fresh subscribe request object. + static json::Value + accountsProposedRequest(std::vector const& accts) + { + json::Value jv{json::ValueType::Object}; + jv[jss::accounts_proposed] = json::ValueType::Array; + for (auto const& a : accts) + jv[jss::accounts_proposed].append(a); + return jv; + } + + // A single, valid XRP/USD order book request, as one entry of a + // jss::books array. + static json::Value + oneBookRequest() + { + using namespace jtx; + json::Value jv{json::ValueType::Object}; + jv[jss::books] = json::ValueType::Array; + json::Value& book = jv[jss::books][0u]; + book[jss::taker_gets] = json::ValueType::Object; + book[jss::taker_gets][jss::currency] = "XRP"; + book[jss::taker_pays] = json::ValueType::Object; + book[jss::taker_pays][jss::currency] = "USD"; + book[jss::taker_pays][jss::issuer] = Account("alice").human(); + return jv; + } + + // A single account_history_tx_stream subscribe request for `acct`. + static json::Value + accountHistoryRequest(std::string const& acct) + { + json::Value jv{json::ValueType::Object}; + jv[jss::account_history_tx_stream] = json::ValueType::Object; + jv[jss::account_history_tx_stream][jss::account] = acct; + return jv; + } + + // An envconfig modifier that lowers the per-connection subscription cap to + // `cap`, so the cap logic in doSubscribe can be driven without subscribing + // the production default (100'000) entries. (Env is non-movable, so this + // returns the config modifier rather than a ready-made Env.) + static auto + cappedConfig(std::size_t cap) + { + return [cap](std::unique_ptr cfg) { + cfg->maxSubscriptionsPerConnection = cap; + return jtx::singleThreadIo(std::move(cfg)); + }; + } + + void + testSubscriptionCapRejects() + { + // A request that alone exceeds the cap is rejected with the exact + // cap error, before any state is recorded. Baseline negative path. + testcase("subscription cap rejects an over-cap request"); + + using namespace jtx; + Env env{*this, envconfig(cappedConfig(5))}; + auto wsc = makeWSClient(env.app().config()); + + // Six accounts against a cap of five: rejected. + auto const jr = + wsc->invoke("subscribe", accountsRequest(makeAccountStrings(6)))[jss::result]; + BEAST_EXPECT(jr[jss::error] == "invalidParams"); + BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection."); + } + + void + testReSubscribeNotOvercounted() + { + // Re-subscribing accounts already held by this connection adds no new + // tracked state, so it must be admitted even at the cap. The cap check + // must count only NET-NEW accounts, not the raw request size. + testcase("re-subscribe at the cap is not over-counted"); + + using namespace jtx; + Env env{*this, envconfig(cappedConfig(5))}; + auto wsc = makeWSClient(env.app().config()); + + // Fill the cap exactly with five distinct accounts. + auto const five = makeAccountStrings(5); + { + auto const r = wsc->invoke("subscribe", accountsRequest(five)); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + + // Re-subscribe the same five: net-new is zero, so it stays within the + // cap and must succeed. (Pre-fix this was wrongly rejected.) + { + auto const r = wsc->invoke("subscribe", accountsRequest(five)); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + } + + void + testBooksCapIndependentOfAccounts() + { + // Book subscriptions are tracked separately (OrderBookDB) and are not + // part of totalSubscriptionCount(). An account set at the cap must not + // block an unrelated book subscription. + testcase("books cap is independent of account count"); + + using namespace jtx; + Env env{*this, envconfig(cappedConfig(5))}; + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + BEAST_EXPECT(env.syncClose()); + + auto wsc = makeWSClient(env.app().config()); + + // Fill the account cap exactly. + { + auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(5))); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + + // A single book subscription must still be admitted: it does not count + // against the account cap. (Pre-fix this was wrongly rejected.) + { + auto const r = wsc->invoke("subscribe", oneBookRequest()); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + } + + void + testMultiFieldNoPartialSubscribe() + { + // A single request mixing fields must be all-or-nothing: if a later + // field trips the cap, an earlier field must NOT have subscribed. The + // leak is detected through the cap arithmetic itself - a follow-up + // request succeeds only if no state leaked from the rejected one. + testcase("multi-field subscribe does not partially subscribe"); + + using namespace jtx; + Env env{*this, envconfig(cappedConfig(5))}; + auto wsc = makeWSClient(env.app().config()); + + // accounts_proposed (3, evaluated first, would subscribe) + + // accounts (3): combined 6 exceeds the cap of 5, so the request is + // rejected. The proposed branch must not have leaked its 3 entries. + json::Value req = accountsProposedRequest(makeAccountStrings(3, 1)); + for (auto const& a : makeAccountStrings(3, 100)) + req[jss::accounts].append(a); + { + auto const jr = wsc->invoke("subscribe", req)[jss::result]; + BEAST_EXPECT(jr[jss::error] == "invalidParams"); + BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection."); + } + + // If the rejected request leaked its 3 proposed subscriptions, the + // connection's count is already 3 and this 3-account request would be + // rejected (3 + 3 > 5). With no leak the count is 0 and it succeeds. + { + auto const r = wsc->invoke("subscribe", accountsRequest(makeAccountStrings(3, 200))); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + } + + void + testHistoryReSubscribeNotOvercounted() + { + // An account_history_tx_stream subscribe is charged against the cap only + // when it is net-new, matching the account branches. Re-subscribing an + // account-history already held on this connection adds no tracked entry, + // so it must NOT be rejected at the cap. The two rejection causes are + // told apart by their exact error_message: the cap check yields "Too + // many subscriptions for this connection."; a duplicate that gets past + // the cap and is rejected downstream by subAccountHistory yields the + // generic "Invalid parameters.". + testcase("account_history re-subscribe at the cap is not over-counted"); + + using namespace jtx; + Env env{*this, envconfig(cappedConfig(1))}; + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + BEAST_EXPECT(env.syncClose()); + + auto wsc = makeWSClient(env.app().config()); + + // First account-history subscribe is net-new: charge 1 fills the cap of + // 1 exactly, so it is admitted. Positive path. + { + auto const r = wsc->invoke("subscribe", accountHistoryRequest(alice.human())); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + + // Re-subscribe the same account-history while sitting exactly at the + // cap. Net-new is zero, so the cap check must pass; the request is then + // rejected by subAccountHistory as a duplicate, NOT by the cap. Proven + // by the exact message: it is the duplicate error, not the cap error. + // (Pre-fix, the flat charge of 1 made the cap check reject this with the + // cap message instead.) + { + auto const jr = + wsc->invoke("subscribe", accountHistoryRequest(alice.human()))[jss::result]; + BEAST_EXPECT(jr[jss::error] == "invalidParams"); + BEAST_EXPECT(jr[jss::error_message] == "Invalid parameters."); + BEAST_EXPECT(jr[jss::error_message] != "Too many subscriptions for this connection."); + } + } + + void + testHistoryCapRejectsNetNew() + { + // A genuinely net-new account-history subscribe on a connection already + // at the cap IS rejected, with the cap error. Negative path, and the + // counterpart to testHistoryReSubscribeNotOvercounted: it confirms the + // net-new charge still rejects when the entry really is new. + testcase("account_history net-new subscribe is rejected at the cap"); + + using namespace jtx; + Env env{*this, envconfig(cappedConfig(1))}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + BEAST_EXPECT(env.syncClose()); + + auto wsc = makeWSClient(env.app().config()); + + // Fill the cap of 1 with alice's account-history. + { + auto const r = wsc->invoke("subscribe", accountHistoryRequest(alice.human())); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + + // A different account-history (bob) is net-new: charge 1 over a cap of 1 + // already full, so it is rejected with the cap error. + { + auto const jr = + wsc->invoke("subscribe", accountHistoryRequest(bob.human()))[jss::result]; + BEAST_EXPECT(jr[jss::error] == "invalidParams"); + BEAST_EXPECT(jr[jss::error_message] == "Too many subscriptions for this connection."); + } + } + + void + testAsyncTeardownDoesNotStall() + { + // Test C (core regression): disconnecting a connection with many + // account subscriptions must NOT block subsequent operations or + // publishing. The teardown is now posted to a JobQueue job + // (scheduleAccountCleanup), so it runs off the disconnect thread. + testcase("async teardown does not stall publishing"); + + using namespace std::chrono_literals; + using namespace jtx; + Env env{*this, singleThreadIo(envconfig())}; + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + BEAST_EXPECT(env.syncClose()); + + // A second, long-lived subscriber to alice that must keep receiving + // publishes after the first connection disconnects. + auto wscLive = makeWSClient(env.app().config()); + { + json::Value jv{json::ValueType::Object}; + jv[jss::accounts] = json::ValueType::Array; + jv[jss::accounts].append(alice.human()); + auto const r = wscLive->invoke("subscribe", jv); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + + // A connection that subscribes to many accounts, then disconnects. A + // few thousand entries is enough to be a real teardown while still + // running fast in CI. + constexpr std::size_t kBulk = 3000; + { + auto wscBulk = makeWSClient(env.app().config()); + auto const r = + wscBulk->invoke("subscribe", accountsRequest(makeAccountStrings(kBulk, 10))); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + // Destroying the client closes the WS connection, which destroys + // the server-side InfoSub and posts the chunked async cleanup job. + // WSClient exposes no explicit close(); resetting the owning + // unique_ptr is the disconnect path. + wscBulk.reset(); + } + + // Immediately after the disconnect, an unrelated operation completes + // promptly (it would block for seconds with inline teardown). This is a + // cheap liveness check; the publish assertion below is the real proof. + { + auto const info = env.app().getOPs().getServerInfo(false, true, false); + BEAST_EXPECT(info.isMember(jss::server_state)); + } + + // The live subscriber still receives a published transaction for alice + // within a short timeout, proving account-publishing was not stalled by + // the concurrent teardown. + { + env(pay(env.master, alice, XRP(100))); + BEAST_EXPECT(env.syncClose()); + BEAST_EXPECT(wscLive->findMsg(5s, [&](auto const& jv) { + return jv.isMember(jss::transaction) && + jv[jss::transaction][jss::TransactionType] == jss::Payment && + jv[jss::transaction][jss::Destination] == alice.human(); + })); + } + + wscLive->invoke("unsubscribe", accountsRequest({alice.human()})); + } + + void + testResubscribeAfterDisconnect() + { + // Test D (Phase 3 correctness): connection A subscribes to account X + // and disconnects (async cleanup pending, keyed on A's seq). A new + // connection B subscribes to X and MUST still receive publishes for X - + // A's deferred, seq-keyed cleanup must not remove B's subscription. + testcase("re-subscribe after disconnect still delivers"); + + using namespace std::chrono_literals; + using namespace jtx; + Env env{*this, singleThreadIo(envconfig())}; + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + BEAST_EXPECT(env.syncClose()); + + // Connection A subscribes to alice, then disconnects. A also subscribes + // to a bulk set so its deferred cleanup is non-trivial and races with B. + { + auto wscA = makeWSClient(env.app().config()); + auto bulk = makeAccountStrings(2000, 10); + bulk.push_back(alice.human()); + auto const r = wscA->invoke("subscribe", accountsRequest(bulk)); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + // Disconnect A by destroying its client (no explicit close()). + wscA.reset(); + } + + // Connection B (a new InfoSub with a distinct seq) subscribes to alice. + auto wscB = makeWSClient(env.app().config()); + { + json::Value jv{json::ValueType::Object}; + jv[jss::accounts] = json::ValueType::Array; + jv[jss::accounts].append(alice.human()); + auto const r = wscB->invoke("subscribe", jv); + BEAST_EXPECTS(r[jss::status] == "success", to_string(r)); + } + + // A publish for alice must reach B. If A's seq-keyed cleanup had wrongly + // removed the shared alice entry, B would receive nothing. + { + env(pay(env.master, alice, XRP(100))); + BEAST_EXPECT(env.syncClose()); + BEAST_EXPECT(wscB->findMsg(5s, [&](auto const& jv) { + return jv.isMember(jss::transaction) && + jv[jss::transaction][jss::TransactionType] == jss::Payment && + jv[jss::transaction][jss::Destination] == alice.human(); + })); + } + + wscB->invoke("unsubscribe", accountsRequest({alice.human()})); + } + void run() override { @@ -1569,6 +1977,14 @@ public: testSubBookChanges(); testNFToken(all); testNFToken(all - featureNFTokenMintOffer); + testAsyncTeardownDoesNotStall(); + testResubscribeAfterDisconnect(); + testSubscriptionCapRejects(); + testReSubscribeNotOvercounted(); + testBooksCapIndependentOfAccounts(); + testMultiFieldNoPartialSubscribe(); + testHistoryReSubscribeNotOvercounted(); + testHistoryCapRejectsNetNew(); } }; diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index cafe72eff9..2fe046f3d4 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -33,6 +33,7 @@ set(test_modules shamap tx protocol_autogen + server ) if(NOT WIN32) list(APPEND test_modules net) diff --git a/src/tests/libxrpl/server/InfoSub.cpp b/src/tests/libxrpl/server/InfoSub.cpp new file mode 100644 index 0000000000..6913812a92 --- /dev/null +++ b/src/tests/libxrpl/server/InfoSub.cpp @@ -0,0 +1,60 @@ +#include + +#include + +#include +#include + +using namespace xrpl; + +// The per-connection subscription cap is enforced by the pure predicate +// exceedsSubscriptionCap(current, additional). Testing it directly (rather than +// by subscribing the real cap through a WebSocket, which would exceed the frame +// limit and drop the connection before the check runs) lets the boundary be +// asserted exactly. +TEST(InfoSubSubscriptionCap, Boundary) +{ + constexpr std::size_t cap = kMaxSubscriptionsPerConnection; + + // Empty connection: anything up to the cap is admitted, cap+1 is not. + EXPECT_FALSE(exceedsSubscriptionCap(0, 0)); + EXPECT_FALSE(exceedsSubscriptionCap(0, cap)); + EXPECT_TRUE(exceedsSubscriptionCap(0, cap + 1)); + + // Exactly at the cap: zero more is fine, one more is rejected. + EXPECT_FALSE(exceedsSubscriptionCap(cap, 0)); + EXPECT_TRUE(exceedsSubscriptionCap(cap, 1)); + + // One below the cap: exactly one more reaches the cap; two exceed it. + EXPECT_FALSE(exceedsSubscriptionCap(cap - 1, 1)); + EXPECT_TRUE(exceedsSubscriptionCap(cap - 1, 2)); +} + +TEST(InfoSubSubscriptionCap, NoOverflow) +{ + constexpr std::size_t cap = kMaxSubscriptionsPerConnection; + constexpr std::size_t max = std::numeric_limits::max(); + + // current + additional must not wrap: a huge additional is rejected even + // when current is 0 (the additional > cap term guards the subtraction). + EXPECT_TRUE(exceedsSubscriptionCap(0, max)); + EXPECT_TRUE(exceedsSubscriptionCap(cap, max)); +} + +TEST(InfoSubSubscriptionCap, ExplicitCap) +{ + // A configured override is honored: the boundary tracks the passed cap, not + // the built-in default. This is the seam doSubscribe uses to enforce a + // per-connection cap set via [max_subscriptions_per_connection]. + constexpr std::size_t cap = 5; + + EXPECT_FALSE(exceedsSubscriptionCap(0, cap, cap)); + EXPECT_TRUE(exceedsSubscriptionCap(0, cap + 1, cap)); + EXPECT_FALSE(exceedsSubscriptionCap(cap, 0, cap)); + EXPECT_TRUE(exceedsSubscriptionCap(cap, 1, cap)); + EXPECT_FALSE(exceedsSubscriptionCap(cap - 1, 1, cap)); + EXPECT_TRUE(exceedsSubscriptionCap(cap - 1, 2, cap)); + + // The overflow guard still holds with a small explicit cap. + EXPECT_TRUE(exceedsSubscriptionCap(0, std::numeric_limits::max(), cap)); +} diff --git a/src/xrpld/app/ledger/AcceptedLedger.h b/src/xrpld/app/ledger/AcceptedLedger.h index 6e42d611d4..a8b78d08b0 100644 --- a/src/xrpld/app/ledger/AcceptedLedger.h +++ b/src/xrpld/app/ledger/AcceptedLedger.h @@ -57,6 +57,15 @@ public: return transactions_.end(); } + /** + * The last accepted transaction. Precondition: size() > 0. + */ + [[nodiscard]] AcceptedLedgerTx const& + back() const + { + return *transactions_.back(); + } + private: std::shared_ptr ledger_; std::vector> transactions_; diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 4b0091dff6..b43149b5af 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -154,6 +154,12 @@ namespace xrpl { +/** + * Concrete NetworkOPs: server sequencer, network tracker, and owner of all + * client subscription state (accounts, books, streams). Subscriptions use three + * independent non-recursive locks (accountLock_, bookLock_, streamLock_); see + * their declarations for the locking and deferred-destruction rules. + */ class NetworkOPsImp final : public NetworkOPs { /** @@ -194,7 +200,7 @@ class NetworkOPsImp final : public NetworkOPs /** * State accounting records two attributes for each possible server state: * 1) Amount of time spent in each state (in microseconds). This value is - * updated upon each state transition. + * updated upon each state transition. * 2) Number of transitions to each state. * * This data can be polled through server_info and represented by @@ -573,6 +579,13 @@ public: unsubAccountHistoryInternal(std::uint64_t seq, AccountID const& account, bool historyOnly) override; + void + scheduleAccountCleanup( + std::uint64_t seq, + hash_set rtAccounts, + hash_set normalAccounts, + hash_set historyAccounts) override; + bool subLedger(InfoSub::ref ispListener, json::Value& jvResult) override; bool @@ -636,6 +649,20 @@ public: bool tryRemoveRpcSub(std::string const& strUrl) override; + /** + * Look up an RPC subscription without taking streamLock_. + * + * Callers MUST already hold streamLock_. This exists so tryRemoveRpcSub + * can reuse the lookup while holding the lock; the plain std::mutex is not + * recursive, so calling the public findRpcSub (which locks) from under the + * lock would self-deadlock. + * + * @param strUrl The subscription URL key into rpcSubMap_. + * @return The matching InfoSub, or an empty pointer if not found. + */ + InfoSub::pointer + findRpcSubLocked(std::string const& strUrl); + beast::Journal const& journal() const override { @@ -724,22 +751,22 @@ private: * Extracts the set of order books affected by @p transaction, then * delivers @p jvObj to every live subscriber of those books. * - * Uses a two-pass design to keep subLock_ hold time short: - * 1. Under subLock_, collect strong InfoSub pointers for all live - * subscribers and prune any expired weak_ptrs encountered. - * 2. Release subLock_, then call send() on each collected pointer. + * Uses a two-pass design to keep bookLock_ hold time short: + * 1. Under bookLock_, collect strong InfoSub pointers for all live + * subscribers and prune any expired weak_ptrs encountered. + * 2. Release bookLock_, then call send() on each collected pointer. * * @param transaction The accepted ledger transaction to inspect. * @param jvObj JSON representation of the transaction to deliver. * - * @note Thread-safety: acquires subLock_ for the collection pass only. - * send() is intentionally called outside the lock to avoid blocking - * all other sub/unsub/publish paths while I/O is in progress. - * @note Contention: subLock_ is shared with all other subscription types. - * On high-throughput nodes processing multi-hop payments that touch - * many offer nodes, this pass holds subLock_ longer than the old - * per-book BookListeners locks did. This is an accepted trade-off - * for lock-domain simplicity. + * @note Thread-safety: acquires bookLock_ for the collection pass only. + * send() is intentionally called outside the lock to avoid blocking + * other book sub/unsub/publish paths while I/O is in progress. + * @note Contention: bookLock_ guards only book subscriptions, so this pass + * no longer competes with account or stream traffic. On high-throughput + * nodes processing multi-hop payments that touch many offer nodes, it + * still holds bookLock_ longer than the old per-book BookListeners + * locks did. This is an accepted trade-off for lock-domain simplicity. */ void pubBookTransaction(AcceptedLedgerTx const& transaction, MultiApiJson const& jvObj); @@ -750,6 +777,23 @@ private: std::shared_ptr const& transaction, TER result); + /** + * Send the ledgerClosed and book-changes stream updates for a ledger. + * Takes streamLock_ only. + */ + void + publishLedgerStreams( + std::shared_ptr const& lpAccepted, + std::shared_ptr const& alpAccepted); + + /** + * On the first published ledger only, start the delayed account-history + * streaming for any subscriptions that were registered before a validated + * ledger existed. Takes accountLock_ only. + */ + void + kickoffAccountHistory(std::shared_ptr const& alpAccepted); + void pubServer(); void @@ -802,7 +846,9 @@ private: hash_map>; /** - * @note called while holding subLock_ + * @note called while holding accountLock_ (it only touches + * subAccountHistory_ and posts a JobQueue task; it never reacquires + * a subscription lock nor touches the stream maps). */ void subAccountHistoryStart( @@ -813,12 +859,85 @@ private: void setAccountHistoryJobTimer(SubAccountHistoryInfoWeak subInfo); + /** + * Maximum number of account entries erased per accountLock_ acquisition + * during disconnect-time cleanup. + * + * The cleanup erase loops drop and reacquire accountLock_ after every + * chunk of this many accounts, bounding how long a large teardown holds + * the lock. A concurrent publish may interleave between chunks; that is + * safe because publishing tolerates a partially-cleaned map (a dead + * subscriber is simply not notified). + */ + static constexpr std::size_t kAccountCleanupChunk = 4096; + + /** + * Erase one connection's entries from a subscription map in + * accountLock_-bounded chunks. + * + * Shared engine behind cleanupAccountSubscriptions and + * cleanupAccountHistorySubscriptions: both walk @p accounts, and for each + * remove this connection's @p seq from the inner per-account map, dropping + * the outer entry once its last subscriber leaves. The lock is released + * between chunks so a competing publish can interleave; no iterator is held + * across the unlock, so a concurrent mutation cannot dangle. + * + * @tparam OuterMap hash_map>. + * @tparam BeforeErase Invoked with the inner value about to be erased, for + * per-entry teardown the plain account maps do not need + * (the history map uses it to stop its paging job). + * @param seq The disconnecting connection's subscription id. + * @param accounts The accounts this connection was subscribed to. + * @param outerMap The subscription map to erase from. + * @param beforeErase Called on each inner value just before it is erased. + * See kAccountCleanupChunk. + */ + template + void + cleanupSubscriptionMap( + std::uint64_t seq, + hash_set const& accounts, + OuterMap& outerMap, + BeforeErase&& beforeErase); + + /** + * Erase one connection's entries from the given account map (subAccount_ + * or subRTAccount_) in accountLock_-bounded chunks. The caller selects the + * map, so this need not know about the real-time/normal distinction. Keyed + * on seq, so it only removes the disconnecting connection's entries. + * See kAccountCleanupChunk. + */ + void + cleanupAccountSubscriptions( + std::uint64_t seq, + hash_set const& accounts, + SubInfoMapType& subMap); + + /** + * Erase one connection's entries from subAccountHistory_ in + * accountLock_-bounded chunks. Keyed on seq. See kAccountCleanupChunk. + */ + void + cleanupAccountHistorySubscriptions(std::uint64_t seq, hash_set const& accounts); + std::reference_wrapper registry_; beast::Journal journal_; std::unique_ptr localTX_; - std::recursive_mutex subLock_; + // Independent lock domains so a long cleanup/publish on one does not stall + // the others. Hold at most one at a time; if ever more, order: accountLock_, + // bookLock_, streamLock_. + // + // Deferred-destruction rule (non-recursive mutexes): under bookLock_ or + // streamLock_, never let the last InfoSub pointer die inside the lock - + // ~InfoSub re-acquires it via unsub* -> self-deadlock. Publishers collect the + // locked pointers in a vector declared before the lock and destruct after + // release (see pubServer / pubBookTransaction). accountLock_ is exempt: + // ~InfoSub offloads account teardown to scheduleAccountCleanup. + std::mutex accountLock_; ///< Guards subAccount_, subRTAccount_, subAccountHistory_. + std::mutex bookLock_; ///< Guards subBook_. + std::mutex streamLock_; ///< Guards streamMaps_[] and rpcSubMap_. std::atomic mode_; @@ -843,18 +962,18 @@ private: /** * Maps each order book to its current set of subscribers. - * Outer key: the Book (currency pair + optional domain). - * Inner key: InfoSub::seq (unique per connection). - * Inner value: weak_ptr so that a dropped connection does not prevent - * the InfoSub from being destroyed; expired entries are pruned lazily - * by pubBookTransaction and eagerly by unsubBookInternal (~InfoSub path). - * Guarded by subLock_. + * Outer key: the Book (currency pair + optional domain). + * Inner key: InfoSub::seq (unique per connection). + * Inner value: weak_ptr so that a dropped connection does not prevent + * the InfoSub from being destroyed; expired entries are pruned lazily + * by pubBookTransaction and eagerly by unsubBookInternal (~InfoSub path). + * Guarded by bookLock_. */ using SubBookMapType = hash_map; SubInfoMapType subAccount_; SubInfoMapType subRTAccount_; - SubBookMapType subBook_; ///< Guarded by subLock_. + SubBookMapType subBook_; ///< Guarded by bookLock_. subRpcMapType rpcSubMap_; @@ -875,6 +994,10 @@ private: SLastEntry // Any new entry must be ADDED ABOVE this one }; + /** + * One weak_ptr subscriber map per stream type. Guarded by streamLock_; + * subject to its deferred-destruction rule (see pubServer). + */ std::array streamMaps_; ServerFeeSummary lastFeeSummary_; @@ -2245,8 +2368,14 @@ NetworkOPsImp::consensusViewChange() void NetworkOPsImp::pubManifest(Manifest const& mo) { + // Hold each locked subscriber alive until after streamLock_ is released: + // if this is the last reference, ~InfoSub re-acquires streamLock_ (via its + // unsub* calls), which would self-deadlock on this non-recursive mutex. + // Declared before the lock so it is destroyed after the lock is dropped. + std::vector toRelease; + // VFALCO consider std::shared_mutex - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); if (!streamMaps_[SManifests].empty()) { @@ -2269,6 +2398,7 @@ NetworkOPsImp::pubManifest(Manifest const& mo) if (auto p = i->second.lock()) { p->send(jvObj, true); + toRelease.push_back(std::move(p)); ++i; } else @@ -2320,11 +2450,16 @@ trunc32(std::uint64_t v) void NetworkOPsImp::pubServer() { + // Hold each locked subscriber alive until after streamLock_ is released; a + // last-reference ~InfoSub would otherwise re-acquire this non-recursive + // mutex and self-deadlock. Declared before the lock, destroyed after it. + std::vector toRelease; + // VFALCO TODO Don't hold the lock across calls to send...make a copy of the // list into a local array while holding the lock then release // the lock and call send on everyone. // - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); if (!streamMaps_[SServer].empty()) { @@ -2362,7 +2497,7 @@ NetworkOPsImp::pubServer() for (auto i = streamMaps_[SServer].begin(); i != streamMaps_[SServer].end();) { - InfoSub::pointer const p = i->second.lock(); + InfoSub::pointer p = i->second.lock(); // VFALCO TODO research the possibility of using thread queues and // linearizing the deletion of subscribers with the @@ -2370,6 +2505,7 @@ NetworkOPsImp::pubServer() if (p) { p->send(jvObj, true); + toRelease.push_back(std::move(p)); ++i; } else @@ -2383,7 +2519,12 @@ NetworkOPsImp::pubServer() void NetworkOPsImp::pubConsensus(ConsensusPhase phase) { - std::scoped_lock const sl(subLock_); + // Hold each locked subscriber alive until after streamLock_ is released; a + // last-reference ~InfoSub would otherwise re-acquire this non-recursive + // mutex and self-deadlock. Declared before the lock, destroyed after it. + std::vector toRelease; + + std::scoped_lock const sl(streamLock_); auto& streamMap = streamMaps_[SConsensusPhase]; if (!streamMap.empty()) @@ -2397,6 +2538,7 @@ NetworkOPsImp::pubConsensus(ConsensusPhase phase) if (auto p = i->second.lock()) { p->send(jvObj, true); + toRelease.push_back(std::move(p)); ++i; } else @@ -2410,8 +2552,13 @@ NetworkOPsImp::pubConsensus(ConsensusPhase phase) void NetworkOPsImp::pubValidation(std::shared_ptr const& val) { + // Hold each locked subscriber alive until after streamLock_ is released; a + // last-reference ~InfoSub would otherwise re-acquire this non-recursive + // mutex and self-deadlock. Declared before the lock, destroyed after it. + std::vector toRelease; + // VFALCO consider std::shared_mutex - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); if (!streamMaps_[SValidations].empty()) { @@ -2503,6 +2650,7 @@ NetworkOPsImp::pubValidation(std::shared_ptr const& val) multiObj.visit( p->getApiVersion(), // [&](json::Value const& jv) { p->send(jv, true); }); + toRelease.push_back(std::move(p)); ++i; } else @@ -2516,7 +2664,12 @@ NetworkOPsImp::pubValidation(std::shared_ptr const& val) void NetworkOPsImp::pubPeerStatus(std::function const& func) { - std::scoped_lock const sl(subLock_); + // Hold each locked subscriber alive until after streamLock_ is released; a + // last-reference ~InfoSub would otherwise re-acquire this non-recursive + // mutex and self-deadlock. Declared before the lock, destroyed after it. + std::vector toRelease; + + std::scoped_lock const sl(streamLock_); if (!streamMaps_[SPeerStatus].empty()) { @@ -2526,11 +2679,12 @@ NetworkOPsImp::pubPeerStatus(std::function const& func) for (auto i = streamMaps_[SPeerStatus].begin(); i != streamMaps_[SPeerStatus].end();) { - InfoSub::pointer const p = i->second.lock(); + InfoSub::pointer p = i->second.lock(); if (p) { p->send(jvObj, true); + toRelease.push_back(std::move(p)); ++i; } else @@ -3074,7 +3228,13 @@ NetworkOPsImp::pubProposedTransaction( MultiApiJson const jvObj = transJson(transaction, result, false, ledger, std::nullopt); { - std::scoped_lock const sl(subLock_); + // Hold each locked subscriber alive until after streamLock_ is + // released; a last-reference ~InfoSub would otherwise re-acquire this + // non-recursive mutex and self-deadlock. Declared before the lock, + // destroyed after the block ends. + std::vector toRelease; + + std::scoped_lock const sl(streamLock_); auto it = streamMaps_[SRtTransactions].begin(); while (it != streamMaps_[SRtTransactions].end()) @@ -3086,6 +3246,7 @@ NetworkOPsImp::pubProposedTransaction( jvObj.visit( p->getApiVersion(), // [&](json::Value const& jv) { p->send(jv, true); }); + toRelease.push_back(std::move(p)); ++it; } else @@ -3117,100 +3278,121 @@ NetworkOPsImp::pubLedger(std::shared_ptr const& lpAccepted) alpAccepted->getLedger().get() == lpAccepted.get(), "xrpl::NetworkOPsImp::pubLedger : accepted input"); - { - JLOG(journal_.debug()) << "Publishing ledger " << lpAccepted->header().seq << " " - << lpAccepted->header().hash; + JLOG(journal_.debug()) << "Publishing ledger " << lpAccepted->header().seq << " " + << lpAccepted->header().hash; - std::scoped_lock const sl(subLock_); - - if (!streamMaps_[SLedger].empty()) - { - json::Value jvObj(json::ValueType::Object); - - jvObj[jss::type] = "ledgerClosed"; - jvObj[jss::ledger_index] = lpAccepted->header().seq; - jvObj[jss::ledger_hash] = to_string(lpAccepted->header().hash); - jvObj[jss::ledger_time] = - json::Value::UInt(lpAccepted->header().closeTime.time_since_epoch().count()); - - jvObj[jss::network_id] = registry_.get().getNetworkIDService().getNetworkID(); - - if (!lpAccepted->rules().enabled(featureXRPFees)) - jvObj[jss::fee_ref] = kFeeUnitsDeprecated; - jvObj[jss::fee_base] = lpAccepted->fees().base.jsonClipped(); - jvObj[jss::reserve_base] = lpAccepted->fees().reserve.jsonClipped(); - jvObj[jss::reserve_inc] = lpAccepted->fees().increment.jsonClipped(); - - jvObj[jss::txn_count] = json::UInt(alpAccepted->size()); - - if (mode_ >= OperatingMode::SYNCING) - { - jvObj[jss::validated_ledgers] = - registry_.get().getLedgerMaster().getCompleteLedgers(); - } - - auto it = streamMaps_[SLedger].begin(); - while (it != streamMaps_[SLedger].end()) - { - InfoSub::pointer const p = it->second.lock(); - if (p) - { - p->send(jvObj, true); - ++it; - } - else - { - it = streamMaps_[SLedger].erase(it); - } - } - } - - if (!streamMaps_[SBookChanges].empty()) - { - json::Value const jvObj = xrpl::RPC::computeBookChanges(lpAccepted); - - auto it = streamMaps_[SBookChanges].begin(); - while (it != streamMaps_[SBookChanges].end()) - { - InfoSub::pointer const p = it->second.lock(); - if (p) - { - p->send(jvObj, true); - ++it; - } - else - { - it = streamMaps_[SBookChanges].erase(it); - } - } - } - - { - static bool kFirstTime = true; - if (kFirstTime) - { - // First validated ledger, start delayed SubAccountHistory - kFirstTime = false; - for (auto& outer : subAccountHistory_) - { - for (auto& inner : outer.second) - { - auto& subInfo = inner.second; - if (subInfo.index->separationLedgerSeq == 0) - { - subAccountHistoryStart(alpAccepted->getLedger(), subInfo); - } - } - } - } - } - } + // Stream updates and the account-history kick-off touch different lock + // domains; each helper takes only its own lock, so the two are never held + // together. + publishLedgerStreams(lpAccepted, alpAccepted); + kickoffAccountHistory(alpAccepted); // Don't lock since pubAcceptedTransaction is locking. for (auto const& accTx : *alpAccepted) { JLOG(journal_.trace()) << "pubAccepted: " << accTx->getJson(); - pubValidatedTransaction(lpAccepted, *accTx, accTx == *(--alpAccepted->end())); + bool const last = &*accTx == &alpAccepted->back(); + pubValidatedTransaction(lpAccepted, *accTx, last); + } +} + +void +NetworkOPsImp::publishLedgerStreams( + std::shared_ptr const& lpAccepted, + std::shared_ptr const& alpAccepted) +{ + // Hold each locked subscriber alive until after streamLock_ is released; a + // last-reference ~InfoSub would otherwise re-acquire this non-recursive + // mutex and self-deadlock. Declared before the lock, destroyed after it; + // covers both the ledger and book-changes loops below. + std::vector toRelease; + + std::scoped_lock const sl(streamLock_); + + if (!streamMaps_[SLedger].empty()) + { + json::Value jvObj(json::ValueType::Object); + + jvObj[jss::type] = "ledgerClosed"; + jvObj[jss::ledger_index] = lpAccepted->header().seq; + jvObj[jss::ledger_hash] = to_string(lpAccepted->header().hash); + jvObj[jss::ledger_time] = + json::Value::UInt(lpAccepted->header().closeTime.time_since_epoch().count()); + + jvObj[jss::network_id] = registry_.get().getNetworkIDService().getNetworkID(); + + if (!lpAccepted->rules().enabled(featureXRPFees)) + jvObj[jss::fee_ref] = kFeeUnitsDeprecated; + jvObj[jss::fee_base] = lpAccepted->fees().base.jsonClipped(); + jvObj[jss::reserve_base] = lpAccepted->fees().reserve.jsonClipped(); + jvObj[jss::reserve_inc] = lpAccepted->fees().increment.jsonClipped(); + + jvObj[jss::txn_count] = json::UInt(alpAccepted->size()); + + if (mode_ >= OperatingMode::SYNCING) + { + jvObj[jss::validated_ledgers] = registry_.get().getLedgerMaster().getCompleteLedgers(); + } + auto it = streamMaps_[SLedger].begin(); + while (it != streamMaps_[SLedger].end()) + { + InfoSub::pointer p = it->second.lock(); + if (p) + { + p->send(jvObj, true); + toRelease.push_back(std::move(p)); + ++it; + } + else + { + it = streamMaps_[SLedger].erase(it); + } + } + } + + if (!streamMaps_[SBookChanges].empty()) + { + json::Value const jvObj = xrpl::RPC::computeBookChanges(lpAccepted); + + auto it = streamMaps_[SBookChanges].begin(); + while (it != streamMaps_[SBookChanges].end()) + { + InfoSub::pointer p = it->second.lock(); + if (p) + { + p->send(jvObj, true); + toRelease.push_back(std::move(p)); + ++it; + } + else + { + it = streamMaps_[SBookChanges].erase(it); + } + } + } +} + +void +NetworkOPsImp::kickoffAccountHistory(std::shared_ptr const& alpAccepted) +{ + // Runs exactly once, the first time a ledger is published. The atomic + // exchange lets the common post-first-ledger path return without taking + // accountLock_, while still admitting exactly one caller even if ledger + // publishing is ever made concurrent. + static std::atomic done{false}; + if (done.exchange(true)) + return; + + // It only reads/writes subAccountHistory_, so it takes accountLock_ alone. + std::scoped_lock const sl(accountLock_); + for (auto& outer : subAccountHistory_) + { + for (auto& inner : outer.second) + { + auto& subInfo = inner.second; + if (subInfo.index->separationLedgerSeq == 0) + subAccountHistoryStart(alpAccepted->getLedger(), subInfo); + } } } @@ -3249,7 +3431,7 @@ NetworkOPsImp::getLocalTxCount() std::size_t NetworkOPsImp::getBookSubscribersCount() { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(bookLock_); std::size_t total = 0; for (auto const& [_, subs] : subBook_) total += subs.size(); @@ -3376,7 +3558,13 @@ NetworkOPsImp::pubValidatedTransaction( MultiApiJson const jvObj = transJson(stTxn, trResult, true, ledger, metaRef); { - std::scoped_lock const sl(subLock_); + // Hold each locked subscriber alive until after streamLock_ is + // released; a last-reference ~InfoSub would otherwise re-acquire this + // non-recursive mutex and self-deadlock. Declared before the lock, + // destroyed after the block ends; covers both loops below. + std::vector toRelease; + + std::scoped_lock const sl(streamLock_); auto it = streamMaps_[STransactions].begin(); while (it != streamMaps_[STransactions].end()) @@ -3388,6 +3576,7 @@ NetworkOPsImp::pubValidatedTransaction( jvObj.visit( p->getApiVersion(), // [&](json::Value const& jv) { p->send(jv, true); }); + toRelease.push_back(std::move(p)); ++it; } else @@ -3407,6 +3596,7 @@ NetworkOPsImp::pubValidatedTransaction( jvObj.visit( p->getApiVersion(), // [&](json::Value const& jv) { p->send(jv, true); }); + toRelease.push_back(std::move(p)); ++it; } else @@ -3431,20 +3621,20 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con // Two-pass design: // - // 1. Under subLock_, walk subBook_, collect a strong pointer for each + // 1. Under bookLock_, walk subBook_, collect a strong pointer for each // unique listener (and prune any expired weak_ptrs we encounter). - // 2. Release subLock_, then send to each collected listener. + // 2. Release bookLock_, then send to each collected listener. // // Reasoning: - // * send() can be slow / blocking, so holding subLock_ across it would - // stall every other sub/unsub/pub path on this server (see the matching - // TODO above pubServer at line ~2275). - // * A strong pointer destructed while subLock_ is held risks running + // * send() can be slow / blocking, so holding bookLock_ across it would + // stall every other book sub/unsub/pub path on this server (see the + // matching TODO above pubServer at line ~2275). + // * A strong pointer destructed while bookLock_ is held risks running // ~InfoSub() in-line, which re-enters unsubBook() and mutates the very // subBook_/SubMapType being iterated -> dangling iterator UB. // - // Releasing subLock_ before any InfoSub::pointer can decay solves both. - // ~InfoSub() reacquires subLock_ via unsubBook() on its own and serializes + // Releasing bookLock_ before any InfoSub::pointer can decay solves both. + // ~InfoSub() reacquires bookLock_ via unsubBook() on its own and serializes // safely with concurrent traffic. std::vector listeners; @@ -3458,7 +3648,7 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con seen.reserve(books.size()); { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(bookLock_); for (auto const& book : books) { @@ -3496,8 +3686,8 @@ NetworkOPsImp::pubBookTransaction(AcceptedLedgerTx const& alTx, MultiApiJson con { jvObj.visit(p->getApiVersion(), [&](json::Value const& jv) { p->send(jv, true); }); } - // listeners destructs here, outside subLock_; ~InfoSub (if any fires) - // will reacquire subLock_ via unsubBook with no iterator hazard. + // listeners destructs here, outside bookLock_; ~InfoSub (if any fires) + // will reacquire bookLock_ via unsubBook with no iterator hazard. } void @@ -3513,7 +3703,7 @@ NetworkOPsImp::pubAccountTransaction( std::vector accountHistoryNotify; auto const currLedgerSeq = ledger->seq(); { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(accountLock_); if (!subAccount_.empty() || !subRTAccount_.empty() || !subAccountHistory_.empty()) { @@ -3646,7 +3836,7 @@ NetworkOPsImp::pubProposedAccountTransaction( std::vector accountHistoryNotify; { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(accountLock_); if (subRTAccount_.empty()) return; @@ -3730,7 +3920,7 @@ NetworkOPsImp::subAccount( isrListener->insertSubAccountInfo(naAccountID, rt); } - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(accountLock_); for (auto const& naAccountID : vnaAccountIDs) { @@ -3773,7 +3963,7 @@ NetworkOPsImp::unsubAccountInternal( hash_set const& vnaAccountIDs, bool rt) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(accountLock_); SubInfoMapType& subMap = rt ? subRTAccount_ : subAccount_; @@ -3795,6 +3985,122 @@ NetworkOPsImp::unsubAccountInternal( } } +template +void +NetworkOPsImp::cleanupSubscriptionMap( + std::uint64_t seq, + hash_set const& accounts, + OuterMap& outerMap, + BeforeErase&& beforeErase) +{ + // Walk the disconnecting connection's accounts in chunks. Each chunk takes + // accountLock_, erases up to kAccountCleanupChunk entries, then releases + // the lock so a competing account-publish can run before the next chunk. + // No iterator into outerMap is held across the unlock: every chunk re-finds + // each account, so a concurrent mutation between chunks cannot dangle. + auto it = accounts.begin(); + auto const end = accounts.end(); + while (it != end) + { + std::scoped_lock const sl(accountLock_); + + for (std::size_t n = 0; n < kAccountCleanupChunk && it != end; ++n, ++it) + { + auto outerIter = outerMap.find(*it); + if (outerIter != outerMap.end()) + { + // Give the caller a chance to tear down this connection's inner + // entry before it is erased (the history map stops its paging + // job here); the plain account maps pass a no-op. + auto innerIter = outerIter->second.find(seq); + if (innerIter != outerIter->second.end()) + beforeErase(innerIter->second); + + // Erase only this connection's seq; other connections sharing + // the account keep their entry, so a reconnect is unaffected. + outerIter->second.erase(seq); + if (outerIter->second.empty()) + outerMap.erase(outerIter); + } + } + } +} + +void +NetworkOPsImp::cleanupAccountSubscriptions( + std::uint64_t seq, + hash_set const& accounts, + SubInfoMapType& subMap) +{ + // Plain account maps need no per-entry teardown before erase. + cleanupSubscriptionMap(seq, accounts, subMap, [](InfoSub::wptr const&) {}); +} + +void +NetworkOPsImp::cleanupAccountHistorySubscriptions( + std::uint64_t seq, + hash_set const& accounts) +{ + // Cancel any in-flight historical paging job for this connection before + // dropping its record. The job holds its own shared_ptr to the index, so + // erasing the map entry alone would not stop it; it reads this atomic + // between pages and exits promptly once set. + cleanupSubscriptionMap( + seq, accounts, subAccountHistory_, [](SubAccountHistoryInfoWeak const& info) { + info.index->stopHistorical = true; + }); +} + +void +NetworkOPsImp::scheduleAccountCleanup( + std::uint64_t seq, + hash_set rtAccounts, + hash_set normalAccounts, + hash_set historyAccounts) +{ + // Nothing to do for a connection that never subscribed to any account. + if (rtAccounts.empty() && normalAccounts.empty() && historyAccounts.empty()) + return; + + // Post the erase work to a low-priority job so the disconnect thread (and + // ~InfoSub) returns immediately. The job captures the sets BY MOVE and + // operates purely on seq + the captured accounts; it never touches the + // destroyed InfoSub. `this` outlives the job per the Source lifetime + // contract. Running on a JobQueue thread, it cannot re-enter accountLock_ + // held by the disconnecting thread, so the plain std::mutex is safe. + // + // The body is exception-guarded: the JobQueue invokes it bare, so an + // escaping exception on the worker thread would terminate the process. + // + // addJob returns false only once the JobQueue has been stopped, i.e. during + // process shutdown. At that point NetworkOPsImp's maps are about to be + // destroyed wholesale and no publish path can run, so dropping the cleanup + // is harmless; no inline fallback is needed. + jobQueue_.addJob( + JtClientAcctHist, + "SubCleanup", + [this, + seq, + rt = std::move(rtAccounts), + normal = std::move(normalAccounts), + history = std::move(historyAccounts)]() noexcept { + try + { + cleanupAccountSubscriptions(seq, rt, subRTAccount_); + cleanupAccountSubscriptions(seq, normal, subAccount_); + cleanupAccountHistorySubscriptions(seq, history); + } + catch (std::exception const& e) + { + JLOG(journal_.error()) << "SubCleanup[seq=" << seq << "]: " << e.what(); + } + catch (...) + { + JLOG(journal_.error()) << "SubCleanup[seq=" << seq << "]: unknown exception"; + } + }); +} + void NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) { @@ -4077,7 +4383,7 @@ NetworkOPsImp::subAccountHistory(InfoSub::ref isrListener, AccountID const& acco return RpcInvalidParams; } - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(accountLock_); SubAccountHistoryInfoWeak ahi{ .sinkWptr = isrListener, .index = std::make_shared(accountId)}; auto simIterator = subAccountHistory_.find(accountId); @@ -4125,7 +4431,7 @@ NetworkOPsImp::unsubAccountHistoryInternal( AccountID const& account, bool historyOnly) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(accountLock_); auto simIterator = subAccountHistory_.find(account); if (simIterator != subAccountHistory_.end()) { @@ -4157,7 +4463,7 @@ NetworkOPsImp::subBook(InfoSub::ref isrListener, Book const& book) // prune in pubBookTransaction. With the reverse ordering, ~InfoSub would // call unsubBookInternal for a key that was never inserted server-side. { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(bookLock_); subBook_[book].try_emplace(isrListener->getSeq(), isrListener); } isrListener->insertBookSubscription(book); @@ -4177,7 +4483,7 @@ NetworkOPsImp::unsubBook(InfoSub::ref isrListener, Book const& book) bool NetworkOPsImp::unsubBookInternal(std::uint64_t uSeq, Book const& book) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(bookLock_); auto it = subBook_.find(book); if (it == subBook_.end()) return false; @@ -4227,7 +4533,7 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, json::Value& jvResult) jvResult[jss::validated_ledgers] = registry_.get().getLedgerMaster().getCompleteLedgers(); } - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SLedger].emplace(isrListener->getSeq(), isrListener).second; } @@ -4235,7 +4541,7 @@ NetworkOPsImp::subLedger(InfoSub::ref isrListener, json::Value& jvResult) bool NetworkOPsImp::subBookChanges(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SBookChanges].emplace(isrListener->getSeq(), isrListener).second; } @@ -4243,7 +4549,7 @@ NetworkOPsImp::subBookChanges(InfoSub::ref isrListener) bool NetworkOPsImp::unsubLedger(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SLedger].erase(uSeq) != 0u; } @@ -4251,7 +4557,7 @@ NetworkOPsImp::unsubLedger(std::uint64_t uSeq) bool NetworkOPsImp::unsubBookChanges(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SBookChanges].erase(uSeq) != 0u; } @@ -4259,7 +4565,7 @@ NetworkOPsImp::unsubBookChanges(std::uint64_t uSeq) bool NetworkOPsImp::subManifests(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SManifests].emplace(isrListener->getSeq(), isrListener).second; } @@ -4267,7 +4573,7 @@ NetworkOPsImp::subManifests(InfoSub::ref isrListener) bool NetworkOPsImp::unsubManifests(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SManifests].erase(uSeq) != 0u; } @@ -4292,7 +4598,7 @@ NetworkOPsImp::subServer(InfoSub::ref isrListener, json::Value& jvResult, bool a jvResult[jss::pubkey_node] = toBase58(TokenType::NodePublic, registry_.get().getApp().nodeIdentity().first); - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SServer].emplace(isrListener->getSeq(), isrListener).second; } @@ -4300,7 +4606,7 @@ NetworkOPsImp::subServer(InfoSub::ref isrListener, json::Value& jvResult, bool a bool NetworkOPsImp::unsubServer(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SServer].erase(uSeq) != 0u; } @@ -4308,7 +4614,7 @@ NetworkOPsImp::unsubServer(std::uint64_t uSeq) bool NetworkOPsImp::subTransactions(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[STransactions].emplace(isrListener->getSeq(), isrListener).second; } @@ -4316,7 +4622,7 @@ NetworkOPsImp::subTransactions(InfoSub::ref isrListener) bool NetworkOPsImp::unsubTransactions(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[STransactions].erase(uSeq) != 0u; } @@ -4324,7 +4630,7 @@ NetworkOPsImp::unsubTransactions(std::uint64_t uSeq) bool NetworkOPsImp::subRTTransactions(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SRtTransactions].emplace(isrListener->getSeq(), isrListener).second; } @@ -4332,7 +4638,7 @@ NetworkOPsImp::subRTTransactions(InfoSub::ref isrListener) bool NetworkOPsImp::unsubRTTransactions(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SRtTransactions].erase(uSeq) != 0u; } @@ -4340,7 +4646,7 @@ NetworkOPsImp::unsubRTTransactions(std::uint64_t uSeq) bool NetworkOPsImp::subValidations(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SValidations].emplace(isrListener->getSeq(), isrListener).second; } @@ -4354,7 +4660,7 @@ NetworkOPsImp::stateAccounting(json::Value& obj) bool NetworkOPsImp::unsubValidations(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SValidations].erase(uSeq) != 0u; } @@ -4362,7 +4668,7 @@ NetworkOPsImp::unsubValidations(std::uint64_t uSeq) bool NetworkOPsImp::subPeerStatus(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SPeerStatus].emplace(isrListener->getSeq(), isrListener).second; } @@ -4370,7 +4676,7 @@ NetworkOPsImp::subPeerStatus(InfoSub::ref isrListener) bool NetworkOPsImp::unsubPeerStatus(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SPeerStatus].erase(uSeq) != 0u; } @@ -4378,7 +4684,7 @@ NetworkOPsImp::unsubPeerStatus(std::uint64_t uSeq) bool NetworkOPsImp::subConsensus(InfoSub::ref isrListener) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SConsensusPhase].emplace(isrListener->getSeq(), isrListener).second; } @@ -4386,15 +4692,14 @@ NetworkOPsImp::subConsensus(InfoSub::ref isrListener) bool NetworkOPsImp::unsubConsensus(std::uint64_t uSeq) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); return streamMaps_[SConsensusPhase].erase(uSeq) != 0u; } InfoSub::pointer -NetworkOPsImp::findRpcSub(std::string const& strUrl) +NetworkOPsImp::findRpcSubLocked(std::string const& strUrl) { - std::scoped_lock const sl(subLock_); - + // Caller already holds streamLock_; this performs the lookup only. auto const it = rpcSubMap_.find(strUrl); if (it != rpcSubMap_.end()) @@ -4403,10 +4708,17 @@ NetworkOPsImp::findRpcSub(std::string const& strUrl) return InfoSub::pointer(); } +InfoSub::pointer +NetworkOPsImp::findRpcSub(std::string const& strUrl) +{ + std::scoped_lock const sl(streamLock_); + return findRpcSubLocked(strUrl); +} + InfoSub::pointer NetworkOPsImp::addRpcSub(std::string const& strUrl, InfoSub::ref rspEntry) { - std::scoped_lock const sl(subLock_); + std::scoped_lock const sl(streamLock_); rpcSubMap_.emplace(strUrl, rspEntry); @@ -4416,20 +4728,31 @@ NetworkOPsImp::addRpcSub(std::string const& strUrl, InfoSub::ref rspEntry) bool NetworkOPsImp::tryRemoveRpcSub(std::string const& strUrl) { - std::scoped_lock const sl(subLock_); - auto pInfo = findRpcSub(strUrl); - - if (!pInfo) - return false; - - // check to see if any of the stream maps still hold a weak reference to - // this entry before removing - for (SubMapType const& map : streamMaps_) + // Declared before the lock so it outlives the scoped_lock and is destroyed + // only after streamLock_ is released. The erase below may drop the last + // strong reference; if so, ~InfoSub runs and its unsub* calls re-acquire + // the non-recursive streamLock_. Destroying pInfo inside the lock would + // self-deadlock. + InfoSub::pointer pInfo; { - if (map.contains(pInfo->getSeq())) + std::scoped_lock const sl(streamLock_); + // Use the no-lock helper: we already hold streamLock_ and the mutex is + // not recursive, so calling the public findRpcSub here would deadlock. + pInfo = findRpcSubLocked(strUrl); + + if (!pInfo) return false; + + // check to see if any of the stream maps still hold a weak reference to + // this entry before removing + for (SubMapType const& map : streamMaps_) + { + if (map.contains(pInfo->getSeq())) + return false; + } + rpcSubMap_.erase(strUrl); } - rpcSubMap_.erase(strUrl); + // pInfo destroyed here, after streamLock_ is released. return true; } diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 852e46218a..a7cb5d053a 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -229,6 +229,12 @@ public: static constexpr int kMaxJobQueueTx = 1000; static constexpr int kMinJobQueueTx = 100; + // Optional override for the per-connection subscription cap. Unset means + // use the built-in default (kMaxSubscriptionsPerConnection in InfoSub.h). + // Kept as an override here, rather than the default itself, so the core + // module need not depend on the server module that owns the constant. + std::optional maxSubscriptionsPerConnection; + // Amendment majority time std::chrono::seconds amendmentMajorityTime = kDefaultAmendmentMajorityTime; diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 3b7b57328b..616717c5fd 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -677,6 +677,9 @@ Config::loadFromString(std::string const& fileContents) if (getSingleSection(secConfig, Sections::kNetworkQuorum, strTemp, j_)) networkQuorum = beast::lexicalCastThrow(strTemp); + if (getSingleSection(secConfig, Sections::kMaxSubscriptionsPerConnection, strTemp, j_)) + maxSubscriptionsPerConnection = beast::lexicalCastThrow(strTemp); + fees = setupFeeVote(section(Sections::kVoting)); /* [fee_default] is documented in the example config files as useful for * things like offline transaction signing. Until that's completely diff --git a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp index 93840bb6d6..cf43501c52 100644 --- a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include +#include #include #include #include @@ -26,6 +28,24 @@ namespace xrpl { +namespace { + +/** + * Test whether admitting `additional` subscriptions would exceed the cap. + * + * @param ispSub The connection's InfoSub, queried for its current count. + * @param additional Number of new items this branch would add. + * @param cap The effective per-connection cap for this request. + * @return true if the request must be rejected to stay within the cap. + */ +[[nodiscard]] bool +wouldExceedSubscriptionCap(InfoSub::ref ispSub, std::size_t additional, std::size_t cap) +{ + return exceedsSubscriptionCap(ispSub->totalSubscriptionCount(), additional, cap); +} + +} // namespace + json::Value doSubscribe(RPC::JsonContext& context) { @@ -105,6 +125,11 @@ doSubscribe(RPC::JsonContext& context) } ispSub->setApiVersion(context.apiVersion); + // Effective per-connection subscription cap: a configured override if set, + // otherwise the built-in default. Resolved once and reused by every branch. + std::size_t const subscriptionCap = + context.app.config().maxSubscriptionsPerConnection.value_or(kMaxSubscriptionsPerConnection); + if (context.params.isMember(jss::streams)) { if (!context.params[jss::streams].isArray()) @@ -166,30 +191,59 @@ doSubscribe(RPC::JsonContext& context) } } + // Parse the proposed (real-time) and normal account sets first, then check + // the cap against their COMBINED net-new total before subscribing either. + // This keeps the account pair all-or-nothing: it never subscribes one set + // and then rejects on the other. Other fields (streams and account_history) + // are still checked and subscribed independently, as they always have been, + // so a later field can be rejected after an earlier one subscribed. The cap + // counts only NET-NEW accounts (those not already tracked on this + // connection), so re-subscribing accounts already held is never wrongly + // rejected. auto accountsProposed = context.params.isMember(jss::accounts_proposed) ? jss::accounts_proposed : jss::rt_accounts; // DEPRECATED - if (context.params.isMember(accountsProposed)) + bool const hasProposed = context.params.isMember(accountsProposed); + bool const hasAccounts = context.params.isMember(jss::accounts); + + hash_set proposedIds; + hash_set accountIds; + + if (hasProposed) { if (!context.params[accountsProposed].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[accountsProposed]); - if (ids.empty()) + proposedIds = RPC::parseAccountIds(context.params[accountsProposed]); + if (proposedIds.empty()) return rpcError(RpcActMalformed); - context.netOps.subAccount(ispSub, ids, true); } - if (context.params.isMember(jss::accounts)) + if (hasAccounts) { if (!context.params[jss::accounts].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[jss::accounts]); - if (ids.empty()) + accountIds = RPC::parseAccountIds(context.params[jss::accounts]); + if (accountIds.empty()) return rpcError(RpcActMalformed); - context.netOps.subAccount(ispSub, ids, false); - JLOG(context.j.debug()) << "doSubscribe: accounts: " << ids.size(); + } + + if (hasProposed || hasAccounts) + { + // Atomic check-and-reserve, so two concurrent requests sharing this + // InfoSub (admin subscribe-by-url) cannot both pass the cap check. + if (!ispSub->tryReserveAccountSubscriptions(proposedIds, accountIds, subscriptionCap)) + return RPC::makeParamError("Too many subscriptions for this connection."); + } + + if (hasProposed) + context.netOps.subAccount(ispSub, proposedIds, true); + + if (hasAccounts) + { + context.netOps.subAccount(ispSub, accountIds, false); + JLOG(context.j.debug()) << "doSubscribe: accounts: " << accountIds.size(); } if (context.params.isMember(jss::account_history_tx_stream)) @@ -206,6 +260,13 @@ doSubscribe(RPC::JsonContext& context) if (!id) return rpcError(RpcInvalidParams); + // Charge the cap only when net-new, like the account branches. Not + // atomic here (subAccountHistory does its own dup-detecting insert), but + // a concurrent race adds at most one entry, so the overshoot is trivial. + std::size_t const historyCharge = ispSub->hasAccountHistorySubscription(*id) ? 0 : 1; + if (wouldExceedSubscriptionCap(ispSub, historyCharge, subscriptionCap)) + return RPC::makeParamError("Too many subscriptions for this connection."); + if (auto result = context.netOps.subAccountHistory(ispSub, *id); result != RpcSuccess) { return rpcError(result); @@ -222,6 +283,10 @@ doSubscribe(RPC::JsonContext& context) if (!context.params[jss::books].isArray()) return rpcError(RpcInvalidParams); + // Book subscriptions are tracked separately (OrderBookDB) and are not + // part of totalSubscriptionCount(), so they are not gated by the + // per-connection account cap. Each book entry is validated and + // subscribed below. for (auto& j : context.params[jss::books]) { if (!j.isObject() || !j.isMember(jss::taker_pays) || !j.isMember(jss::taker_gets) || From 7877ee42a0fa9f37959616e46e458f9f5eabdb5a Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:21:56 -0400 Subject: [PATCH 12/68] fix: Reject oversized validator manifest before decoding --- include/xrpl/basics/base64.h | 29 ++++++++++++++++++ include/xrpl/server/Manifest.h | 33 +++++++++++++++++++++ src/libxrpl/basics/base64.cpp | 18 ----------- src/libxrpl/server/Manifest.cpp | 5 ++++ src/xrpld/app/misc/detail/ValidatorList.cpp | 9 ++++++ 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h index 24fd660e65..4c743531a6 100644 --- a/include/xrpl/basics/base64.h +++ b/include/xrpl/basics/base64.h @@ -41,6 +41,35 @@ namespace xrpl { +namespace base64 { + +/** + * Returns the maximum number of characters needed to base64-encode @p nBytes bytes. + * + * @param nBytes Number of input bytes. + * @return Size of the encoded string, including padding. + */ +constexpr std::size_t +encodedSize(std::size_t const nBytes) +{ + return 4 * ((nBytes + 2) / 3); +} + +/** + * Returns the maximum number of bytes a base64 string of @p nChars characters + * decodes to. + * + * @param nChars Number of base64 characters. + * @return Upper bound on the number of decoded bytes. + */ +constexpr std::size_t +decodedSize(std::size_t const nChars) +{ + return ((nChars / 4) * 3) + 2; +} + +} // namespace base64 + std::string base64Encode(std::uint8_t const* data, std::size_t len); diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 710545271a..362080ef36 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -3,12 +3,14 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -164,6 +166,37 @@ struct Manifest std::string to_string(Manifest const& m); +/** + *Largest a valid manifest can be, in decoded bytes. + * + * A manifest has a fixed set of fields. Each is serialized as a field header + * (1-2 bytes), an optional length prefix (1 byte for these sizes), and the + * field body. Taking every field at its largest gives the maximum below, so + * anything larger cannot be a valid manifest. + * + * Field header + length + body = bytes + * sfVersion (U16) 2 0 2 4 + * sfSequence (U32) 1 0 4 5 + * sfPublicKey (33) 1 1 33 35 + * sfSigningPubKey (33) 1 1 33 35 + * sfSignature (72) 1 1 72 74 + * sfMasterSignature (72) 2 1 72 75 + * sfDomain (128) 1 1 128 130 + * ----- + * 358 + */ +constexpr std::size_t kMaxManifestBytes = 358; + +/** + * Largest a valid manifest can be, in base64 characters. + * + * base64 encodes 3 bytes as 4 characters, so this is the encoded form of + * @ref kMaxManifestBytes. Callers that receive a base64 manifest should + * reject anything longer than this before decoding, to avoid allocating + * memory for an oversized input. + */ +constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes); + /** * Constructs Manifest from serialized string * diff --git a/src/libxrpl/basics/base64.cpp b/src/libxrpl/basics/base64.cpp index c980a08669..f067dcbdca 100644 --- a/src/libxrpl/basics/base64.cpp +++ b/src/libxrpl/basics/base64.cpp @@ -76,24 +76,6 @@ getInverse() return &kTab[0]; } -/** - * Returns max chars needed to encode a base64 string - */ -constexpr std::size_t -encodedSize(std::size_t n) -{ - return 4 * ((n + 2) / 3); -} - -/** - * Returns max bytes needed to decode a base64 string - */ -constexpr std::size_t -decodedSize(std::size_t n) -{ - return ((n / 4) * 3) + 2; -} - /** * Encode a series of octets as a padded, base64 string. * diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index b26c67e531..798d8130b7 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -62,6 +62,11 @@ deserializeManifest(Slice s, beast::Journal journal) if (s.empty()) return std::nullopt; + // A valid manifest has a fixed maximum size, so reject anything larger + // before parsing it. + if (s.size() > kMaxManifestBytes) + return std::nullopt; + static SOTemplate const kManifestFormat{ // A manifest must include: // - the master public key diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index a9e7156158..b77dcc23c2 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1127,6 +1127,15 @@ ValidatorList::applyList( json::Value list; auto const& manifest = localManifest ? *localManifest : globalManifest; + // Reject an oversized manifest before decoding it, so we do not allocate + // memory for an input that cannot be a valid manifest. deserializeManifest + // also enforces the decoded-byte limit, but checking here avoids the + // base64 decode entirely. + if (manifest.size() > kMaxManifestBase64) + { + JLOG(j_.warn()) << "UNL manifest exceeds maximum size"; + return PublisherListStats{ListDisposition::Invalid}; + } auto m = deserializeManifest(base64Decode(manifest)); if (!m) { From 7d3611df2ab314b50e13a534fadcd60d4a6ebff3 Mon Sep 17 00:00:00 2001 From: Jingchen Date: Fri, 17 Jul 2026 12:33:44 +0100 Subject: [PATCH 13/68] fix: Compute validation suppression key over canonical serialisation --- include/xrpl/protocol/STObject.h | 8 +++-- include/xrpl/protocol/STValidation.h | 26 +++++++++++--- src/libxrpl/protocol/STObject.cpp | 20 ++++++++--- src/test/protocol/STValidation_test.cpp | 45 ++++++++++++++++++++----- src/xrpld/overlay/detail/PeerImp.cpp | 22 ++++++++---- 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index ad87d106c4..c7fc4fa796 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -90,7 +90,11 @@ public: operator=(STObject&& other); STObject(SOTemplate const& type, SField const& name); - STObject(SOTemplate const& type, SerialIter& sit, SField const& name); + STObject( + SOTemplate const& type, + SerialIter& sit, + SField const& name, + bool requireCanonicalOrder = false); STObject(SerialIter& sit, SField const& name, int depth = 0); STObject(SerialIter&& sit, SField const& name); explicit STObject(SField const& name); @@ -123,7 +127,7 @@ public: set(SOTemplate const&); bool - set(SerialIter& u, int depth = 0); + set(SerialIter& u, int depth = 0, bool requireCanonicalOrder = false); [[nodiscard]] SerializedTypeID getSType() const override; diff --git a/include/xrpl/protocol/STValidation.h b/include/xrpl/protocol/STValidation.h index 444fdfa600..8101b27341 100644 --- a/include/xrpl/protocol/STValidation.h +++ b/include/xrpl/protocol/STValidation.h @@ -54,6 +54,22 @@ class STValidation final : public STObject, public CountedObject NetClock::time_point seenTime_; public: + /** + * @struct DeserializeOptions + * @brief Options controlling deserialization of a STValidation. + + * @var DeserializeOptions::checkSignature + * Whether to verify the data was signed properly + * + * @var DeserializeOptions::requireCanonicalOrder + * Whether to require the fields to be in canonical order + */ + struct DeserializeOptions + { + bool checkSignature; + bool requireCanonicalOrder; + }; + /** * Construct a STValidation from a peer from serialized data. * @@ -64,12 +80,12 @@ public: * that signed the validation. For manifest based * validators, this should be the NodeID of the master * public key. - * @param checkSignature Whether to verify the data was signed properly + * @param options Options controlling deserialization * * @note Throws if the object is not valid */ template - STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature); + STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, DeserializeOptions options); /** * Construct, sign and trust a new STValidation issued by this node. @@ -163,8 +179,8 @@ private: }; template -STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature) - : STObject(validationFormat(), sit, sfValidation) +STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, DeserializeOptions options) + : STObject(validationFormat(), sit, sfValidation, options.requireCanonicalOrder) , signingPubKey_([this]() { auto const spk = getFieldVL(sfSigningPubKey); @@ -175,7 +191,7 @@ STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool ch }()) , nodeID_(lookupNodeID(signingPubKey_)) { - if (checkSignature && !isValid()) + if (options.checkSignature && !isValid()) { JLOG(debugLog().error()) << "Invalid signature in validation: " << getJson(JsonOptions::Values::None); diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 4b3ace2be3..e8a6df8c0a 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -56,10 +56,15 @@ STObject::STObject(SOTemplate const& type, SField const& name) : STBase(name) set(type); } -STObject::STObject(SOTemplate const& type, SerialIter& sit, SField const& name) : STBase(name) +STObject::STObject( + SOTemplate const& type, + SerialIter& sit, + SField const& name, + bool requireCanonicalOrder) + : STBase(name) { v_.reserve(type.size()); - set(sit); + set(sit, 0, requireCanonicalOrder); applyTemplate(type); // May throw } @@ -208,12 +213,13 @@ STObject::applyTemplateFromSField(SField const& sField) // return true = terminated with end-of-object bool -STObject::set(SerialIter& sit, int depth) +STObject::set(SerialIter& sit, int depth, bool requireCanonicalOrder) { bool reachedEndOfObject = false; v_.clear(); + std::optional prevFieldCode; // Consume data in the pipe until we run out or reach the end while (!sit.empty()) { @@ -238,7 +244,6 @@ STObject::set(SerialIter& sit, int depth) } auto const& fn = SField::getField(type, field); - if (fn.isInvalid()) { JLOG(debugLog().error()) @@ -246,6 +251,13 @@ STObject::set(SerialIter& sit, int depth) Throw("Unknown field"); } + if (requireCanonicalOrder && prevFieldCode.has_value() && fn.fieldCodeMem <= *prevFieldCode) + { + JLOG(debugLog().error()) << "Fields in object are not in canonical order"; + Throw("Fields in object are not in canonical order"); + } + prevFieldCode = fn.fieldCodeMem; + // Unflatten the field v_.emplace_back(sit, fn, depth + 1); diff --git a/src/test/protocol/STValidation_test.cpp b/src/test/protocol/STValidation_test.cpp index e42411bd3f..eb9aefd0ed 100644 --- a/src/test/protocol/STValidation_test.cpp +++ b/src/test/protocol/STValidation_test.cpp @@ -153,7 +153,10 @@ public: SerialIter sit{kPayload8}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, true); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = true, .requireCanonicalOrder = false}); BEAST_EXPECT(val); BEAST_EXPECT(val->isFieldPresent(sfLedgerSequence)); @@ -174,7 +177,10 @@ public: { SerialIter sit{kPayload1}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("An exception should have been thrown"); } catch (std::exception const& ex) @@ -186,7 +192,10 @@ public: { SerialIter sit{kPayload2}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("An exception should have been thrown"); } catch (std::exception const& ex) @@ -198,7 +207,10 @@ public: { SerialIter sit{kPayload3}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("An exception should have been thrown"); } catch (std::exception const& ex) @@ -210,7 +222,10 @@ public: { SerialIter sit{kPayload4}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("An exception should have been thrown"); } catch (std::exception const& ex) @@ -224,7 +239,10 @@ public: { SerialIter sit{kPayload5}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("Expected exception not thrown from validation"); } catch (std::exception const& ex) @@ -236,7 +254,10 @@ public: { SerialIter sit{kPayload6}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("Expected exception not thrown from validation"); } catch (std::exception const& ex) @@ -249,7 +270,10 @@ public: SerialIter sit{kPayload7}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, false); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = false}); fail("Expected exception not thrown from validation"); } @@ -279,7 +303,10 @@ public: SerialIter sit{makeSlice(v2)}; auto val = std::make_shared( - sit, [](PublicKey const& pk) { return calcNodeID(pk); }, true); + sit, + [](PublicKey const& pk) { return calcNodeID(pk); }, + STValidation::DeserializeOptions{ + .checkSignature = true, .requireCanonicalOrder = false}); fail("Mutated validation signature checked out: offset=" + std::to_string(i)); } diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 5662b6d33d..962ab0f408 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -2346,12 +2346,22 @@ PeerImp::onMessage(std::shared_ptr const& m) std::shared_ptr val; { SerialIter sit(makeSlice(m->validation())); - val = std::make_shared( - std::ref(sit), - [this](PublicKey const& pk) { - return calcNodeID(app_.getValidatorManifests().getMasterKey(pk)); - }, - false); + try + { + val = std::make_shared( + std::ref(sit), + [this](PublicKey const& pk) { + return calcNodeID(app_.getValidatorManifests().getMasterKey(pk)); + }, + STValidation::DeserializeOptions{ + .checkSignature = false, .requireCanonicalOrder = true}); + } + catch (std::exception const& e) + { + JLOG(pJournal_.warn()) << "Validation: Exception, " << e.what(); + fee_.update(Resource::kFeeInvalidData, e.what()); + return; + } val->setSeen(closeTime); } From 06a9b1b61762e3b7fa71b40720a13e93ce95b1dc Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 17 Jul 2026 13:07:01 +0100 Subject: [PATCH 14/68] chore: Update mpt-crypto to 1.0.2 --- conan.lock | 2 +- conanfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conan.lock b/conan.lock index 9dfbb86960..b5d0fe2cd4 100644 --- a/conan.lock +++ b/conan.lock @@ -12,7 +12,7 @@ "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933", "openssl/3.6.3#f806de8933e3bf6f01016c6a888cee2e%1783945160.863288", "nudb/2.0.9#11149c73f8f2baff9a0198fe25971fc7%1782392402.297166", - "mpt-crypto/0.4.0-rc4#ffdba12f2332357f0d8b0ae944cfff52%1784138702.932355", + "mpt-crypto/1.0.2#b313cef0c1a493eb970ad185b2e9bab7%1784285108.866483", "lz4/1.10.0#982d9b673900f665a1da109e09c17cab%1782392402.164188", "libiconv/1.17#9923bc6dc6f106646d6967e0039a5ada%1782392792.775744", "libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1782392402.420732", diff --git a/conanfile.py b/conanfile.py index f0b10cf34b..bde79db65d 100644 --- a/conanfile.py +++ b/conanfile.py @@ -134,7 +134,7 @@ class Xrpl(ConanFile): if self.options.jemalloc: self.requires("jemalloc/5.3.1") self.requires("lz4/1.10.0", force=True) - self.requires("mpt-crypto/0.4.0-rc4", transitive_headers=True) + self.requires("mpt-crypto/1.0.2", transitive_headers=True) self.requires("protobuf/6.33.5", force=True) if self.options.rocksdb: self.requires("rocksdb/10.5.1") From 90b2a68da89646afdceab3fd47545c64837c29a1 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:07:42 +0100 Subject: [PATCH 15/68] fix: Reject oversized SHAMap nodes in gotStaleData and fetch-pack path --- src/xrpld/app/ledger/InboundLedgers.h | 31 +++++++++++++++++++ .../app/ledger/detail/InboundLedgers.cpp | 3 ++ src/xrpld/overlay/detail/PeerImp.cpp | 13 ++++++++ 3 files changed, 47 insertions(+) diff --git a/src/xrpld/app/ledger/InboundLedgers.h b/src/xrpld/app/ledger/InboundLedgers.h index e288201c66..97182644e7 100644 --- a/src/xrpld/app/ledger/InboundLedgers.h +++ b/src/xrpld/app/ledger/InboundLedgers.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,36 @@ namespace xrpl { +// Per-node cap for AS state leaves stashed via `gotStaleData`. +// +// `gotStaleData` only handles `liAS_NODE` payloads, which carry +// SHAMap state-map leaves (ledger objects). +// +// Sizing: worst-case serialized size across all 31 ledger entry +// types is ~53 KB (`XChainOwnedCreateAccountClaimID`, 256 +// attestations x ~209 B, capped by `kMaxAttestations` in +// `include/xrpl/protocol/XChainAttestations.h`), followed by +// `XChainOwnedClaimID` ~40 KB, `NFTokenPage` ~9.5 KB, and +// `LedgerHashes` ~8.2 KB. 256 KiB leaves ~4.8x headroom over the +// current worst case. +// +// Future-proofing: this cap is NOT derived from a single protocol +// constant — it is a soft bound over independently-tuned caps +// (`kMaxAttestations`, `kDirMaxTokensPerPage`, `kMaxTokenUriLength`, +// etc.). Two types (`Amendments`, `NegativeUNL`) have no hard schema +// cap and grow with network state. Revisit if a new object type or +// a lifted array cap approaches ~256 KiB. The downstream +// `SHAMapAccountStateLeafNode` construction rejects anything above +// the 16 MiB SHAMapItem invariant regardless. +inline constexpr std::size_t kMaxFetchPackNodeBytes = 256 * 1024; + +// Aggregate cap on the sum of `nodedata().size()` across all entries +// in a single `TMLedgerData` message. Rejects amplification-shaped +// payloads (many nodes, each individually under `kMaxFetchPackNodeBytes`, +// that together dwarf the per-message budget) at ingress in PeerImp, +// before dispatch into `InboundLedger::gotData` or `gotStaleData`. +inline constexpr std::size_t kMaxLedgerDataBytes = megabytes(1); + /** * Manages the lifetime of inbound ledgers. * diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index dc361694cf..81544fd234 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -259,6 +259,9 @@ public: if (!node.has_nodeid() || !node.has_nodedata()) return; + if (node.nodedata().size() > kMaxFetchPackNodeBytes) + return; + auto newNode = SHAMapTreeNode::makeFromWire(makeSlice(node.nodedata())); if (!newNode) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 962ab0f408..0b969792ba 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1699,6 +1699,19 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } + { + std::size_t totalNodeBytes = 0; + for (int i = 0; i < m->nodes_size(); ++i) + totalNodeBytes += m->nodes(i).nodedata().size(); + if (totalNodeBytes > kMaxLedgerDataBytes) + { + JLOG(pJournal_.warn()) + << "Ledger data: oversized nodes (" << totalNodeBytes << " bytes)"; + fee_.update(Resource::kFeeInvalidData, "oversized ledger nodes"); + return; + } + } + // If there is a request cookie, attempt to relay the message if (m->has_requestcookie()) { From bf65e5fa7ba42f5fdb6f7ae2cff01ed344730723 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 17 Jul 2026 16:20:28 +0100 Subject: [PATCH 16/68] chore: Upload codecov for whole XRPLF org --- .github/workflows/reusable-build-test-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index b2327f67ea..a1daeed5fe 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -366,7 +366,7 @@ jobs: --target coverage - name: Upload coverage report - if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }} + if: ${{ github.repository_owner == 'XRPLF' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: disable_search: true From 033dca2f0e709360cc305b0e7a18615d642d114a Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Fri, 17 Jul 2026 14:02:35 -0400 Subject: [PATCH 17/68] feat: Make DynamicMPT opt-in-immutable --- include/xrpl/protocol/LedgerFormats.h | 22 +- include/xrpl/protocol/TxFlags.h | 59 +- .../xrpl/protocol/detail/ledger_entries.macro | 2 +- include/xrpl/protocol/detail/sfields.macro | 2 +- .../xrpl/protocol/detail/transactions.macro | 4 +- .../ledger_entries/MPTokenIssuance.h | 20 +- .../transactions/MPTokenIssuanceCreate.h | 20 +- .../transactions/MPTokenIssuanceSet.h | 20 +- .../transactors/token/MPTokenIssuanceCreate.h | 2 +- .../tx/transactors/token/MPTokenIssuanceSet.h | 34 ++ .../token/MPTokenIssuanceCreate.cpp | 32 +- .../transactors/token/MPTokenIssuanceSet.cpp | 139 ++--- .../tx/transactors/vault/VaultCreate.cpp | 1 - src/test/app/ConfidentialTransfer_test.cpp | 62 +- src/test/app/Delegate_test.cpp | 7 +- src/test/app/Loan_test.cpp | 5 +- src/test/app/MPToken_test.cpp | 538 +++++++++--------- src/test/app/Vault_test.cpp | 9 +- src/test/jtx/impl/mpt.cpp | 170 +++--- src/test/jtx/mpt.h | 9 +- .../ledger_entries/MPTokenIssuanceTests.cpp | 30 +- .../MPTokenIssuanceCreateTests.cpp | 30 +- .../transactions/MPTokenIssuanceSetTests.cpp | 30 +- 23 files changed, 619 insertions(+), 628 deletions(-) diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 7c504f6bdd..68205e27e6 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -190,17 +190,6 @@ enum LedgerEntryType : std::uint16_t { LSF_FLAG(lsfMPTCanClawback, 0x00000040) \ LSF_FLAG(lsfMPTCanHoldConfidentialBalance, 0x00000080)) \ \ - LEDGER_OBJECT(MPTokenIssuanceMutable, \ - LSF_FLAG(lsmfMPTCanEnableCanLock, 0x00000002) \ - LSF_FLAG(lsmfMPTCanEnableRequireAuth, 0x00000004) \ - LSF_FLAG(lsmfMPTCanEnableCanEscrow, 0x00000008) \ - LSF_FLAG(lsmfMPTCanEnableCanTrade, 0x00000010) \ - LSF_FLAG(lsmfMPTCanEnableCanTransfer, 0x00000020) \ - LSF_FLAG(lsmfMPTCanEnableCanClawback, 0x00000040) \ - LSF_FLAG(lsmfMPTCannotEnableCanHoldConfidentialBalance, 0x00000080) \ - LSF_FLAG(lsmfMPTCanMutateMetadata, 0x00010000) \ - LSF_FLAG(lsmfMPTCanMutateTransferFee, 0x00020000)) \ - \ LEDGER_OBJECT(MPToken, \ LSF_FLAG2(lsfMPTLocked, 0x00000001) \ LSF_FLAG(lsfMPTAuthorized, 0x00000002) \ @@ -294,6 +283,17 @@ getAllLedgerFlags() #pragma pop_macro("TO_MAP") #pragma pop_macro("ALL_LEDGER_FLAGS") +// MPTokenIssuance ImmutableFlags (sfImmutableFlags) +inline constexpr std::uint32_t lsifMPTCanLock = 0x00000002; +inline constexpr std::uint32_t lsifMPTRequireAuth = 0x00000004; +inline constexpr std::uint32_t lsifMPTCanEscrow = 0x00000008; +inline constexpr std::uint32_t lsifMPTCanTrade = 0x00000010; +inline constexpr std::uint32_t lsifMPTCanTransfer = 0x00000020; +inline constexpr std::uint32_t lsifMPTCanClawback = 0x00000040; +inline constexpr std::uint32_t lsifMPTCanHoldConfidentialBalance = 0x00000080; +inline constexpr std::uint32_t lsifMPTMetadata = 0x00010000; +inline constexpr std::uint32_t lsifMPTTransferFee = 0x00020000; + //------------------------------------------------------------------------------ /** diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 0afdebb898..14bc0571e9 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -152,7 +152,14 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; \ TRANSACTION(MPTokenIssuanceSet, \ TF_FLAG(tfMPTLock, 0x00000001) \ - TF_FLAG(tfMPTUnlock, 0x00000002), \ + TF_FLAG(tfMPTUnlock, 0x00000002) \ + TF_FLAG(tfMPTSetCanLock, 0x00000004) \ + TF_FLAG(tfMPTSetRequireAuth, 0x00000008) \ + TF_FLAG(tfMPTSetCanEscrow, 0x00000010) \ + TF_FLAG(tfMPTSetCanTrade, 0x00000020) \ + TF_FLAG(tfMPTSetCanTransfer, 0x00000040) \ + TF_FLAG(tfMPTSetCanClawback, 0x00000080) \ + TF_FLAG(tfMPTSetCanHoldConfidentialBalance, 0x00000100), \ MASK_ADJ(0)) \ \ TRANSACTION(NFTokenCreateOffer, \ @@ -356,38 +363,26 @@ inline constexpr FlagValue tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment); inline constexpr FlagValue tfTrustSetPermissionMask = ~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze); -// MPTokenIssuanceCreate MutableFlags: -// Indicating specific fields or flags may be changed after issuance. -inline constexpr FlagValue tmfMPTCanEnableCanLock = lsmfMPTCanEnableCanLock; -inline constexpr FlagValue tmfMPTCanEnableRequireAuth = lsmfMPTCanEnableRequireAuth; -inline constexpr FlagValue tmfMPTCanEnableCanEscrow = lsmfMPTCanEnableCanEscrow; -inline constexpr FlagValue tmfMPTCanEnableCanTrade = lsmfMPTCanEnableCanTrade; -inline constexpr FlagValue tmfMPTCanEnableCanTransfer = lsmfMPTCanEnableCanTransfer; -inline constexpr FlagValue tmfMPTCanEnableCanClawback = lsmfMPTCanEnableCanClawback; -inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata; -inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee; -inline constexpr FlagValue tmfMPTCannotEnableCanHoldConfidentialBalance = - lsmfMPTCannotEnableCanHoldConfidentialBalance; -inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask = - ~(tmfMPTCanEnableCanLock | tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanEscrow | - tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | tmfMPTCanEnableCanClawback | - tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee | - tmfMPTCannotEnableCanHoldConfidentialBalance); +// MPTokenIssuanceCreate / MPTokenIssuanceSet ImmutableFlags: +// Defines the immutable fields and flags specific to MPTokenIssuance. +inline constexpr FlagValue tifMPTCanLock = lsifMPTCanLock; +inline constexpr FlagValue tifMPTRequireAuth = lsifMPTRequireAuth; +inline constexpr FlagValue tifMPTCanEscrow = lsifMPTCanEscrow; +inline constexpr FlagValue tifMPTCanTrade = lsifMPTCanTrade; +inline constexpr FlagValue tifMPTCanTransfer = lsifMPTCanTransfer; +inline constexpr FlagValue tifMPTCanClawback = lsifMPTCanClawback; +inline constexpr FlagValue tifMPTMetadata = lsifMPTMetadata; +inline constexpr FlagValue tifMPTTransferFee = lsifMPTTransferFee; +inline constexpr FlagValue tifMPTCanHoldConfidentialBalance = lsifMPTCanHoldConfidentialBalance; +inline constexpr FlagValue tifMPTokenIssuanceImmutableMask = + ~(tifMPTCanLock | tifMPTRequireAuth | tifMPTCanEscrow | tifMPTCanTrade | tifMPTCanTransfer | + tifMPTCanClawback | tifMPTMetadata | tifMPTTransferFee | tifMPTCanHoldConfidentialBalance); -// MPTokenIssuanceSet MutableFlags: -// Enable mutable capability flags. These flags are one-way: once enabled, -// the corresponding capability cannot be disabled by MPTokenIssuanceSet. - -inline constexpr FlagValue tmfMPTSetCanLock = 0x00000001; -inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000002; -inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000004; -inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000008; -inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000010; -inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000020; -inline constexpr FlagValue tmfMPTSetCanHoldConfidentialBalance = 0x00000040; -inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = - ~(tmfMPTSetCanLock | tmfMPTSetRequireAuth | tmfMPTSetCanEscrow | tmfMPTSetCanTrade | - tmfMPTSetCanTransfer | tmfMPTSetCanClawback | tmfMPTSetCanHoldConfidentialBalance); +// MPTokenIssuanceSet set of flags that is used to enable capabilities on an MPTokenIssuance. +// Used as `txFlags & tfMPTokenIssuanceSetEnableFlagMask` to extract the capability-enabling bits. +inline constexpr FlagValue tfMPTokenIssuanceSetEnableFlagMask = tfMPTSetCanLock | + tfMPTSetRequireAuth | tfMPTSetCanEscrow | tfMPTSetCanTrade | tfMPTSetCanTransfer | + tfMPTSetCanClawback | tfMPTSetCanHoldConfidentialBalance; // Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a // TrustLine to be added to the issuer of that token without explicit permission from that issuer. diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 90810e06d2..cc2eff0c52 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -404,7 +404,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfPreviousTxnID, SoeRequired}, {sfPreviousTxnLgrSeq, SoeRequired}, {sfDomainID, SoeOptional}, - {sfMutableFlags, SoeDefault}, + {sfImmutableFlags, SoeDefault}, {sfReferenceHolding, SoeOptional}, {sfIssuerEncryptionKey, SoeOptional}, {sfAuditorEncryptionKey, SoeOptional}, diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 4ef76c8b75..56527628c9 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -97,7 +97,7 @@ TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) TYPED_SFIELD(sfPermissionValue, UINT32, 52) -TYPED_SFIELD(sfMutableFlags, UINT32, 53) +TYPED_SFIELD(sfImmutableFlags, UINT32, 53) TYPED_SFIELD(sfStartDate, UINT32, 54) TYPED_SFIELD(sfPaymentInterval, UINT32, 55) TYPED_SFIELD(sfGracePeriod, UINT32, 56) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index e805596c00..dc03cf7c59 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -705,7 +705,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, {sfMaximumAmount, SoeOptional}, {sfMPTokenMetadata, SoeOptional}, {sfDomainID, SoeOptional}, - {sfMutableFlags, SoeOptional}, + {sfImmutableFlags, SoeOptional}, })) /** This transaction type destroys a MPTokensIssuance instance */ @@ -734,7 +734,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, {sfDomainID, SoeOptional}, {sfMPTokenMetadata, SoeOptional}, {sfTransferFee, SoeOptional}, - {sfMutableFlags, SoeOptional}, + {sfImmutableFlags, SoeOptional}, {sfIssuerEncryptionKey, SoeOptional}, {sfAuditorEncryptionKey, SoeOptional}, })) diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h index 8518a0fe14..6a2caf52ae 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h @@ -256,27 +256,27 @@ public: } /** - * @brief Get sfMutableFlags (SoeDefault) + * @brief Get sfImmutableFlags (SoeDefault) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getMutableFlags() const + getImmutableFlags() const { - if (hasMutableFlags()) - return this->sle_->at(sfMutableFlags); + if (hasImmutableFlags()) + return this->sle_->at(sfImmutableFlags); return std::nullopt; } /** - * @brief Check if sfMutableFlags is present. + * @brief Check if sfImmutableFlags is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasMutableFlags() const + hasImmutableFlags() const { - return this->sle_->isFieldPresent(sfMutableFlags); + return this->sle_->isFieldPresent(sfImmutableFlags); } /** @@ -557,13 +557,13 @@ public: } /** - * @brief Set sfMutableFlags (SoeDefault) + * @brief Set sfImmutableFlags (SoeDefault) * @return Reference to this builder for method chaining. */ MPTokenIssuanceBuilder& - setMutableFlags(std::decay_t const& value) + setImmutableFlags(std::decay_t const& value) { - object_[sfMutableFlags] = value; + object_[sfImmutableFlags] = value; return *this; } diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h index e6fece8354..82ffba9996 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h @@ -178,29 +178,29 @@ public: } /** - * @brief Get sfMutableFlags (SoeOptional) + * @brief Get sfImmutableFlags (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getMutableFlags() const + getImmutableFlags() const { - if (hasMutableFlags()) + if (hasImmutableFlags()) { - return this->tx_->at(sfMutableFlags); + return this->tx_->at(sfImmutableFlags); } return std::nullopt; } /** - * @brief Check if sfMutableFlags is present. + * @brief Check if sfImmutableFlags is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasMutableFlags() const + hasImmutableFlags() const { - return this->tx_->isFieldPresent(sfMutableFlags); + return this->tx_->isFieldPresent(sfImmutableFlags); } }; @@ -302,13 +302,13 @@ public: } /** - * @brief Set sfMutableFlags (SoeOptional) + * @brief Set sfImmutableFlags (SoeOptional) * @return Reference to this builder for method chaining. */ MPTokenIssuanceCreateBuilder& - setMutableFlags(std::decay_t const& value) + setImmutableFlags(std::decay_t const& value) { - object_[sfMutableFlags] = value; + object_[sfImmutableFlags] = value; return *this; } diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h index 803868c640..ed7e1f0f6c 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h @@ -163,29 +163,29 @@ public: } /** - * @brief Get sfMutableFlags (SoeOptional) + * @brief Get sfImmutableFlags (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getMutableFlags() const + getImmutableFlags() const { - if (hasMutableFlags()) + if (hasImmutableFlags()) { - return this->tx_->at(sfMutableFlags); + return this->tx_->at(sfImmutableFlags); } return std::nullopt; } /** - * @brief Check if sfMutableFlags is present. + * @brief Check if sfImmutableFlags is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasMutableFlags() const + hasImmutableFlags() const { - return this->tx_->isFieldPresent(sfMutableFlags); + return this->tx_->isFieldPresent(sfImmutableFlags); } /** @@ -341,13 +341,13 @@ public: } /** - * @brief Set sfMutableFlags (SoeOptional) + * @brief Set sfImmutableFlags (SoeOptional) * @return Reference to this builder for method chaining. */ MPTokenIssuanceSetBuilder& - setMutableFlags(std::decay_t const& value) + setImmutableFlags(std::decay_t const& value) { - object_[sfMutableFlags] = value; + object_[sfImmutableFlags] = value; return *this; } diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index 5d35f65f44..1aa853d6e2 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -32,7 +32,7 @@ struct MPTCreateArgs std::optional transferFee = std::nullopt; std::optional const& metadata{}; std::optional domainId = std::nullopt; - std::optional mutableFlags = std::nullopt; + std::optional immutableFlags = std::nullopt; // Set only by callers that issue an MPT representing a wrapped asset // (e.g. VaultCreate's share token). The keylet must point to an // existing MPToken or RippleState owned by `account`. Surfaces on diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h index 52f155e8fe..a2a966009d 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h @@ -3,12 +3,15 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include namespace xrpl { @@ -22,6 +25,37 @@ public: { } + // Maps each MPTokenIssuanceSet set flag(e.g., tfMPTSetCanLock), to the issuance's + // corresponding immutable flag (e.g., lsifMPTCanLock) and the target ledger flag (e.g., + // lsfMPTCanLock). + struct FlagMapping + { + std::uint32_t setFlag; + std::uint32_t immutableFlag; + std::uint32_t ledgerFlag; + }; + + static constexpr std::array flagMapping = { + {{.setFlag = tfMPTSetCanLock, .immutableFlag = lsifMPTCanLock, .ledgerFlag = lsfMPTCanLock}, + {.setFlag = tfMPTSetRequireAuth, + .immutableFlag = lsifMPTRequireAuth, + .ledgerFlag = lsfMPTRequireAuth}, + {.setFlag = tfMPTSetCanEscrow, + .immutableFlag = lsifMPTCanEscrow, + .ledgerFlag = lsfMPTCanEscrow}, + {.setFlag = tfMPTSetCanTrade, + .immutableFlag = lsifMPTCanTrade, + .ledgerFlag = lsfMPTCanTrade}, + {.setFlag = tfMPTSetCanTransfer, + .immutableFlag = lsifMPTCanTransfer, + .ledgerFlag = lsfMPTCanTransfer}, + {.setFlag = tfMPTSetCanClawback, + .immutableFlag = lsifMPTCanClawback, + .ledgerFlag = lsfMPTCanClawback}, + {.setFlag = tfMPTSetCanHoldConfidentialBalance, + .immutableFlag = lsifMPTCanHoldConfidentialBalance, + .ledgerFlag = lsfMPTCanHoldConfidentialBalance}}}; + static bool checkExtraFeatures(PreflightContext const& ctx); diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index aad1642f68..375110c330 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -35,18 +35,24 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx) ctx.rules.enabled(featureSingleAssetVault))) return false; - if (ctx.tx.isFieldPresent(sfMutableFlags) && !ctx.rules.enabled(featureDynamicMPT)) + if (ctx.tx.isFieldPresent(sfImmutableFlags) && !ctx.rules.enabled(featureDynamicMPT)) return false; if (ctx.tx.isFlag(tfMPTCanHoldConfidentialBalance) && !ctx.rules.enabled(featureConfidentialTransfer)) return false; - // can not set tmfMPTCannotEnableCanHoldConfidentialBalance without featureConfidentialTransfer - auto const mutableFlags = ctx.tx[~sfMutableFlags]; - return !mutableFlags || - ((*mutableFlags & tmfMPTCannotEnableCanHoldConfidentialBalance) == 0u) || - ctx.rules.enabled(featureConfidentialTransfer); + // can not set tifMPTCanHoldConfidentialBalance without featureConfidentialTransfer + auto const immutableFlags = ctx.tx[~sfImmutableFlags]; + // NOLINTBEGIN(readability-simplify-boolean-expr) + if (immutableFlags && ((*immutableFlags & tifMPTCanHoldConfidentialBalance) != 0u) && + !ctx.rules.enabled(featureConfidentialTransfer)) + { + return false; + } + // NOLINTEND(readability-simplify-boolean-expr) + + return true; } std::uint32_t @@ -64,10 +70,10 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx.isFieldPresent(sfReferenceHolding)) return temMALFORMED; - // If the mutable flags field is included, at least one flag must be - // specified. - if (auto const mutableFlags = ctx.tx[~sfMutableFlags]; mutableFlags && - ((*mutableFlags == 0u) || ((*mutableFlags & tmfMPTokenIssuanceCreateMutableMask) != 0u))) + // If the immutable flags field is included, at least one flag must be + // specified, and undefined flags must not be specified. + if (auto const immutableFlags = ctx.tx[~sfImmutableFlags]; immutableFlags && + ((*immutableFlags == 0u) || ((*immutableFlags & tifMPTokenIssuanceImmutableMask) != 0u))) return temINVALID_FLAG; if (auto const fee = ctx.tx[~sfTransferFee]) @@ -170,8 +176,8 @@ MPTokenIssuanceCreate::create( if (args.domainId) (*mptIssuance)[sfDomainID] = *args.domainId; - if (args.mutableFlags) - (*mptIssuance)[sfMutableFlags] = *args.mutableFlags; + if (args.immutableFlags) + (*mptIssuance)[sfImmutableFlags] = *args.immutableFlags; if (args.referenceHolding) { @@ -217,7 +223,7 @@ MPTokenIssuanceCreate::doApply() .transferFee = tx[~sfTransferFee], .metadata = tx[~sfMPTokenMetadata], .domainId = tx[~sfDomainID], - .mutableFlags = tx[~sfMutableFlags], + .immutableFlags = tx[~sfImmutableFlags], }); return result ? tesSUCCESS : result.error(); } diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index d526251069..e8fd2e22b6 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -20,7 +20,6 @@ #include #include -#include #include namespace xrpl { @@ -39,56 +38,29 @@ MPTokenIssuanceSet::getFlagsMask(PreflightContext const& ctx) return tfMPTokenIssuanceSetMask; } -// Maps each MPTokenIssuanceSet MutableFlags to the corresponding mutable -// flag and the target ledger flag to mutate. -struct MPTMutabilityFlags -{ - std::uint32_t setFlag; - std::uint32_t canEnableFlag; - std::uint32_t ledgerFlag; -}; - -static constexpr std::array kMptMutabilityFlags = { - {{.setFlag = tmfMPTSetCanLock, - .canEnableFlag = lsmfMPTCanEnableCanLock, - .ledgerFlag = lsfMPTCanLock}, - {.setFlag = tmfMPTSetRequireAuth, - .canEnableFlag = lsmfMPTCanEnableRequireAuth, - .ledgerFlag = lsfMPTRequireAuth}, - {.setFlag = tmfMPTSetCanEscrow, - .canEnableFlag = lsmfMPTCanEnableCanEscrow, - .ledgerFlag = lsfMPTCanEscrow}, - {.setFlag = tmfMPTSetCanTrade, - .canEnableFlag = lsmfMPTCanEnableCanTrade, - .ledgerFlag = lsfMPTCanTrade}, - {.setFlag = tmfMPTSetCanTransfer, - .canEnableFlag = lsmfMPTCanEnableCanTransfer, - .ledgerFlag = lsfMPTCanTransfer}, - {.setFlag = tmfMPTSetCanClawback, - .canEnableFlag = lsmfMPTCanEnableCanClawback, - .ledgerFlag = lsfMPTCanClawback}}}; - NotTEC MPTokenIssuanceSet::preflight(PreflightContext const& ctx) { - auto const mutableFlags = ctx.tx[~sfMutableFlags]; + auto const txFlags = ctx.tx.getFlags(); + auto const enableFlags = txFlags & tfMPTokenIssuanceSetEnableFlagMask; auto const metadata = ctx.tx[~sfMPTokenMetadata]; auto const transferFee = ctx.tx[~sfTransferFee]; - auto const isMutate = mutableFlags || metadata || transferFee; + auto const immutableFlags = ctx.tx[~sfImmutableFlags]; + auto const isMutate = (enableFlags != 0u) || metadata || transferFee || immutableFlags; auto const hasIssuerElGamalKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey); auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey); - auto const txFlags = ctx.tx.getFlags(); - - bool const enablePrivacy = - mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u; + bool const enablePrivacy = (enableFlags & tfMPTSetCanHoldConfidentialBalance) != 0u; auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID); auto const hasHolder = ctx.tx.isFieldPresent(sfHolder); if (isMutate && !ctx.rules.enabled(featureDynamicMPT)) return temDISABLED; - if ((hasIssuerElGamalKey || hasAuditorElGamalKey || enablePrivacy) && + bool const setConfidentialBalanceImmutable = + immutableFlags && (*immutableFlags & tifMPTCanHoldConfidentialBalance) != 0u; + if ((hasIssuerElGamalKey || hasAuditorElGamalKey || enablePrivacy || + setConfidentialBalanceImmutable) && !ctx.rules.enabled(featureConfidentialTransfer)) return temDISABLED; @@ -122,8 +94,9 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (isMutate && holderID) return temMALFORMED; - // Can not set flags when mutating MPTokenIssuance - if (isMutate && ((ctx.tx.getFlags() & tfUniversalMask) != 0u)) + // A single transaction may either lock/unlock or mutate capability + // flags, but not both. + if (isMutate && (ctx.tx.isFlag(tfMPTLock) || ctx.tx.isFlag(tfMPTUnlock))) return temMALFORMED; if (transferFee && *transferFee > kMaxTransferFee) @@ -135,11 +108,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) if (metadata && metadata->length() > kMaxMpTokenMetadataLength) return temMALFORMED; - if (mutableFlags) - { - if ((*mutableFlags == 0u) || ((*mutableFlags & tmfMPTokenIssuanceSetMutableMask) != 0u)) - return temINVALID_FLAG; - } + // If the immutable flags field is included, at least one flag must be + // specified, and undefined flags must not be specified. + if (immutableFlags && + ((*immutableFlags == 0u) || + ((*immutableFlags & tifMPTokenIssuanceImmutableMask) != 0u))) + return temINVALID_FLAG; } if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey)) @@ -207,40 +181,32 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) } } - // sfMutableFlags is soeDEFAULT, defaulting to 0 if not specified on + // sfImmutableFlags is soeDEFAULT, defaulting to 0 if not specified on // the ledger. - auto const currentMutableFlags = sleMptIssuance->getFieldU32(sfMutableFlags); + auto const currentImmutableFlags = sleMptIssuance->getFieldU32(sfImmutableFlags); - auto isMutableFlag = [&](std::uint32_t mutableFlag) -> bool { - return currentMutableFlags & mutableFlag; - }; + auto isImmutable = [&](std::uint32_t flag) -> bool { return currentImmutableFlags & flag; }; - auto const mutableFlags = ctx.tx[~sfMutableFlags]; - // Whether the transaction is enabling confidential amounts. - bool const enablesConfidentialAmount = - mutableFlags && (*mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u; - if (mutableFlags) + auto const enableFlags = ctx.tx.getFlags() & tfMPTokenIssuanceSetEnableFlagMask; + if (enableFlags != 0u) { - if (std::ranges::any_of(kMptMutabilityFlags, [mutableFlags, &isMutableFlag](auto const& f) { - return !isMutableFlag(f.canEnableFlag) && ((*mutableFlags & f.setFlag) != 0u); + // If any of the flags to be set is immutable, return tecNO_PERMISSION. + if (std::ranges::any_of(flagMapping, [&](auto const& f) { + return isImmutable(f.immutableFlag) && ctx.tx.isFlag(f.setFlag); })) return tecNO_PERMISSION; - - if (enablesConfidentialAmount && - isMutableFlag(lsmfMPTCannotEnableCanHoldConfidentialBalance)) - return tecNO_PERMISSION; } - if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata)) + if (isImmutable(lsifMPTMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata)) return tecNO_PERMISSION; if (auto const fee = ctx.tx[~sfTransferFee]) { // A non-zero TransferFee is only valid if the lsfMPTCanTransfer flag - // was previously enabled (at issuance or via a prior mutation). Setting - // it by tmfMPTSetCanTransfer in the current transaction does not meet - // this requirement. - if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer)) + // is already set on the ledger object, or is being enabled by this + // same transaction. The Immutability of lsfMPTCanTransfer is checked above. + if (fee > 0u && !sleMptIssuance->isFlag(lsfMPTCanTransfer) && + (enableFlags & tfMPTSetCanTransfer) == 0u) return tecNO_PERMISSION; // Cannot set a non-zero TransferFee on an issuance that has confidential @@ -248,7 +214,8 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) if (fee > 0u && sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance)) return tecNO_PERMISSION; - if (!isMutableFlag(lsmfMPTCanMutateTransferFee)) + // Cannot set TransferFee if it is immutable + if (isImmutable(lsifMPTTransferFee)) return tecNO_PERMISSION; } @@ -266,27 +233,29 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; // LCOV_EXCL_LINE } - if (enablesConfidentialAmount && sleMptIssuance->isFieldPresent(sfTransferFee) && + auto const enablesConfidentialBalance = + (enableFlags & tfMPTSetCanHoldConfidentialBalance) != 0u; + if (enablesConfidentialBalance && sleMptIssuance->isFieldPresent(sfTransferFee) && (*sleMptIssuance)[sfTransferFee] > 0u) return tecNO_PERMISSION; // Encryption keys can only be set if confidential amounts are already // enabled on the issuance OR if the transaction is enabling it if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) && - !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount) + !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance) { return tecNO_PERMISSION; } if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) && - !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialAmount) + !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance) { return tecNO_PERMISSION; } // cannot upload key if there's circulating supply of COA if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) || - ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialAmount) && + ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialBalance) && (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0) { return tecNO_PERMISSION; // LCOV_EXCL_LINE @@ -327,23 +296,41 @@ MPTokenIssuanceSet::doApply() flagsOut &= ~lsfMPTLocked; } - if (auto const mutableFlags = ctx_.tx[~sfMutableFlags].value_or(0)) + if (auto const enableFlags = (ctx_.tx.getFlags() & tfMPTokenIssuanceSetEnableFlagMask); + enableFlags != 0u) { - for (auto const& f : kMptMutabilityFlags) + for (auto const& f : flagMapping) { - if ((mutableFlags & f.setFlag) != 0u) + if (ctx_.tx.isFlag(f.setFlag)) { flagsOut |= f.ledgerFlag; } } - - if ((mutableFlags & tmfMPTSetCanHoldConfidentialBalance) != 0u) - flagsOut |= lsfMPTCanHoldConfidentialBalance; } if (flagsIn != flagsOut) sle->setFieldU32(sfFlags, flagsOut); + if (auto const immutableFlags = ctx_.tx[~sfImmutableFlags]) + { + // sle is guaranteed to be an ltMPTOKEN_ISSUANCE rather than an ltMPTOKEN. + // Preflight verification ensures that sfHolder and sfImmutableFlags can + // never both be present in the same transaction. Therefore, if + // sfImmutableFlags is present, sfHolder must be absent. + // + // In doApply, the absence of sfHolder causes the MPTokenIssuance keylet + // to be peeked. The runtime check below is a defensive fallback in case + // this invariant is ever broken by a future change. + XRPL_ASSERT( + sle->getType() == ltMPTOKEN_ISSUANCE, + "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance"); + + if (sle->getType() != ltMPTOKEN_ISSUANCE) + return tecINTERNAL; // LCOV_EXCL_LINE + + (*sle)[sfImmutableFlags] = (*sle)[sfImmutableFlags] | *immutableFlags; + } + if (auto const transferFee = ctx_.tx[~sfTransferFee]) { // TransferFee uses soeDEFAULT style: diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index e1f5873a89..d793b3a1e2 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -209,7 +209,6 @@ VaultCreate::doApply() .transferFee = std::nullopt, .metadata = tx[~sfMPTokenMetadata], .domainId = tx[~sfDomainID], - .mutableFlags = std::nullopt, .referenceHolding = referenceHolding, }); if (!maybeShare) diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index d3e0182db5..864de165da 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -616,7 +616,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, }); } @@ -637,7 +637,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, .issuerPubKey = mptAlice.getPubKey(alice), .auditorPubKey = mptAlice.getPubKey(auditor), }); @@ -880,11 +880,11 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase Account const alice("alice"); MPTTester mptAlice(env, alice, {.holders = {}}); - // Create with tmfMPTCannotEnableCanHoldConfidentialBalance + // Create with tifMPTCanHoldConfidentialBalance mptAlice.create({ .ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + .immutableFlags = tifMPTCanHoldConfidentialBalance, }); mptAlice.generateKeyPair(alice); @@ -893,7 +893,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase // because the issuance cannot mutate canConfidentialAmount mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, .issuerPubKey = mptAlice.getPubKey(alice), .err = tecNO_PERMISSION, }); @@ -965,15 +965,11 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase Account const alice("alice"); MPTTester mptAlice(env, alice, {.holders = {}}); - mptAlice.create({ - .ownerCount = 1, - .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateTransferFee, - }); + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanLock}); mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, .transferFee = 100, .err = temBAD_TRANSFER_FEE, }); @@ -986,16 +982,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase Account const alice("alice"); MPTTester mptAlice(env, alice, {.holders = {}}); - mptAlice.create({ - .transferFee = 100, - .ownerCount = 1, - .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanMutateTransferFee, - }); + mptAlice.create( + {.transferFee = 100, .ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanLock}); mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, .err = tecNO_PERMISSION, }); } @@ -1007,11 +999,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase Account const alice("alice"); MPTTester mptAlice(env, alice, {.holders = {}}); - mptAlice.create({ - .ownerCount = 1, - .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, - .mutableFlags = tmfMPTCanMutateTransferFee, - }); + mptAlice.create( + {.ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance}); mptAlice.set({ .account = alice, @@ -5087,7 +5077,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase testcase("mutate lsfMPTCanHoldConfidentialBalance"); using namespace test::jtx; - // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance + // can not create mpt issuance with tifMPTCanHoldConfidentialBalance // when featureDynamicMPT is disabled { Env env{*this, features - featureDynamicMPT}; @@ -5097,12 +5087,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.create({ .ownerCount = 0, - .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + .immutableFlags = tifMPTCanHoldConfidentialBalance, .err = temDISABLED, }); } - // can not create mpt issuance with tmfMPTCannotEnableCanHoldConfidentialBalance when + // can not create mpt issuance with tifMPTCanHoldConfidentialBalance when // featureConfidentialTransfer is disabled { Env env{*this, features - featureConfidentialTransfer}; @@ -5112,12 +5102,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.create({ .ownerCount = 0, - .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + .immutableFlags = tifMPTCanHoldConfidentialBalance, .err = temDISABLED, }); } - // if lsmfMPTCannotEnableCanHoldConfidentialBalance is set, can not set/clear + // if lsifMPTCanHoldConfidentialBalance is set, can not set/clear // lsfMPTCanHoldConfidentialBalance { Env env{*this, features}; @@ -5128,12 +5118,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.create({ .ownerCount = 1, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCannotEnableCanHoldConfidentialBalance, + .immutableFlags = tifMPTCanHoldConfidentialBalance, }); mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, .err = tecNO_PERMISSION, }); } @@ -5148,7 +5138,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.create({ .ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, - .mutableFlags = tmfMPTCanEnableCanLock, + .immutableFlags = tifMPTCanLock, }); mptAlice.authorize({ @@ -5200,14 +5190,14 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase // lsfMPTCanHoldConfidentialBalance was already set mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, }); verifyToggle(tesSUCCESS, 10); - // set tmfMPTSetCanHoldConfidentialBalance again + // set tfMPTSetCanHoldConfidentialBalance again mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, }); verifyToggle(tesSUCCESS, 30); } @@ -5220,7 +5210,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase Account const bob("bob"); MPTTester mptAlice(env, alice, {.holders = {bob}}); - // lsmfMPTCannotEnableCanHoldConfidentialBalance is false by default, + // lsifMPTCanHoldConfidentialBalance is false by default, // so that lsfMPTCanHoldConfidentialBalance can be mutated mptAlice.create({ .ownerCount = 1, @@ -5243,7 +5233,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase // confidential outstanding balance mptAlice.set({ .account = alice, - .mutableFlags = tmfMPTSetCanHoldConfidentialBalance, + .flags = tfMPTSetCanHoldConfidentialBalance, .err = tecNO_PERMISSION, }); } diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index a8ddc27e6f..a3fa2ac0b5 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2167,11 +2167,12 @@ class Delegate_test : public beast::unit_test::Suite env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); env.close(); - // Field is not permitted, permitted fields for delegation is defined in - // permissions.macro. + // tfMPTSetCanLock is a valid MPTokenIssuanceSet flag but is not + // covered by the MPTokenIssuanceLock granular permission, so a + // delegate holding only that permission cannot set it. mpt.set( {.account = alice, - .mutableFlags = 2, + .flags = tfMPTSetCanLock, .delegate = bob, .err = terNO_DELEGATE_PERMISSION}); diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 371fcae54f..7035d1dca3 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -5448,8 +5448,7 @@ protected: {.env = env, .issuer = issuer, .holders = {lender, borrower}, - .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanTrade}); + .flags = tfMPTCanTransfer | tfMPTCanLock}); PrettyAsset const asset = mpt.issuanceID(); env(pay(issuer, lender, asset(10'000'000))); env(pay(issuer, borrower, asset(100'000))); @@ -5484,7 +5483,7 @@ protected: env.close(); // Enable CanTrade and verify the DEX path is restored. - mpt.set({.mutableFlags = tmfMPTSetCanTrade}); + mpt.set({.flags = tfMPTSetCanTrade}); env.close(); env(offer(lender, XRP(1), asset(10))); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index befc46e2ae..f3f967af81 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ #include #include #include +#include #include #include @@ -602,9 +604,9 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.authorize({.account = bob, .holderCount = 1}); - // test invalid flag - only valid flags are tfMPTLock (1) and Unlock - // (2) - mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG}); + // test invalid flag - an unrecognized flag bit is always + // rejected, regardless of which amendments are enabled + mptAlice.set({.account = alice, .flags = 0x00001000, .err = temINVALID_FLAG}); if (!features[featureSingleAssetVault] && !features[featureDynamicMPT] && !features[featureConfidentialTransfer]) @@ -3393,26 +3395,26 @@ class MPToken_test : public beast::unit_test::Suite using namespace test::jtx; Account const alice("alice"); - // Can not provide MutableFlags when DynamicMPT amendment is not enabled + // Can not provide ImmutableFlags when DynamicMPT amendment is not enabled { Env env{*this, features - featureDynamicMPT}; MPTTester mptAlice(env, alice); - mptAlice.create({.ownerCount = 0, .mutableFlags = 2, .err = temDISABLED}); - mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temDISABLED}); + mptAlice.create({.ownerCount = 0, .immutableFlags = 2, .err = temDISABLED}); + mptAlice.create({.ownerCount = 0, .immutableFlags = 0, .err = temDISABLED}); } - // MutableFlags contains invalid values + // ImmutableFlags contains invalid values { Env env{*this, features}; MPTTester mptAlice(env, alice); // Value 1 is reserved for MPT lock. - mptAlice.create({.ownerCount = 0, .mutableFlags = 1, .err = temINVALID_FLAG}); - mptAlice.create({.ownerCount = 0, .mutableFlags = 17, .err = temINVALID_FLAG}); - mptAlice.create({.ownerCount = 0, .mutableFlags = 65535, .err = temINVALID_FLAG}); + mptAlice.create({.ownerCount = 0, .immutableFlags = 1, .err = temINVALID_FLAG}); + mptAlice.create({.ownerCount = 0, .immutableFlags = 17, .err = temINVALID_FLAG}); + mptAlice.create({.ownerCount = 0, .immutableFlags = 65535, .err = temINVALID_FLAG}); - // MutableFlags can not be 0 - mptAlice.create({.ownerCount = 0, .mutableFlags = 0, .err = temINVALID_FLAG}); + // ImmutableFlags can not be 0 + mptAlice.create({.ownerCount = 0, .immutableFlags = 0, .err = temINVALID_FLAG}); } } @@ -3425,16 +3427,16 @@ class MPToken_test : public beast::unit_test::Suite Account const alice("alice"); Account const bob("bob"); - // Can not provide MutableFlags, MPTokenMetadata or TransferFee when + // Can not provide mutate related flags, MPTokenMetadata or TransferFee when // DynamicMPT amendment is not enabled { Env env{*this, features - featureDynamicMPT}; MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const mptID = makeMptID(env.seq(alice), alice); - // MutableFlags is not allowed when DynamicMPT is not enabled - mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 2, .err = temDISABLED}); - mptAlice.set({.account = alice, .id = mptID, .mutableFlags = 0, .err = temDISABLED}); + // Mutate related flags is not allowed when DynamicMPT is not enabled + mptAlice.set( + {.account = alice, .id = mptID, .flags = tfMPTSetCanLock, .err = temDISABLED}); // MPTokenMetadata is not allowed when DynamicMPT is not enabled mptAlice.set({.account = alice, .id = mptID, .metadata = "test", .err = temDISABLED}); @@ -3445,19 +3447,19 @@ class MPToken_test : public beast::unit_test::Suite mptAlice.set({.account = alice, .id = mptID, .transferFee = 0, .err = temDISABLED}); } - // Can not provide holder when MutableFlags, MPTokenMetadata or + // Can not provide holder when mutate related flags, MPTokenMetadata or // TransferFee is present { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const mptID = makeMptID(env.seq(alice), alice); - // Holder is not allowed when MutableFlags is present + // Holder is not allowed when mutate related flags is present mptAlice.set( {.account = alice, .holder = bob, .id = mptID, - .mutableFlags = 2, + .flags = tfMPTSetCanLock, .err = temMALFORMED}); // Holder is not allowed when MPTokenMetadata is present @@ -3477,27 +3479,24 @@ class MPToken_test : public beast::unit_test::Suite .err = temMALFORMED}); } - // Can not set Flags when MutableFlags, MPTokenMetadata or + // Can not lock when mutate related flags, MPTokenMetadata or // TransferFee is present { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateMetadata | tmfMPTCanEnableCanLock | - tmfMPTCanMutateTransferFee}); + mptAlice.create({.ownerCount = 1}); - // Setting flags is not allowed when MutableFlags is present + // Lock is not allowed when mutate related flags is present mptAlice.set( - {.account = alice, .flags = tfMPTCanLock, .mutableFlags = 2, .err = temMALFORMED}); + {.account = alice, .flags = tfMPTLock | tfMPTSetCanLock, .err = temMALFORMED}); - // Setting flags is not allowed when MPTokenMetadata is present + // Lock is not allowed when MPTokenMetadata is present mptAlice.set( - {.account = alice, .flags = tfMPTCanLock, .metadata = "test", .err = temMALFORMED}); + {.account = alice, .flags = tfMPTLock, .metadata = "test", .err = temMALFORMED}); - // setting flags is not allowed when TransferFee is present + // Lock is not allowed when TransferFee is present mptAlice.set( - {.account = alice, .flags = tfMPTCanLock, .transferFee = 100, .err = temMALFORMED}); + {.account = alice, .flags = tfMPTLock, .transferFee = 100, .err = temMALFORMED}); } // Flags being 0 or tfFullyCanonicalSig is fine @@ -3509,48 +3508,39 @@ class MPToken_test : public beast::unit_test::Suite {.transferFee = 10, .ownerCount = 1, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanMutateMetadata}); + .immutableFlags = tifMPTTransferFee}); - mptAlice.set({.account = alice, .flags = 0, .transferFee = 100, .metadata = "test"}); - mptAlice.set( - {.account = alice, - .flags = tfFullyCanonicalSig, - .transferFee = 200, - .metadata = "test2"}); + mptAlice.set({.account = alice, .flags = 0, .metadata = "test"}); + mptAlice.set({.account = alice, .flags = tfFullyCanonicalSig, .metadata = "test2"}); } - // Invalid MutableFlags + // Invalid flags { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const mptID = makeMptID(env.seq(alice), alice); - for (auto const flags : {10000, 0, 5000}) + for (auto const flags : {0x0200u, 0x0800u, 0x2000u, 0x0201u}) { mptAlice.set( - {.account = alice, .id = mptID, .mutableFlags = flags, .err = temINVALID_FLAG}); + {.account = alice, .id = mptID, .flags = flags, .err = temINVALID_FLAG}); } } - // Can not mutate flag which is not mutable + // Can not set flag which is immutable { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create({.ownerCount = 1}); + mptAlice.create( + {.ownerCount = 1, + .immutableFlags = tifMPTCanLock | tifMPTCanTrade | tifMPTCanTransfer | + tifMPTCanClawback | tifMPTCanEscrow | tifMPTRequireAuth | + tifMPTCanHoldConfidentialBalance}); - auto const mutableFlags = { - tmfMPTSetCanLock, - tmfMPTSetRequireAuth, - tmfMPTSetCanEscrow, - tmfMPTSetCanTrade, - tmfMPTSetCanTransfer, - tmfMPTSetCanClawback}; - - for (auto const& mutableFlag : mutableFlags) + for (auto const& f : MPTokenIssuanceSet::flagMapping) { - mptAlice.set( - {.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION}); + mptAlice.set({.account = alice, .flags = f.setFlag, .err = tecNO_PERMISSION}); } } @@ -3559,18 +3549,18 @@ class MPToken_test : public beast::unit_test::Suite Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata}); + mptAlice.create({.ownerCount = 1}); std::string const metadata(kMaxMpTokenMetadataLength + 1, 'a'); mptAlice.set({.account = alice, .metadata = metadata, .err = temMALFORMED}); } - // Can not mutate metadata when it is not mutable + // Can not set metadata when it is immutable { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create({.ownerCount = 1}); + mptAlice.create({.ownerCount = 1, .immutableFlags = tifMPTMetadata}); mptAlice.set({.account = alice, .metadata = "test", .err = tecNO_PERMISSION}); } @@ -3580,7 +3570,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob}}); auto const mptID = makeMptID(env.seq(alice), alice); - mptAlice.create({.ownerCount = 1, .mutableFlags = tmfMPTCanMutateTransferFee}); + mptAlice.create({.ownerCount = 1}); mptAlice.set( {.account = alice, @@ -3594,83 +3584,70 @@ class MPToken_test : public beast::unit_test::Suite Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .mutableFlags = tmfMPTCanMutateTransferFee | tmfMPTCanEnableCanTransfer}); + mptAlice.create({.ownerCount = 1}); + // MPTCanTransfer is not set, return tecNO_PERMISSION mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); - // Can not set transfer fee even when trying to set MPTCanTransfer - // at the same time. MPTCanTransfer must be set first, then transfer - // fee can be set in a separate transaction. - mptAlice.set( - {.account = alice, - .mutableFlags = tmfMPTSetCanTransfer, - .transferFee = 100, - .err = tecNO_PERMISSION}); + // Setting a non-zero transfer fee is fine if MPTCanTransfer is + // being enabled in the same transaction + mptAlice.set({.account = alice, .flags = tfMPTSetCanTransfer, .transferFee = 100}); + BEAST_EXPECT(mptAlice.checkFlags(lsfMPTCanTransfer)); + BEAST_EXPECT(mptAlice.checkTransferFee(100)); } - // Can not mutate transfer fee when it is not mutable + // Can not set transfer fee when it is immutable { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create({.transferFee = 10, .ownerCount = 1, .flags = tfMPTCanTransfer}); + mptAlice.create( + {.transferFee = 10, + .ownerCount = 1, + .flags = tfMPTCanTransfer, + .immutableFlags = tifMPTTransferFee}); mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); - mptAlice.set({.account = alice, .transferFee = 0, .err = tecNO_PERMISSION}); } - // Set some flags mutable. Can not mutate the others + // Set some flags immutable. Others can still be set. { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); mptAlice.create( {.ownerCount = 1, - .mutableFlags = tmfMPTCanEnableCanTrade | tmfMPTCanEnableCanTransfer | - tmfMPTCanMutateMetadata}); + .immutableFlags = tifMPTCanTrade | tifMPTCanTransfer | tifMPTMetadata}); - // Can not mutate transfer fee - mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); + auto const canEnableFlags = { + tfMPTSetCanLock, tfMPTSetRequireAuth, tfMPTSetCanEscrow, tfMPTSetCanClawback}; - auto const invalidFlags = { - tmfMPTSetCanLock, tmfMPTSetRequireAuth, tmfMPTSetCanEscrow, tmfMPTSetCanClawback}; + // Can not enable immutable flags + mptAlice.set({.account = alice, .flags = tfMPTSetCanTrade, .err = tecNO_PERMISSION}); + mptAlice.set({.account = alice, .flags = tfMPTSetCanTransfer, .err = tecNO_PERMISSION}); - // Can not mutate flags which are not mutable - for (auto const& mutableFlag : invalidFlags) + // Can enable flags which are not immutable + for (auto const& mutableFlag : canEnableFlags) { - mptAlice.set( - {.account = alice, .mutableFlags = mutableFlag, .err = tecNO_PERMISSION}); + mptAlice.set({.account = alice, .flags = mutableFlag}); } - - // Can mutate MPTCanTrade - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); - - // Can mutate MPTCanTransfer - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); - - // Can mutate metadata - mptAlice.set({.account = alice, .metadata = "test"}); - mptAlice.set({.account = alice, .metadata = ""}); } } void - testMutateMPT(FeatureBitset features) + testSetMPT(FeatureBitset features) { - testcase("Mutate MPT"); + testcase("Set MPT"); using namespace test::jtx; Account const alice("alice"); - // Mutate metadata + // Set metadata { Env env{*this, features}; MPTTester mptAlice(env, alice); - mptAlice.create( - {.metadata = "test", .ownerCount = 1, .mutableFlags = tmfMPTCanMutateMetadata}); + mptAlice.create({.metadata = "test", .ownerCount = 1}); std::vector const metadatas = { "mutate metadata", @@ -3691,7 +3668,7 @@ class MPToken_test : public beast::unit_test::Suite BEAST_EXPECT(!mptAlice.isMetadataPresent()); } - // Mutate transfer fee + // Set transfer fee { Env env{*this, features}; MPTTester mptAlice(env, alice); @@ -3699,8 +3676,7 @@ class MPToken_test : public beast::unit_test::Suite {.transferFee = 100, .metadata = "test", .ownerCount = 1, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee}); + .flags = tfMPTCanTransfer}); for (std::uint16_t const fee : std::initializer_list{1, 10, 100, 200, 500, 1000, kMaxTransferFee}) @@ -3718,33 +3694,29 @@ class MPToken_test : public beast::unit_test::Suite BEAST_EXPECT(mptAlice.checkTransferFee(10)); } - // Test mutable flag enablement + // Test setting flags { - auto testFlagSet = [&](std::uint32_t createFlags, std::uint32_t setFlags) { + auto testFlagSet = [&](std::uint32_t setFlags) { Env env{*this, features}; MPTTester mptAlice(env, alice); - // Create the MPT object with the specified initial flags - mptAlice.create({.metadata = "test", .ownerCount = 1, .mutableFlags = createFlags}); + // Create issuance and the flags can be enabled once by default. + mptAlice.create({.metadata = "test", .ownerCount = 1}); - // Setting the same mutable capability more than once is harmless. - mptAlice.set({.account = alice, .mutableFlags = setFlags}); - mptAlice.set({.account = alice, .mutableFlags = setFlags}); + // Setting the same immutable flag more than once is harmless. + mptAlice.set({.account = alice, .flags = setFlags}); + mptAlice.set({.account = alice, .flags = setFlags}); }; - testFlagSet(tmfMPTCanEnableCanLock, tmfMPTSetCanLock); - testFlagSet(tmfMPTCanEnableRequireAuth, tmfMPTSetRequireAuth); - testFlagSet(tmfMPTCanEnableCanEscrow, tmfMPTSetCanEscrow); - testFlagSet(tmfMPTCanEnableCanTrade, tmfMPTSetCanTrade); - testFlagSet(tmfMPTCanEnableCanTransfer, tmfMPTSetCanTransfer); - testFlagSet(tmfMPTCanEnableCanClawback, tmfMPTSetCanClawback); + for (auto const& f : MPTokenIssuanceSet::flagMapping) + testFlagSet(f.setFlag); } } void - testMutateCanLock(FeatureBitset features) + testSetCanLock(FeatureBitset features) { - testcase("Mutate MPTCanLock"); + testcase("Set MPTCanLock"); using namespace test::jtx; Account const alice("alice"); @@ -3754,78 +3726,41 @@ class MPToken_test : public beast::unit_test::Suite { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .holderCount = 0, - .flags = tfMPTCanLock | tfMPTCanTransfer, - .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanTrade | - tmfMPTCanMutateTransferFee}); + mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob, .holderCount = 1}); - // Lock bob's mptoken - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + // Lock bob's mptoken fails because alice has not enabled MPTCanLock + mptAlice.set( + {.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); - // Can mutate the mutable flags and fields - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTrade}); - mptAlice.set({.account = alice, .transferFee = 200}); + // set CanLock + mptAlice.set({.account = alice, .flags = tfMPTSetCanLock}); + + // Now can lock + mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); } // Global lock { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .holderCount = 0, - .flags = tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanClawback | - tmfMPTCanMutateMetadata}); + mptAlice.create({.ownerCount = 1, .holderCount = 0}); mptAlice.authorize({.account = bob, .holderCount = 1}); - // Lock issuance - mptAlice.set({.account = alice, .flags = tfMPTLock}); - - // Can mutate the mutable flags and fields - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback}); - mptAlice.set({.account = alice, .metadata = "mutate"}); - } - - // Test lock and unlock after enabling MPTCanLock - { - Env env{*this, features}; - MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .holderCount = 0, - .mutableFlags = tmfMPTCanEnableCanLock | tmfMPTCanEnableCanClawback | - tmfMPTCanMutateMetadata}); - mptAlice.authorize({.account = bob, .holderCount = 1}); - - // Can not lock or unlock before MPTCanLock is enabled + // Lock issuance fails because alice has not enabled MPTCanLock mptAlice.set({.account = alice, .flags = tfMPTLock, .err = tecNO_PERMISSION}); - mptAlice.set({.account = alice, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); - mptAlice.set( - {.account = alice, .holder = bob, .flags = tfMPTLock, .err = tecNO_PERMISSION}); - mptAlice.set( - {.account = alice, .holder = bob, .flags = tfMPTUnlock, .err = tecNO_PERMISSION}); - // Set MPTCanLock - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanLock}); - - // Can lock and unlock + // Set CanLock + mptAlice.set({.account = alice, .flags = tfMPTSetCanLock}); + // Now can lock mptAlice.set({.account = alice, .flags = tfMPTLock}); - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock}); - mptAlice.set({.account = alice, .flags = tfMPTUnlock}); - mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); } } void - testMutateRequireAuth(FeatureBitset features) + testSetRequireAuth(FeatureBitset features) { - testcase("Mutate MPTRequireAuth"); + testcase("Set MPTRequireAuth"); using namespace test::jtx; // test enabling RequireAuth flag on the issuance and its effect on payment @@ -3835,16 +3770,13 @@ class MPToken_test : public beast::unit_test::Suite Account const bob("bob"); MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanEnableRequireAuth}); + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = bob}); mptAlice.pay(alice, bob, 1000); - // Set RequireAuth because it is mutable. - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth}); + // Set RequireAuth + mptAlice.set({.account = alice, .flags = tfMPTSetRequireAuth}); // This should fail because bob is not authorized yet. mptAlice.pay(alice, bob, 1000, tecNO_AUTH); @@ -3855,9 +3787,9 @@ class MPToken_test : public beast::unit_test::Suite } void - testMutateCanEscrow(FeatureBitset features) + testSetCanEscrow(FeatureBitset features) { - testcase("Mutate MPTCanEscrow"); + testcase("Set MPTCanEscrow"); using namespace test::jtx; using namespace std::literals; @@ -3868,11 +3800,7 @@ class MPToken_test : public beast::unit_test::Suite auto const carol = Account("carol"); MPTTester mptAlice(env, alice, {.holders = {carol, bob}}); - mptAlice.create( - {.ownerCount = 1, - .holderCount = 0, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanEnableCanEscrow}); + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); mptAlice.authorize({.account = carol}); mptAlice.authorize({.account = bob}); @@ -3888,8 +3816,8 @@ class MPToken_test : public beast::unit_test::Suite Fee(baseFee * 150), Ter(tecNO_PERMISSION)); - // MPTCanEscrow is enabled now - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanEscrow}); + // Set MPTCanEscrow + mptAlice.set({.account = alice, .flags = tfMPTSetCanEscrow}); env(escrow::create(carol, bob, mpt(3)), escrow::kCondition(escrow::kCb1), escrow::kFinishTime(env.now() + 1s), @@ -3897,9 +3825,9 @@ class MPToken_test : public beast::unit_test::Suite } void - testMutateCanTransfer(FeatureBitset features) + testSetCanTransfer(FeatureBitset features) { - testcase("Mutate MPTCanTransfer"); + testcase("Set MPTCanTransfer"); using namespace test::jtx; Account const alice("alice"); @@ -3910,9 +3838,7 @@ class MPToken_test : public beast::unit_test::Suite Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); - mptAlice.create( - {.ownerCount = 1, - .mutableFlags = tmfMPTCanEnableCanTransfer | tmfMPTCanMutateTransferFee}); + mptAlice.create({.ownerCount = 1}); mptAlice.authorize({.account = bob}); mptAlice.authorize({.account = carol}); @@ -3926,20 +3852,10 @@ class MPToken_test : public beast::unit_test::Suite // Can not set non-zero transfer fee when MPTCanTransfer is not set mptAlice.set({.account = alice, .transferFee = 100, .err = tecNO_PERMISSION}); - // Can not set non-zero transfer fee even when trying to set - // MPTCanTransfer at the same time - mptAlice.set( - {.account = alice, - .mutableFlags = tmfMPTSetCanTransfer, - .transferFee = 100, - .err = tecNO_PERMISSION}); - - // Alice sets MPTCanTransfer - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanTransfer}); - - // Can set transfer fee now + // Set MPTCanTransfer BEAST_EXPECT(!mptAlice.isTransferFeePresent()); - mptAlice.set({.account = alice, .transferFee = 100}); + mptAlice.set({.account = alice, .flags = tfMPTSetCanTransfer, .transferFee = 100}); + BEAST_EXPECT(mptAlice.checkFlags(lsfMPTCanTransfer)); BEAST_EXPECT(mptAlice.isTransferFeePresent()); // Bob can pay carol @@ -3958,16 +3874,13 @@ class MPToken_test : public beast::unit_test::Suite } } - // Can set transfer fee to zero when tmfMPTCanMutateTransferFee is set. + // Can set transfer fee to zero when transfer fee is mutable (i.e. + // tifMPTTransferFee is not set). { Env env{*this, features}; MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); - mptAlice.create( - {.transferFee = 100, - .ownerCount = 1, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanMutateTransferFee}); + mptAlice.create({.transferFee = 100, .ownerCount = 1, .flags = tfMPTCanTransfer}); BEAST_EXPECT(mptAlice.checkTransferFee(100)); @@ -3978,9 +3891,9 @@ class MPToken_test : public beast::unit_test::Suite } void - testMutateCanClawback(FeatureBitset features) + testSetCanClawback(FeatureBitset features) { - testcase("Mutate MPTCanClawback"); + testcase("Set MPTCanClawback"); using namespace test::jtx; Env env(*this, features); @@ -3989,8 +3902,7 @@ class MPToken_test : public beast::unit_test::Suite MPTTester mptAlice(env, alice, {.holders = {bob}}); - mptAlice.create( - {.ownerCount = 1, .holderCount = 0, .mutableFlags = tmfMPTCanEnableCanClawback}); + mptAlice.create({.ownerCount = 1, .holderCount = 0}); // Bob creates an MPToken mptAlice.authorize({.account = bob}); @@ -4001,13 +3913,117 @@ class MPToken_test : public beast::unit_test::Suite // MPTCanClawback is not enabled mptAlice.claw(alice, bob, 1, tecNO_PERMISSION); - // Enable MPTCanClawback - mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetCanClawback}); + // Set MPTCanClawback + mptAlice.set({.account = alice, .flags = tfMPTSetCanClawback}); // Can clawback now mptAlice.claw(alice, bob, 1); } + void + testSetImmutableFlags(FeatureBitset features) + { + testcase("Set MPT ImmutableFlags via MPTokenIssuanceSet"); + + using namespace test::jtx; + Account const alice{"alice"}; + Account const bob{"bob"}; + + // ImmutableFlags requires featureDynamicMPT. + { + Env env(*this, features - featureDynamicMPT); + MPTTester mptAlice(env, alice); + mptAlice.create({.ownerCount = 1}); + + mptAlice.set( + {.account = alice, .immutableFlags = tifMPTCanClawback, .err = temDISABLED}); + } + + // ImmutableFlags containing tifMPTCanHoldConfidentialBalance requires + // featureConfidentialTransfer. + { + Env env(*this, features - featureConfidentialTransfer); + MPTTester mptAlice(env, alice); + mptAlice.create({.ownerCount = 1}); + + mptAlice.set( + {.account = alice, + .immutableFlags = tifMPTCanHoldConfidentialBalance, + .err = temDISABLED}); + } + + // ImmutableFlags of 0, or containing unknown bits, is rejected. + { + Env env(*this, features); + MPTTester mptAlice(env, alice); + mptAlice.create({.ownerCount = 1}); + + mptAlice.set({.account = alice, .immutableFlags = 0, .err = temINVALID_FLAG}); + mptAlice.set({.account = alice, .immutableFlags = 1, .err = temINVALID_FLAG}); + } + + // Holder is not allowed alongside ImmutableFlags, and ImmutableFlags + // can not be combined with Lock/Unlock in the same transaction. + { + Env env(*this, features); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create({.ownerCount = 1}); + + mptAlice.set( + {.account = alice, + .holder = bob, + .immutableFlags = tifMPTCanClawback, + .err = temMALFORMED}); + + mptAlice.set( + {.account = alice, + .flags = tfMPTLock, + .immutableFlags = tifMPTCanClawback, + .err = temMALFORMED}); + } + + // Can sets ImmutableFlags and the capability flags in the same transaction + { + Env env(*this, features); + MPTTester mptAlice(env, alice); + mptAlice.create({.ownerCount = 1}); + + mptAlice.set( + {.account = alice, + .flags = tfMPTSetCanClawback, + .immutableFlags = tifMPTCanClawback}); + + mptAlice.set( + {.account = alice, + .flags = tfMPTSetCanTransfer | tfMPTSetRequireAuth, + .immutableFlags = tifMPTCanTrade}); + } + + // Setting ImmutableFlags persists to the ledger, permanently blocks + // enabling the corresponding capability, and merges (rather than + // overwrites) across multiple transactions. + { + Env env(*this, features); + MPTTester mptAlice(env, alice); + mptAlice.create({.ownerCount = 1}); + + mptAlice.set({.account = alice, .immutableFlags = tifMPTCanClawback}); + BEAST_EXPECT(mptAlice.checkImmutableFlags(tifMPTCanClawback)); + + // The CanClawback can no longer be enabled. + mptAlice.set({.account = alice, .flags = tfMPTSetCanClawback, .err = tecNO_PERMISSION}); + + // A distinct bit merges with the first rather than overwriting it. + // Both CanClawback and CanTrade are now immutable. + mptAlice.set({.account = alice, .immutableFlags = tifMPTCanTrade}); + BEAST_EXPECT(mptAlice.checkImmutableFlags(tifMPTCanClawback | tifMPTCanTrade)); + + // Setting the same bit again is a harmless no-op. + mptAlice.set({.account = alice, .immutableFlags = tifMPTCanClawback}); + BEAST_EXPECT(mptAlice.checkImmutableFlags(tifMPTCanClawback | tifMPTCanTrade)); + } + } + void testMultiSendMaximumAmount(FeatureBitset features) { @@ -4398,14 +4414,14 @@ class MPToken_test : public beast::unit_test::Suite .holders = {alice, carol}, .pay = 100, .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanEnableCanTrade}); + .immutableFlags = tifMPTCanTrade}); MPTTester const eth( {.env = env, .issuer = gw, .holders = {alice, carol}, .pay = 100, .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanEnableCanTrade}); + .immutableFlags = tifMPTCanTrade}); // Can't create env(offer(gw, eth(10), btc(10)), Ter(tecNO_PERMISSION)); @@ -4641,30 +4657,25 @@ class MPToken_test : public beast::unit_test::Suite .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanTrade | - tmfMPTCanEnableCanTransfer}); + .flags = tfMPTCanLock | kMptDexFlags}); MPTTester eth( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanLock | kMptDexFlags, - .mutableFlags = tmfMPTCanEnableCanTransfer}); + .flags = tfMPTCanLock | kMptDexFlags}); MPTTester const usd( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanTransfer}); + .flags = kMptDexFlags | tfMPTCanLock}); MPTTester const cad( {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = kMptDexFlags | tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanTransfer}); + .flags = kMptDexFlags | tfMPTCanLock}); env(offer(bob, eth(1'000), btc(1'000)), Txflags(tfPassive)); env.close(); @@ -4694,7 +4705,7 @@ class MPToken_test : public beast::unit_test::Suite env(pay(gw, ed, eth(100))); env(pay(gw, ed, btc(100))); env.close(); - btc.set({.mutableFlags = tmfMPTSetRequireAuth}); + btc.set({.flags = tfMPTSetRequireAuth}); // authorize bob to enable the offers trading btc.authorize({.account = gw, .holder = bob}); env.close(); @@ -4932,8 +4943,7 @@ class MPToken_test : public beast::unit_test::Suite .issuer = gw, .holders = {alice, carol, bob}, .pay = 1'000, - .flags = tfMPTCanTransfer, - .mutableFlags = tmfMPTCanEnableCanTrade}); + .flags = tfMPTCanTransfer}); MPTTester const eth( {.env = env, .issuer = gw, @@ -4952,7 +4962,7 @@ class MPToken_test : public beast::unit_test::Suite env.close(); // Enable MPTCanTrade so BTC can be crossed through offers. - btc.set({.mutableFlags = tmfMPTSetCanTrade}); + btc.set({.flags = tfMPTSetCanTrade}); env(offer(bob, XRP(1), btc(1))); env(offer(bob, btc(1), eth(1))); env(offer(bob, eth(1), usd(1))); @@ -6753,11 +6763,7 @@ class MPToken_test : public beast::unit_test::Suite env.close(); MPTTester mpt( - {.env = env, - .issuer = gw, - .holders = {alice, carol}, - .flags = tfMPTCanTrade, - .mutableFlags = tmfMPTCanEnableCanTransfer}); + {.env = env, .issuer = gw, .holders = {alice, carol}, .flags = tfMPTCanTrade}); // src is issuer uint256 checkId{keylet::check(gw, env.seq(gw)).key}; @@ -6793,7 +6799,7 @@ class MPToken_test : public beast::unit_test::Suite env.close(); // can create now - mpt.set({.account = gw, .mutableFlags = tmfMPTSetCanTransfer}); + mpt.set({.account = gw, .flags = tfMPTSetCanTransfer}); checkId = keylet::check(alice, env.seq(alice)).key; env(check::create(alice, carol, mpt(100))); env.close(); @@ -7223,37 +7229,26 @@ class MPToken_test : public beast::unit_test::Suite auto const txfee = Fee(drops(increment)); auto const badMPT = MPT(gw, 1'000); - auto const makeMPT = [&](std::uint32_t const flags, - Holders holders = {}, - std::uint64_t const pay = 0, - std::optional const mutableFlags = - std::nullopt) { - return MPTTester( - {.env = env, - .issuer = gw, - .holders = holders, - .pay = pay ? std::optional{pay} : std::nullopt, - .flags = flags, - .mutableFlags = mutableFlags}); - }; + auto const makeMPT = + [&](std::uint32_t const flags, Holders holders = {}, std::uint64_t const pay = 0) { + return MPTTester( + {.env = env, + .issuer = gw, + .holders = holders, + .pay = pay ? std::optional{pay} : std::nullopt, + .flags = flags}); + }; auto const makeDexMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { - return makeMPT( - tfMPTCanLock | kMptDexFlags, - holders, - pay, - tmfMPTCanEnableRequireAuth | tmfMPTCanEnableCanTransfer | - tmfMPTCanEnableCanTrade); + return makeMPT(tfMPTCanLock | kMptDexFlags, holders, pay); }; auto const makeNoTransferMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { - return makeMPT( - tfMPTCanLock | tfMPTCanTrade, holders, pay, tmfMPTCanEnableCanTransfer); + return makeMPT(tfMPTCanLock | tfMPTCanTrade, holders, pay); }; auto const makeNoTradeMPT = [&](Holders holders = {}, std::uint64_t const pay = 0) { - return makeMPT( - tfMPTCanLock | tfMPTCanTransfer, holders, pay, tmfMPTCanEnableCanTrade); + return makeMPT(tfMPTCanLock | tfMPTCanTransfer, holders, pay); }; // AMMCreate @@ -7299,7 +7294,7 @@ class MPToken_test : public beast::unit_test::Suite // MPTRequireAuth is set // alice is not authorized usd.set({.flags = tfMPTUnlock}); - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + usd.set({.flags = tfMPTSetRequireAuth}); createFail(usd, alice, tecNO_AUTH); // issuer can create createDeleteAMM(usd, gw); @@ -7316,7 +7311,7 @@ class MPToken_test : public beast::unit_test::Suite createFail(usd2, alice, tecNO_AUTH); // issuer can create createDeleteAMM(usd2, gw); - usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + usd2.set({.flags = tfMPTSetCanTransfer}); // alice can create createDeleteAMM(usd2, alice); } @@ -7328,7 +7323,7 @@ class MPToken_test : public beast::unit_test::Suite // alice and issuer can't create createFail(usd3, alice, tecNO_PERMISSION); createFail(usd3, gw, tecNO_PERMISSION); - usd3.set({.mutableFlags = tmfMPTSetCanTrade}); + usd3.set({.flags = tfMPTSetCanTrade}); // alice can create createDeleteAMM(usd3, alice); } @@ -7383,7 +7378,7 @@ class MPToken_test : public beast::unit_test::Suite // MPTRequireAuth is set // carol is not authorized by the issuer - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + usd.set({.flags = tfMPTSetRequireAuth}); env.close(); amm.deposit( {.account = carol, @@ -7429,7 +7424,7 @@ class MPToken_test : public beast::unit_test::Suite .err = Ter(tecNO_AUTH)}); // issuer can deposit amm2.deposit({.account = gw, .tokens = 1'000}); - usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + usd2.set({.flags = tfMPTSetCanTransfer}); // carol can deposit amm2.deposit({.account = carol, .tokens = 1'000}); } @@ -7499,7 +7494,7 @@ class MPToken_test : public beast::unit_test::Suite usd.set({.flags = tfMPTUnlock}); // MPTRequireAuth is set - usd.set({.mutableFlags = tmfMPTSetRequireAuth}); + usd.set({.flags = tfMPTSetRequireAuth}); usd.authorize({.account = gw, .holder = carol, .flags = tfMPTUnauthorize}); // carol can't withdraw amm.withdraw( @@ -7543,7 +7538,7 @@ class MPToken_test : public beast::unit_test::Suite usd2.authorize({.account = bob, .flags = tfMPTUnauthorize}); // Can redeem env(pay(carol, gw, usd2(1))); - usd2.set({.mutableFlags = tmfMPTSetCanTransfer}); + usd2.set({.flags = tfMPTSetCanTransfer}); // carol can withdraw amm2.withdraw({.account = carol, .asset1Out = usd2(1), .asset2Out = eur(1)}); } @@ -7739,13 +7734,14 @@ public: // Dynamic MPT testInvalidCreateDynamic(all); testInvalidSetDynamic(all); - testMutateMPT(all); - testMutateCanLock(all); - testMutateRequireAuth(all); - testMutateCanEscrow(all); - testMutateCanTransfer(all); - testMutateCanTransfer(all - featureMPTokensV2); - testMutateCanClawback(all); + testSetMPT(all); + testSetCanLock(all); + testSetRequireAuth(all); + testSetCanEscrow(all); + testSetCanTransfer(all); + testSetCanTransfer(all - featureMPTokensV2); + testSetCanClawback(all); + testSetImmutableFlags(all); // Test offer crossing testOfferCrossing(all); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 617820c89c..62cf25f495 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -1602,8 +1602,7 @@ class Vault_test : public beast::unit_test::Suite mptt.create( {.flags = tfMPTCanTransfer | tfMPTCanLock | (args.enableClawback ? tfMPTCanClawback : kNone) | - (args.requireAuth ? tfMPTRequireAuth : kNone), - .mutableFlags = tmfMPTCanEnableCanTransfer}); + (args.requireAuth ? tfMPTRequireAuth : kNone)}); PrettyAsset const asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); @@ -2206,9 +2205,7 @@ class Vault_test : public beast::unit_test::Suite Vault const vault{env}; MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanTrade}); + mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); PrettyAsset const asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = alice}); @@ -2252,7 +2249,7 @@ class Vault_test : public beast::unit_test::Suite env.close(); // Enable CanTrade on the underlying. - mptt.set({.mutableFlags = tmfMPTSetCanTrade}); + mptt.set({.flags = tfMPTSetCanTrade}); env.close(); env(offer(alice, XRP(1), asset(10))); diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index dddfc88c7f..c6cd49fa26 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include @@ -38,7 +39,6 @@ #include #include -#include #include #include #include @@ -94,21 +94,6 @@ makePedersenParams(PedersenProofParams const& params) } // namespace -struct MPTSetFlagMapping -{ - std::uint32_t setFlag; - std::uint32_t ledgerFlag; -}; - -static constexpr std::array mptSetFlagMappings = {{ - {.setFlag = tmfMPTSetCanLock, .ledgerFlag = lsfMPTCanLock}, - {.setFlag = tmfMPTSetRequireAuth, .ledgerFlag = lsfMPTRequireAuth}, - {.setFlag = tmfMPTSetCanEscrow, .ledgerFlag = lsfMPTCanEscrow}, - {.setFlag = tmfMPTSetCanClawback, .ledgerFlag = lsfMPTCanClawback}, - {.setFlag = tmfMPTSetCanTrade, .ledgerFlag = lsfMPTCanTrade}, - {.setFlag = tmfMPTSetCanTransfer, .ledgerFlag = lsfMPTCanTransfer}, -}}; - void MptFlags::operator()(Env& env) const { @@ -195,7 +180,7 @@ makeMPTCreate(MPTInitDef const& arg) .transferFee = arg.transferFee, .pay = {{arg.holders, *arg.pay}}, .flags = arg.flags, - .mutableFlags = arg.mutableFlags, + .immutableFlags = arg.immutableFlags, .authHolder = arg.authHolder}; } return { @@ -203,7 +188,7 @@ makeMPTCreate(MPTInitDef const& arg) .transferFee = arg.transferFee, .authorize = arg.holders, .flags = arg.flags, - .mutableFlags = arg.mutableFlags, + .immutableFlags = arg.immutableFlags, .authHolder = arg.authHolder}; } @@ -245,8 +230,8 @@ MPTTester::createJV(MPTCreate const& arg) jv[sfMaximumAmount] = std::to_string(*arg.maxAmt); if (arg.domainID) jv[sfDomainID] = to_string(*arg.domainID); - if (arg.mutableFlags) - jv[sfMutableFlags] = *arg.mutableFlags; + if (arg.immutableFlags) + jv[sfImmutableFlags] = *arg.immutableFlags; jv[sfTransactionType] = jss::MPTokenIssuanceCreate; return jv; @@ -264,7 +249,7 @@ MPTTester::create(MPTCreate const& arg) .assetScale = arg.assetScale, .transferFee = arg.transferFee, .metadata = arg.metadata, - .mutableFlags = arg.mutableFlags, + .immutableFlags = arg.immutableFlags, .domainID = arg.domainID}); if (!isTesSuccess(submit(arg, jv))) { @@ -463,8 +448,8 @@ MPTTester::setJV(MPTSet const& arg) jv[sfDelegate] = arg.delegate->human(); if (arg.domainID) jv[sfDomainID] = to_string(*arg.domainID); - if (arg.mutableFlags) - jv[sfMutableFlags] = *arg.mutableFlags; + if (arg.immutableFlags) + jv[sfImmutableFlags] = *arg.immutableFlags; if (arg.transferFee) jv[sfTransferFee] = *arg.transferFee; if (arg.metadata) @@ -487,95 +472,85 @@ MPTTester::set(MPTSet const& arg) {.account = arg.account ? arg.account : issuer_, .holder = arg.holder, .id = arg.id ? arg.id : id_, - .mutableFlags = arg.mutableFlags, + .immutableFlags = arg.immutableFlags, .transferFee = arg.transferFee, .metadata = arg.metadata, .delegate = arg.delegate, .domainID = arg.domainID, .issuerPubKey = arg.issuerPubKey, .auditorPubKey = arg.auditorPubKey}); - if (submit(arg, jv) == tesSUCCESS && ((arg.flags.value_or(0) != 0u) || arg.mutableFlags)) + if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0) != 0u) { - if (((arg.flags.value_or(0) != 0u) || arg.mutableFlags)) - { - auto require = [&](std::optional const& holder, bool unchanged) { - auto flags = getFlags(holder); - if (!unchanged) + auto require = [&](std::optional const& holder, bool unchanged) { + auto flags = getFlags(holder); + if (!unchanged) + { + if (arg.flags) { - if (arg.flags) + if (*arg.flags & tfMPTLock) { - if (*arg.flags & tfMPTLock) - { - flags |= lsfMPTLocked; - } - else if (*arg.flags & tfMPTUnlock) - { - flags &= ~lsfMPTLocked; - } + flags |= lsfMPTLocked; + } + else if (*arg.flags & tfMPTUnlock) + { + flags &= ~lsfMPTLocked; } - if (arg.mutableFlags) + for (auto const& f : MPTokenIssuanceSet::flagMapping) { - for (auto const& [setFlag, ledgerFlag] : mptSetFlagMappings) + if ((*arg.flags & f.setFlag) != 0u) { - if ((*arg.mutableFlags & setFlag) != 0u) - { - flags |= ledgerFlag; - } + flags |= f.ledgerFlag; } - - if (*arg.mutableFlags & tmfMPTSetCanHoldConfidentialBalance) - flags |= tfMPTCanHoldConfidentialBalance; } } - env_.require(MptFlags(*this, flags, holder)); - }; - if (arg.account) - require(std::nullopt, arg.holder.has_value()); - if (auto const account = (arg.holder ? std::get_if(&(*arg.holder)) : nullptr)) - require(*account, false); - - if (arg.issuerPubKey) - { - env_.require(RequireAny([&]() -> bool { - return forObject([&](SLEP const& sle) -> bool { - if (sle) - { - auto const issuerPubKey = getPubKey(issuer_); - if (!issuerPubKey) - { - Throw( - "MPTTester::set: issuer's pubkey is not set"); - } - - return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey); - } - return false; - }); - })); } - if (arg.auditorPubKey) - { - env_.require(RequireAny([&]() -> bool { - return forObject([&](SLEP const& sle) -> bool { - if (sle) + env_.require(MptFlags(*this, flags, holder)); + }; + if (arg.account) + require(std::nullopt, arg.holder.has_value()); + if (auto const account = (arg.holder ? std::get_if(&(*arg.holder)) : nullptr)) + require(*account, false); + + if (arg.issuerPubKey) + { + env_.require(RequireAny([&]() -> bool { + return forObject([&](SLEP const& sle) -> bool { + if (sle) + { + auto const issuerPubKey = getPubKey(issuer_); + if (!issuerPubKey) { - if (!auditor_.has_value()) - Throw("MPTTester::set: auditor is not set"); - - auto const auditorPubKey = getPubKey(*auditor_); - if (!auditorPubKey) - { - Throw( - "MPTTester::set: auditor's pubkey is not set"); - } - - return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey); + Throw("MPTTester::set: issuer's pubkey is not set"); } - return false; - }); - })); - } + + return strHex((*sle)[sfIssuerEncryptionKey]) == strHex(*issuerPubKey); + } + return false; + }); + })); + } + if (arg.auditorPubKey) + { + env_.require(RequireAny([&]() -> bool { + return forObject([&](SLEP const& sle) -> bool { + if (sle) + { + if (!auditor_.has_value()) + Throw("MPTTester::set: auditor is not set"); + + auto const auditorPubKey = getPubKey(*auditor_); + if (!auditorPubKey) + { + Throw( + "MPTTester::set: auditor's pubkey is not set"); + } + + return strHex((*sle)[sfAuditorEncryptionKey]) == strHex(*auditorPubKey); + } + return false; + }); + })); } } } @@ -664,6 +639,15 @@ MPTTester::isTransferFeePresent() const return forObject([&](SLEP const& sle) -> bool { return sle->isFieldPresent(sfTransferFee); }); } +[[nodiscard]] bool +MPTTester::checkImmutableFlags(std::uint32_t expectedFlags) const +{ + // sfImmutableFlags is soeDEFAULT, defaulting to 0 if not present. + return forObject([&](SLEP const& sle) -> bool { + return sle->getFieldU32(sfImmutableFlags) == expectedFlags; + }); +} + void MPTTester::pay( Account const& src, diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index c6532ab14a..35ab7264bd 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -147,7 +147,7 @@ struct MPTCreate // if empty vector then pay to either authorize or all holders. std::optional, std::uint64_t>> pay = std::nullopt; std::optional flags = {0}; - std::optional mutableFlags = std::nullopt; + std::optional immutableFlags = std::nullopt; bool authHolder = false; std::optional domainID = std::nullopt; std::optional err = std::nullopt; @@ -183,7 +183,7 @@ struct MPTInitDef std::uint16_t transferFee = 0; std::optional pay = std::nullopt; std::uint32_t flags = kMptDexFlags; - std::optional mutableFlags = std::nullopt; + std::optional immutableFlags = std::nullopt; bool authHolder = false; bool fund = false; bool close = true; @@ -229,7 +229,7 @@ struct MPTSet std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; - std::optional mutableFlags = std::nullopt; + std::optional immutableFlags = std::nullopt; std::optional transferFee = std::nullopt; std::optional metadata = std::nullopt; std::optional delegate = std::nullopt; @@ -609,6 +609,9 @@ public: [[nodiscard]] bool isTransferFeePresent() const; + [[nodiscard]] bool + checkImmutableFlags(std::uint32_t expectedFlags) const; + [[nodiscard]] Account const& issuer() const { diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp index 8dc5960ee0..974d0e81d7 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp @@ -32,7 +32,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); auto const domainIDValue = canonical_UINT256(); - auto const mutableFlagsValue = canonical_UINT32(); + auto const immutableFlagsValue = canonical_UINT32(); auto const referenceHoldingValue = canonical_UINT256(); auto const issuerEncryptionKeyValue = canonical_VL(); auto const auditorEncryptionKeyValue = canonical_VL(); @@ -53,7 +53,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) builder.setLockedAmount(lockedAmountValue); builder.setMPTokenMetadata(mPTokenMetadataValue); builder.setDomainID(domainIDValue); - builder.setMutableFlags(mutableFlagsValue); + builder.setImmutableFlags(immutableFlagsValue); builder.setReferenceHolding(referenceHoldingValue); builder.setIssuerEncryptionKey(issuerEncryptionKeyValue); builder.setAuditorEncryptionKey(auditorEncryptionKeyValue); @@ -153,11 +153,11 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) } { - auto const& expected = mutableFlagsValue; - auto const actualOpt = entry.getMutableFlags(); + auto const& expected = immutableFlagsValue; + auto const actualOpt = entry.getImmutableFlags(); ASSERT_TRUE(actualOpt.has_value()); - expectEqualField(expected, *actualOpt, "sfMutableFlags"); - EXPECT_TRUE(entry.hasMutableFlags()); + expectEqualField(expected, *actualOpt, "sfImmutableFlags"); + EXPECT_TRUE(entry.hasImmutableFlags()); } { @@ -217,7 +217,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); auto const domainIDValue = canonical_UINT256(); - auto const mutableFlagsValue = canonical_UINT32(); + auto const immutableFlagsValue = canonical_UINT32(); auto const referenceHoldingValue = canonical_UINT256(); auto const issuerEncryptionKeyValue = canonical_VL(); auto const auditorEncryptionKeyValue = canonical_VL(); @@ -237,7 +237,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) sle->at(sfPreviousTxnID) = previousTxnIDValue; sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; sle->at(sfDomainID) = domainIDValue; - sle->at(sfMutableFlags) = mutableFlagsValue; + sle->at(sfImmutableFlags) = immutableFlagsValue; sle->at(sfReferenceHolding) = referenceHoldingValue; sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue; sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue; @@ -391,16 +391,16 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) } { - auto const& expected = mutableFlagsValue; + auto const& expected = immutableFlagsValue; - auto const fromSleOpt = entryFromSle.getMutableFlags(); - auto const fromBuilderOpt = entryFromBuilder.getMutableFlags(); + auto const fromSleOpt = entryFromSle.getImmutableFlags(); + auto const fromBuilderOpt = entryFromBuilder.getImmutableFlags(); ASSERT_TRUE(fromSleOpt.has_value()); ASSERT_TRUE(fromBuilderOpt.has_value()); - expectEqualField(expected, *fromSleOpt, "sfMutableFlags"); - expectEqualField(expected, *fromBuilderOpt, "sfMutableFlags"); + expectEqualField(expected, *fromSleOpt, "sfImmutableFlags"); + expectEqualField(expected, *fromBuilderOpt, "sfImmutableFlags"); } { @@ -531,8 +531,8 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getMPTokenMetadata().has_value()); EXPECT_FALSE(entry.hasDomainID()); EXPECT_FALSE(entry.getDomainID().has_value()); - EXPECT_FALSE(entry.hasMutableFlags()); - EXPECT_FALSE(entry.getMutableFlags().has_value()); + EXPECT_FALSE(entry.hasImmutableFlags()); + EXPECT_FALSE(entry.getImmutableFlags().has_value()); EXPECT_FALSE(entry.hasReferenceHolding()); EXPECT_FALSE(entry.getReferenceHolding().has_value()); EXPECT_FALSE(entry.hasIssuerEncryptionKey()); diff --git a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceCreateTests.cpp index f7151fc749..8228188950 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceCreateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceCreateTests.cpp @@ -34,7 +34,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderSettersRoundTrip) auto const maximumAmountValue = canonical_UINT64(); auto const mPTokenMetadataValue = canonical_VL(); auto const domainIDValue = canonical_UINT256(); - auto const mutableFlagsValue = canonical_UINT32(); + auto const immutableFlagsValue = canonical_UINT32(); MPTokenIssuanceCreateBuilder builder{ accountValue, @@ -48,7 +48,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderSettersRoundTrip) builder.setMaximumAmount(maximumAmountValue); builder.setMPTokenMetadata(mPTokenMetadataValue); builder.setDomainID(domainIDValue); - builder.setMutableFlags(mutableFlagsValue); + builder.setImmutableFlags(immutableFlagsValue); auto tx = builder.build(publicKey, secretKey); @@ -107,11 +107,11 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderSettersRoundTrip) } { - auto const& expected = mutableFlagsValue; - auto const actualOpt = tx.getMutableFlags(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present"; - expectEqualField(expected, *actualOpt, "sfMutableFlags"); - EXPECT_TRUE(tx.hasMutableFlags()); + auto const& expected = immutableFlagsValue; + auto const actualOpt = tx.getImmutableFlags(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present"; + expectEqualField(expected, *actualOpt, "sfImmutableFlags"); + EXPECT_TRUE(tx.hasImmutableFlags()); } } @@ -135,7 +135,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderFromStTxRoundTrip) auto const maximumAmountValue = canonical_UINT64(); auto const mPTokenMetadataValue = canonical_VL(); auto const domainIDValue = canonical_UINT256(); - auto const mutableFlagsValue = canonical_UINT32(); + auto const immutableFlagsValue = canonical_UINT32(); // Build an initial transaction MPTokenIssuanceCreateBuilder initialBuilder{ @@ -149,7 +149,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderFromStTxRoundTrip) initialBuilder.setMaximumAmount(maximumAmountValue); initialBuilder.setMPTokenMetadata(mPTokenMetadataValue); initialBuilder.setDomainID(domainIDValue); - initialBuilder.setMutableFlags(mutableFlagsValue); + initialBuilder.setImmutableFlags(immutableFlagsValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -204,10 +204,10 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderFromStTxRoundTrip) } { - auto const& expected = mutableFlagsValue; - auto const actualOpt = rebuiltTx.getMutableFlags(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present"; - expectEqualField(expected, *actualOpt, "sfMutableFlags"); + auto const& expected = immutableFlagsValue; + auto const actualOpt = rebuiltTx.getImmutableFlags(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present"; + expectEqualField(expected, *actualOpt, "sfImmutableFlags"); } } @@ -275,8 +275,8 @@ TEST(TransactionsMPTokenIssuanceCreateTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getMPTokenMetadata().has_value()); EXPECT_FALSE(tx.hasDomainID()); EXPECT_FALSE(tx.getDomainID().has_value()); - EXPECT_FALSE(tx.hasMutableFlags()); - EXPECT_FALSE(tx.getMutableFlags().has_value()); + EXPECT_FALSE(tx.hasImmutableFlags()); + EXPECT_FALSE(tx.getImmutableFlags().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp index e7b34590b2..af696ce47b 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/MPTokenIssuanceSetTests.cpp @@ -34,7 +34,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip) auto const domainIDValue = canonical_UINT256(); auto const mPTokenMetadataValue = canonical_VL(); auto const transferFeeValue = canonical_UINT16(); - auto const mutableFlagsValue = canonical_UINT32(); + auto const immutableFlagsValue = canonical_UINT32(); auto const issuerEncryptionKeyValue = canonical_VL(); auto const auditorEncryptionKeyValue = canonical_VL(); @@ -50,7 +50,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip) builder.setDomainID(domainIDValue); builder.setMPTokenMetadata(mPTokenMetadataValue); builder.setTransferFee(transferFeeValue); - builder.setMutableFlags(mutableFlagsValue); + builder.setImmutableFlags(immutableFlagsValue); builder.setIssuerEncryptionKey(issuerEncryptionKeyValue); builder.setAuditorEncryptionKey(auditorEncryptionKeyValue); @@ -109,11 +109,11 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip) } { - auto const& expected = mutableFlagsValue; - auto const actualOpt = tx.getMutableFlags(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present"; - expectEqualField(expected, *actualOpt, "sfMutableFlags"); - EXPECT_TRUE(tx.hasMutableFlags()); + auto const& expected = immutableFlagsValue; + auto const actualOpt = tx.getImmutableFlags(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present"; + expectEqualField(expected, *actualOpt, "sfImmutableFlags"); + EXPECT_TRUE(tx.hasImmutableFlags()); } { @@ -153,7 +153,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip) auto const domainIDValue = canonical_UINT256(); auto const mPTokenMetadataValue = canonical_VL(); auto const transferFeeValue = canonical_UINT16(); - auto const mutableFlagsValue = canonical_UINT32(); + auto const immutableFlagsValue = canonical_UINT32(); auto const issuerEncryptionKeyValue = canonical_VL(); auto const auditorEncryptionKeyValue = canonical_VL(); @@ -169,7 +169,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip) initialBuilder.setDomainID(domainIDValue); initialBuilder.setMPTokenMetadata(mPTokenMetadataValue); initialBuilder.setTransferFee(transferFeeValue); - initialBuilder.setMutableFlags(mutableFlagsValue); + initialBuilder.setImmutableFlags(immutableFlagsValue); initialBuilder.setIssuerEncryptionKey(issuerEncryptionKeyValue); initialBuilder.setAuditorEncryptionKey(auditorEncryptionKeyValue); @@ -225,10 +225,10 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip) } { - auto const& expected = mutableFlagsValue; - auto const actualOpt = rebuiltTx.getMutableFlags(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present"; - expectEqualField(expected, *actualOpt, "sfMutableFlags"); + auto const& expected = immutableFlagsValue; + auto const actualOpt = rebuiltTx.getImmutableFlags(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present"; + expectEqualField(expected, *actualOpt, "sfImmutableFlags"); } { @@ -310,8 +310,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getMPTokenMetadata().has_value()); EXPECT_FALSE(tx.hasTransferFee()); EXPECT_FALSE(tx.getTransferFee().has_value()); - EXPECT_FALSE(tx.hasMutableFlags()); - EXPECT_FALSE(tx.getMutableFlags().has_value()); + EXPECT_FALSE(tx.hasImmutableFlags()); + EXPECT_FALSE(tx.getImmutableFlags().has_value()); EXPECT_FALSE(tx.hasIssuerEncryptionKey()); EXPECT_FALSE(tx.getIssuerEncryptionKey().has_value()); EXPECT_FALSE(tx.hasAuditorEncryptionKey()); From 68a765d92946dc718d6cd29a3bbc600e2cbb4844 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:07:57 +0100 Subject: [PATCH 18/68] fix: Bound untrusted manifest cache --- include/xrpl/server/Manifest.h | 113 +++++++++++++++----- src/libxrpl/server/Manifest.cpp | 105 ++++++++++++++---- src/libxrpl/server/Wallet.cpp | 21 +++- src/test/app/Manifest_test.cpp | 72 +++++++++---- src/test/app/ValidatorList_test.cpp | 59 ++++++---- src/xrpld/app/misc/detail/ValidatorList.cpp | 10 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 30 ++++-- 7 files changed, 313 insertions(+), 97 deletions(-) diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 362080ef36..19dfbfd54f 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -45,12 +45,15 @@ namespace xrpl { dynamically generates the signatureless form when it needs to verify the signature. - An instance of ManifestCache stores, for each trusted validator, (a) its + An instance of ManifestCache stores, for each known validator, (a) its master public key, and (b) the most senior of all valid manifests it has seen for that validator, if any. On startup, the [validator_token] config entry (which contains the manifest for this validator) is decoded and added to the manifest cache. Other manifests are added as "gossip" - received from xrpld peers. + received from xrpld peers, including ones for validators this node does not + list. Manifests for unlisted validators are capped (kMaxUntrustedCount) + so peer gossip cannot grow the cache without bound; listed validators are + not capped. Entries are never evicted, so a stored revocation is permanent. When an ephemeral key is compromised, a new signing key pair is created, along with a new manifest vouching for it (with a higher sequence number), @@ -258,30 +261,17 @@ loadValidatorToken( beast::Journal journal = beast::Journal(beast::Journal::getNullSink())); enum class ManifestDisposition { - /** - * Manifest is valid - */ - Accepted = 0, + Accepted = 0, ///< Manifest is valid - /** - * Sequence is too old - */ - Stale, + Stale, ///< Sequence is too old - /** - * The master key is not acceptable to us - */ - BadMasterKey, + BadMasterKey, ///< The master key is not acceptable to us - /** - * The ephemeral key is not acceptable to us - */ - BadEphemeralKey, + BadEphemeralKey, ///< The ephemeral key is not acceptable to us - /** - * Timely, but invalid signature - */ - Invalid + Invalid, ///< Timely, but invalid signature + + UntrustedCapacity ///< Unlisted and limit reached }; inline std::string @@ -299,11 +289,25 @@ to_string(ManifestDisposition m) return "badEphemeralKey"; case ManifestDisposition::Invalid: return "invalid"; + case ManifestDisposition::UntrustedCapacity: + return "untrustedCapacity"; default: return "unknown"; } } +/** + * Whether a manifest counts against the 'untrusted' cache cap. + * + * Passed to `ManifestCache::applyManifest` with no default, so every caller + * must choose. `Capped` is the safe, flood-resistant value; only listed or + * configured keys should use `Uncapped`. + */ +enum class ManifestRateLimitCap : std::uint8_t { + Capped, ///< Subject to the untrusted cap (unlisted peer gossip) + Uncapped ///< Bypasses the cap (listed/trusted or config manifests) +}; + class DatabaseCon; /** @@ -327,6 +331,38 @@ private: std::atomic seq_{0}; + /** + * Master keys of cached manifests for validators this node does not list. + * + * One entry per capped key in `map_`; its size enforces the cap below. + * A key is added when first cached under `Capped` and removed when it + * becomes listed (see `promoteToTrusted`) or an `Uncapped` update arrives, + * never re-added on de-listing. Uncapped keys are not tracked here. + */ + hash_set untrustedKeys_; + + /** + * Maximum number of untrusted master keys kept in the cache. + * + * Once reached, a manifest for a brand-new unlisted key is rejected. + */ + static constexpr std::size_t kMaxUntrustedCount = 50000; + + /** + * Running count of manifests rejected because the untrusted cap was full. + * + * Drives throttled logging (see `kUntrustedRejectCount`). Atomic because + * `applyManifest` may run concurrently. + */ + std::atomic untrustedRejectCount_{0}; + + /** + * Number of cap rejections between summary warnings. + * + * @see untrustedRejectCount_ + */ + static constexpr std::uint64_t kUntrustedRejectCount = 10000; + public: explicit ManifestCache(beast::Journal j = beast::Journal(beast::Journal::getNullSink())) : j_(j) { @@ -411,17 +447,44 @@ public: /** * Add manifest to cache. * + * A brand-new unlisted key is rejected once the untrusted cap is full; + * updates to a cached key and `Uncapped` manifests bypass the cap. The + * caller decides `cap` before calling so the cache lock is not held while + * consulting the validator list, which would risk a lock-ordering deadlock. + * * @param m Manifest to add * - * @return `ManifestDisposition::accepted` if successful, or - * `stale` or `invalid` otherwise + * @param cap `Uncapped` skips the untrusted cap; use it for keys that are + * listed, configured, or loaded from the DB. Note `Uncapped` does not + * assert the key is currently trusted (a DB entry may predate a + * de-listing). Callers must state this explicitly so a manifest is + * never left uncapped by omission. + * + * @return `Accepted` if stored, `Stale` if superseded, `Invalid`/ + * `BadEphemeralKey` if malformed, or `UntrustedCapacity` if the + * untrusted cap is full. * * @par Thread Safety * * May be called concurrently */ ManifestDisposition - applyManifest(Manifest m); + applyManifest(Manifest m, ManifestRateLimitCap cap); + + /** + * Stop counting a master key against the untrusted cap. + * + * Called when a cached untrusted key becomes listed, freeing its slot. + * Idempotent and a no-op for keys that were never counted. + * + * @param pk Master public key that is now listed/trusted + * + * @par Thread Safety + * + * May be called concurrently + */ + void + promoteToTrusted(PublicKey const& pk); /** * Populate manifest cache with manifests in database and config. diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index 798d8130b7..b34955dc28 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -382,16 +382,20 @@ ManifestCache::revoked(PublicKey const& pk) const } ManifestDisposition -ManifestCache::applyManifest(Manifest m) +ManifestCache::applyManifest(Manifest m, ManifestRateLimitCap const cap) { + bool const uncapped = cap == ManifestRateLimitCap::Uncapped; + + // The signature is checked only on the first `prewriteCheck` run (under the + // read lock). It is expensive, so `checkSignature` is cleared the first + // time it is read; the second run (under the write lock) skips it. + bool checkSignature = true; + // Check the manifest against the conditions that do not require a - // `unique_lock` (write lock) on the `mutex_`. Since the signature can be - // relatively expensive, the `checkSignature` parameter determines if the - // signature should be checked. Since `prewriteCheck` is run twice (see - // comment below), `checkSignature` only needs to be set to true on the - // first run. - auto prewriteCheck = [this, &m](auto const& iter, bool checkSignature, auto const& lock) - -> std::optional { + // `unique_lock` (write lock) on the `mutex_`. + auto prewriteCheck = [this, &m, &checkSignature]( + auto const& iter, + auto const& lock) -> std::optional { XRPL_ASSERT(lock.owns_lock(), "xrpl::ManifestCache::applyManifest::prewriteCheck : locked"); (void)lock; // not used. parameter is present to ensure the mutex is // locked when the lambda is called. @@ -406,11 +410,15 @@ ManifestCache::applyManifest(Manifest m) return ManifestDisposition::Stale; } - if (checkSignature && !m.verify()) + if (checkSignature) { - if (auto stream = j_.warn()) - logMftAct(stream, "Invalid", m.masterKey, m.sequence); - return ManifestDisposition::Invalid; + checkSignature = false; + if (!m.verify()) + { + if (auto stream = j_.warn()) + logMftAct(stream, "Invalid", m.masterKey, m.sequence); + return ManifestDisposition::Invalid; + } } // If the master key associated with a manifest is or might be @@ -470,14 +478,51 @@ ManifestCache::applyManifest(Manifest m) return std::nullopt; }; + // Reject a brand-new manifest for an unlisted key once the untrusted cap + // is full. Updates to a cached key and uncapped manifests always pass. + // Called under both the read and write lock, since the cap can be reached + // between the two. The lock param enforces that. + auto atUntrustedCap = [this, &m, uncapped](auto const& iter, auto const& lock) { + XRPL_ASSERT( + lock.owns_lock(), "xrpl::ManifestCache::applyManifest::atUntrustedCap : locked"); + (void)lock; // not used. parameter is present to ensure the mutex is + // locked when the lambda is called. + if (iter == map_.end() && !uncapped && untrustedKeys_.size() >= kMaxUntrustedCount) + { + // Log each rejection at debug, but warn only once per interval so a + // flood does not fill the log. + if (auto stream = j_.debug()) + logMftAct(stream, "UntrustedCapacity", m.masterKey, m.sequence); + if (auto const n = untrustedRejectCount_.fetch_add(1) + 1; + n % kUntrustedRejectCount == 0) + { + JLOG(j_.warn()) << "Untrusted manifest cap reached; " << n + << " manifests rejected so far"; + } + return true; + } + return false; + }; + { std::shared_lock const sl{mutex_}; - if (auto d = prewriteCheck(map_.find(m.masterKey), /*checkSig*/ true, sl)) + auto const iter = map_.find(m.masterKey); + + if (atUntrustedCap(iter, sl)) + return ManifestDisposition::UntrustedCapacity; + + if (auto d = prewriteCheck(iter, sl); d.has_value()) return *d; } std::unique_lock const sl{mutex_}; auto const iter = map_.find(m.masterKey); + + // Re-check the cap under the write lock: the cache may have grown while the + // read lock above was released. + if (atUntrustedCap(iter, sl)) + return ManifestDisposition::UntrustedCapacity; + // Since we released the previously held read lock, it's possible that the // collections have been written to. This means we need to run // `prewriteCheck` again. This re-does work, but `prewriteCheck` is @@ -487,7 +532,7 @@ ManifestCache::applyManifest(Manifest m) // doesn't need to happen again (signature checks are somewhat expensive). // Note: It's a mistake to use an upgradable lock. This is a recipe for // deadlock. - if (auto d = prewriteCheck(iter, /*checkSig*/ false, sl)) + if (auto d = prewriteCheck(iter, sl); d.has_value()) return *d; bool const revoked = m.revoked(); @@ -506,6 +551,12 @@ ManifestCache::applyManifest(Manifest m) } auto masterKey = m.masterKey; + + // Count this key against the untrusted cap. Uncapped keys (listed, + // configured, or DB-loaded) are not tracked. + if (!uncapped) + untrustedKeys_.insert(masterKey); + map_.emplace(std::move(masterKey), std::move(m)); // Something has changed. Keep track of it. @@ -519,6 +570,11 @@ ManifestCache::applyManifest(Manifest m) if (auto stream = j_.info()) logMftAct(stream, "AcceptedUpdate", m.masterKey, m.sequence, iter->second.sequence); + // If this key was counted against the cap but now arrives uncapped, free + // its slot without waiting for promoteToTrusted. + if (uncapped) + untrustedKeys_.erase(m.masterKey); + signingToMasterKeys_.erase( *iter->second.signingKey); // NOLINT(bugprone-unchecked-optional-access) prewriteCheck // ensures old manifest is not revoked @@ -526,8 +582,8 @@ ManifestCache::applyManifest(Manifest m) if (!revoked) { signingToMasterKeys_.emplace( - *m.signingKey, m.masterKey); // NOLINT(bugprone-unchecked-optional-access) non-revoked - // manifest always has signingKey + *m.signingKey, m.masterKey); // NOLINT(bugprone-unchecked-optional-access) + // non-revoked manifest always has signingKey } iter->second = std::move(m); @@ -538,6 +594,16 @@ ManifestCache::applyManifest(Manifest m) return ManifestDisposition::Accepted; } +void +ManifestCache::promoteToTrusted(PublicKey const& pk) +{ + // Frees the key's untrusted slot; a no-op (and idempotent) if the key was + // never counted. Not re-added on de-listing, so list/de-list cannot grow + // the count. + std::unique_lock const sl{mutex_}; + untrustedKeys_.erase(pk); +} + void ManifestCache::load(DatabaseCon& dbCon, std::string const& dbTable) { @@ -568,7 +634,8 @@ ManifestCache::load( JLOG(j_.warn()) << "Configured manifest revokes public key"; } - if (applyManifest(std::move(*mo)) == ManifestDisposition::Invalid) + if (applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + ManifestDisposition::Invalid) { JLOG(j_.error()) << "Manifest in config was rejected"; return false; @@ -590,7 +657,9 @@ ManifestCache::load( auto mo = deserializeManifest(base64Decode(revocationStr)); - if (!mo || !mo->revoked() || applyManifest(std::move(*mo)) == ManifestDisposition::Invalid) + if (!mo || !mo->revoked() || + applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + ManifestDisposition::Invalid) { JLOG(j_.error()) << "Invalid validator key revocation in config"; return false; diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp index f3a7ff76ba..e6af5b6411 100644 --- a/src/libxrpl/server/Wallet.cpp +++ b/src/libxrpl/server/Wallet.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -77,7 +78,9 @@ getManifests( continue; } - cache.applyManifest(std::move(*mo)); + // Only trusted manifests are persisted (see saveManifests), so + // anything loaded from the DB bypasses the untrusted cap. + cache.applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped); } else { @@ -107,19 +110,27 @@ saveManifests( { soci::transaction tr(session); session << "DELETE FROM " << dbTable; + // Count skipped untrusted manifests and log one summary afterwards, since + // the cache can hold many and per-entry logging would flood at shutdown. + std::size_t skipped = 0; for (auto const& v : map) { - // Save all revocation manifests, - // but only save trusted non-revocation manifests. - if (!v.second.revoked() && !isTrusted(v.second.masterKey)) + // Persist only trusted keys. Untrusted gossip is left out so a flood + // cannot survive a restart on disk. + if (!isTrusted(v.second.masterKey)) { - JLOG(j.info()) << "Untrusted manifest in cache not saved to db"; + ++skipped; continue; } saveManifest(session, dbTable, v.second.serialized); } tr.commit(); + + if (skipped != 0) + { + JLOG(j.info()) << skipped << " untrusted manifest(s) in cache not saved to db"; + } } void diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index 50e8ab4a8d..ae30414b92 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -399,7 +399,8 @@ public: BEAST_EXPECT( ManifestDisposition::Accepted == cache.applyManifest( - makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0))); + makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0), + ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first); BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk); @@ -411,7 +412,8 @@ public: BEAST_EXPECT( ManifestDisposition::Accepted == cache.applyManifest( - makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1))); + makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1), + ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -421,7 +423,8 @@ public: BEAST_EXPECT( ManifestDisposition::BadEphemeralKey == cache.applyManifest( - makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2))); + makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2), + ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -431,7 +434,8 @@ public: // key from a revoked master public key BEAST_EXPECT( ManifestDisposition::Accepted == - cache.applyManifest(makeRevocation(sk, KeyType::Ed25519))); + cache.applyManifest( + makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.revoked(pk)); BEAST_EXPECT(cache.getSigningKey(pk) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -902,39 +906,69 @@ public: // applyManifest should accept new manifests with // higher sequence numbers auto const seq0 = cache.sequence(); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); BEAST_EXPECT(cache.sequence() > seq0); auto const seq1 = cache.sequence(); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); BEAST_EXPECT(cache.sequence() == seq1); - BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Accepted); - BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA2)) == ManifestDisposition::BadEphemeralKey); + BEAST_EXPECT( + cache.applyManifest(clone(sA2), ManifestRateLimitCap::Capped) == + ManifestDisposition::BadEphemeralKey); // applyManifest should accept manifests with max sequence numbers // that revoke the master public key BEAST_EXPECT(!cache.revoked(pkA)); BEAST_EXPECT(sAMax.revoked()); - BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Accepted); - BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); BEAST_EXPECT(cache.revoked(pkA)); // applyManifest should reject manifests with invalid signatures - BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Accepted); - BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); BEAST_EXPECT(!deserializeManifest(fake)); - BEAST_EXPECT(cache.applyManifest(clone(sB1)) == ManifestDisposition::Invalid); - BEAST_EXPECT(cache.applyManifest(clone(sB2)) == ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sB1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Invalid); + BEAST_EXPECT( + cache.applyManifest(clone(sB2), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); auto const sC0 = makeManifest( kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47); - BEAST_EXPECT(cache.applyManifest(clone(sC0)) == ManifestDisposition::BadMasterKey); + BEAST_EXPECT( + cache.applyManifest(clone(sC0), ManifestRateLimitCap::Capped) == + ManifestDisposition::BadMasterKey); } testLoadStore(cache); diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 60228f6723..0340b24680 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -278,8 +278,10 @@ private: trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers)); BEAST_EXPECT(trustedKeys->listed(localSigningPublicOuter)); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifests.applyManifest(*deserializeManifest(cfgManifest)); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + manifests.applyManifest( + *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT( trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers)); @@ -369,8 +371,10 @@ private: app.config().legacy(Sections::kDatabasePath), env.journal); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifests.applyManifest(*deserializeManifest(cfgManifest)); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + manifests.applyManifest( + *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT(trustedKeys->load(localSigningPublicOuter, cfgKeys, emptyCfgPublishers)); @@ -455,13 +459,16 @@ private: auto const pubRevokedSigning = randomKeyPair(KeyType::Secp256k1); // make this manifest revoked (seq num = max) // -- thus should not be loaded - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - pubManifests.applyManifest(*deserializeManifest(makeManifestString( - pubRevokedPublic, - pubRevokedSecret, - pubRevokedSigning.first, - pubRevokedSigning.second, - std::numeric_limits::max()))); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + pubManifests.applyManifest( + *deserializeManifest(makeManifestString( + pubRevokedPublic, + pubRevokedSecret, + pubRevokedSigning.first, + pubRevokedSigning.second, + std::numeric_limits::max())), + ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) // these two are not revoked (and not in the manifest cache at all.) auto legitKey1 = randomMasterKey(); @@ -494,13 +501,16 @@ private: auto const pubRevokedSigning = randomKeyPair(KeyType::Secp256k1); // make this manifest revoked (seq num = max) // -- thus should not be loaded - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - pubManifests.applyManifest(*deserializeManifest(makeManifestString( - pubRevokedPublic, - pubRevokedSecret, - pubRevokedSigning.first, - pubRevokedSigning.second, - std::numeric_limits::max()))); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + pubManifests.applyManifest( + *deserializeManifest(makeManifestString( + pubRevokedPublic, + pubRevokedSecret, + pubRevokedSigning.first, + pubRevokedSigning.second, + std::numeric_limits::max())), + ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) // this one is not revoked (and not in the manifest cache at all.) auto legitKey = randomMasterKey(); @@ -1218,7 +1228,8 @@ private: BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m1)) == ManifestDisposition::Accepted); + manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); BEAST_EXPECT(trustedKeysOuter->listed(signingPublic1)); @@ -1232,7 +1243,8 @@ private: masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2)); BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m2)) == ManifestDisposition::Accepted); + manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); BEAST_EXPECT(trustedKeysOuter->listed(signingPublic2)); @@ -1249,7 +1261,8 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) BEAST_EXPECT(max->revoked()); BEAST_EXPECT( - manifestsOuter.applyManifest(std::move(*max)) == ManifestDisposition::Accepted); + manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT(manifestsOuter.getSigningKey(masterPublic) == masterPublic); @@ -2668,7 +2681,9 @@ private: auto threshold = listThreshold > 0 ? std::optional(listThreshold) : std::nullopt; if (self) { - valManifests.applyManifest(*deserializeManifest(base64Decode(self->manifest))); + valManifests.applyManifest( + *deserializeManifest(base64Decode(self->manifest)), + ManifestRateLimitCap::Capped); BEAST_EXPECT( result->load(self->signingPublic, emptyCfgKeys, cfgPublishers, threshold)); } diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index b77dcc23c2..1b4d2bab80 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1065,6 +1065,8 @@ ValidatorList::updatePublisherList( { // Increment list count for added keys ++keyListings_[*iNew]; + // Key is now listed: free its untrusted slot if it had one. + validatorManifests_.promoteToTrusted(*iNew); ++iNew; } else if (iNew == publisherList.end() || (iOld != oldList.end() && *iOld < *iNew)) @@ -1103,7 +1105,8 @@ ValidatorList::updatePublisherList( continue; } - if (auto const r = validatorManifests_.applyManifest(std::move(*m)); + if (auto const r = + validatorManifests_.applyManifest(std::move(*m), ManifestRateLimitCap::Uncapped); r == ManifestDisposition::Invalid) { JLOG(j_.warn()) << "List for " << strHex(pubKey) @@ -1357,7 +1360,10 @@ ValidatorList::verify( PublicKey masterPubKey = manifest.masterKey; auto const revoked = manifest.revoked(); - auto const result = publisherManifests_.applyManifest(std::move(manifest)); + // Publisher keys are configured/trusted (checked above), so bypass the + // untrusted cap. + auto const result = + publisherManifests_.applyManifest(std::move(manifest), ManifestRateLimitCap::Uncapped); if (revoked && result == ManifestDisposition::Accepted) { diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 6a6a6edace..e59cff85d8 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -675,13 +675,22 @@ OverlayImpl::onManifests( if (auto mo = deserializeManifest(s)) { auto const serialized = mo->serialized; + // Resolve trust before applyManifest takes the manifest-cache + // lock: listed() takes the validator-list lock, so ordering it + // first avoids holding the two locks in opposite orders. + bool const isTrusted = app_.getValidators().listed(mo->masterKey); + // Updates to a known key are relayed even when untrusted. Use + // getSequence, not getManifest, to avoid copying the cached payload + // on this hot path. + bool const isKnown = + app_.getValidatorManifests().getSequence(mo->masterKey).has_value(); - auto const result = app_.getValidatorManifests().applyManifest(std::move(*mo)); + auto const result = app_.getValidatorManifests().applyManifest( + std::move(*mo), + isTrusted ? ManifestRateLimitCap::Uncapped : ManifestRateLimitCap::Capped); if (result == ManifestDisposition::Accepted) { - relay.add_list()->set_stobject(s); - // N.B.: this is important; the applyManifest call above moves // the loaded Manifest out of the optional so we need to // reload it here. @@ -693,10 +702,19 @@ OverlayImpl::onManifests( // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above app_.getOPs().pubManifest(*mo); - if (app_.getValidators().listed(mo->masterKey)) + // Relay only trusted manifests or updates to known keys, so + // untrusted gossip for a brand-new key cannot be amplified. + // Persist to the wallet DB only for trusted keys, so untrusted + // gossip never survives a restart. + if (isTrusted || isKnown) { - auto db = app_.getWalletDB().checkoutDb(); - addValidatorManifest(*db, serialized); + relay.add_list()->set_stobject(s); + + if (isTrusted) + { + auto db = app_.getWalletDB().checkoutDb(); + addValidatorManifest(*db, serialized); + } } // NOLINTEND(bugprone-unchecked-optional-access) } From 6b3eaf091b64d653115186384c575453e2cf18aa Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:06:17 -0400 Subject: [PATCH 19/68] fix: Change ConfidentialMPTConvert to no delegate --- .../xrpl/protocol/detail/transactions.macro | 2 +- .../transactions/ConfidentialMPTConvert.h | 2 +- .../app/ConfidentialTransferExtended_test.cpp | 198 +++++++++--------- src/test/app/Delegate_test.cpp | 2 +- 4 files changed, 105 insertions(+), 99 deletions(-) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dc03cf7c59..6d809f7ab5 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1085,7 +1085,7 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, - Delegation::Delegable, + Delegation::NotDelegable, featureConfidentialTransfer, NoPriv, ({ diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h index dec7f733c9..284b7f9e70 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h @@ -19,7 +19,7 @@ class ConfidentialMPTConvertBuilder; * @brief Transaction: ConfidentialMPTConvert * * Type: ttCONFIDENTIAL_MPT_CONVERT (85) - * Delegable: Delegation::Delegable + * Delegable: Delegation::NotDelegable * Amendment: featureConfidentialTransfer * Privileges: NoPriv * diff --git a/src/test/app/ConfidentialTransferExtended_test.cpp b/src/test/app/ConfidentialTransferExtended_test.cpp index 953325a6e9..fe5e0b3064 100644 --- a/src/test/app/ConfidentialTransferExtended_test.cpp +++ b/src/test/app/ConfidentialTransferExtended_test.cpp @@ -1653,30 +1653,35 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase mptAlice.generateKeyPair(carol); mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); - // Bob delegates Convert, MergeInbox to dave. - env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"})); + // ConfidentialMPTConvert is not delegable: attempting to grant it as a + // delegated permission is rejected at preflight of DelegateSet. + env(delegate::set(bob, dave, {"ConfidentialMPTConvert"}), Ter(temMALFORMED)); env.close(); - // Carol has no permission from bob to convert on his behalf. + // Bob delegates MergeInbox to dave. + env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox"})); + env.close(); + + // A Convert carrying a Delegate is rejected at preflight because the + // transaction type is not delegable at all. mptAlice.convert({ .account = bob, .amt = 10, .holderPubKey = mptAlice.getPubKey(bob), - .delegate = carol, - .err = terNO_DELEGATE_PERMISSION, + .delegate = dave, + .err = temINVALID, }); - // Dave executes Convert on behalf of bob, registering bob's key. + // Bob converts, registering bob's key. mptAlice.convert({ .account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob), - .delegate = dave, }); env.require(MptBalance(mptAlice, bob, 100)); - // Dave executes Convert again on behalf of bob (no key registration). - mptAlice.convert({.account = bob, .amt = 50, .delegate = dave}); + // Bob converts again (no key registration). + mptAlice.convert({.account = bob, .amt = 50}); // Dave executes MergeInbox on behalf of bob. mptAlice.mergeInbox({.account = bob, .delegate = dave}); @@ -1698,10 +1703,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase .err = terNO_DELEGATE_PERMISSION}); // Bob delegates ConfidentialMPTSend to dave. - env(delegate::set( - bob, - dave, - {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox", "ConfidentialMPTSend"})); + env(delegate::set(bob, dave, {"ConfidentialMPTMergeInbox", "ConfidentialMPTSend"})); env.close(); // Dave executes Send on behalf of bob. @@ -1716,10 +1718,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env(delegate::set( bob, dave, - {"ConfidentialMPTConvert", - "ConfidentialMPTMergeInbox", - "ConfidentialMPTSend", - "ConfidentialMPTConvertBack"})); + {"ConfidentialMPTMergeInbox", "ConfidentialMPTSend", "ConfidentialMPTConvertBack"})); env.close(); // Dave executes ConvertBack on behalf of bob. @@ -1766,16 +1765,15 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase // Creating the Delegate SLE consumes one owner reserve slot for bob. auto const bobOwnersBefore = ownerCount(env, bob); - env(delegate::set(bob, carol, {"ConfidentialMPTConvert", "ConfidentialMPTMergeInbox"})); + env(delegate::set(bob, carol, {"ConfidentialMPTMergeInbox"})); env.close(); env.require(Owners(bob, bobOwnersBefore + 1)); - // Carol converts and merge inbox on behalf of bob. + // Bob converts; carol merges inbox on behalf of bob. mptAlice.convert({ .account = bob, .amt = 50, .holderPubKey = mptAlice.getPubKey(bob), - .delegate = carol, }); mptAlice.mergeInbox({.account = bob, .delegate = carol}); @@ -1784,16 +1782,18 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase env.close(); env.require(Owners(bob, bobOwnersBefore)); - // Carol can no longer convert on behalf of bob. - mptAlice.convert({ + // Bob converts again to populate a fresh inbox. + mptAlice.convert({.account = bob, .amt = 30}); + + // Carol can no longer merge inbox on behalf of bob. + mptAlice.mergeInbox({ .account = bob, - .amt = 30, .delegate = carol, .err = terNO_DELEGATE_PERMISSION, }); - // Bob can still convert by himself. - mptAlice.convert({.account = bob, .amt = 30}); + // Bob can still merge his inbox. + mptAlice.mergeInbox({.account = bob}); } // Verifies that a delegated confidential transfer works correctly when an @@ -1833,16 +1833,15 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase .auditorPubKey = mptAlice.getPubKey(auditor), }); - // Bob delegates Convert and Send permissions to dave. - env(delegate::set(bob, dave, {"ConfidentialMPTSend", "ConfidentialMPTConvert"})); + // Bob delegates Send permission to dave (Convert is not delegable). + env(delegate::set(bob, dave, {"ConfidentialMPTSend"})); env.close(); - // Dave converts on behalf of bob. + // Bob converts. mptAlice.convert({ .account = bob, .amt = 50, .holderPubKey = mptAlice.getPubKey(bob), - .delegate = dave, }); mptAlice.mergeInbox({.account = bob}); @@ -2229,7 +2228,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase mpt.pay(alice, frank, 40); mpt.generateKeyPair(frank); - env(delegate::set(bob, dave, {"ConfidentialMPTConvert", "ConfidentialMPTConvertBack"})); + env(delegate::set(bob, dave, {"ConfidentialMPTConvertBack"})); env(delegate::set(carol, erin, {"ConfidentialMPTSend"})); env(delegate::set(bob, erin, {"ConfidentialMPTMergeInbox"})); env.close(); @@ -2238,15 +2237,15 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase auto const bobSeq = env.seq(bob); auto const carolSeq = env.seq(carol); auto const frankSeq = env.seq(frank); - auto const batchFee = batch::calcConfidentialBatchFee(env, 3, 6); + auto const batchFee = batch::calcConfidentialBatchFee(env, 4, 6); - // Dave submits the batch. Bob's convert and convertback use Dave as Delegate; + // Dave submits the batch. Bob's convertback uses Dave as Delegate; + // Convert is not delegable, so Bob signs his own convert inner tx. // Carol's send and Bob's mergeInbox use Erin as Delegate. Frank's // convert and mergeInbox are non-delegated. auto jv1 = mpt.convertBackJV({.account = bob, .amt = 30}, bobSeq); jv1[jss::Delegate] = dave.human(); - auto jv2 = mpt.convertJV({.account = bob, .amt = 20}, bobSeq + 1); - jv2[jss::Delegate] = dave.human(); + auto const jv2 = mpt.convertJV({.account = bob, .amt = 20}, bobSeq + 1); auto jv3 = mpt.sendJV({.account = carol, .dest = bob, .amt = 15}, carolSeq); jv3[jss::Delegate] = erin.human(); auto const jv4 = mpt.convertJV( @@ -2262,7 +2261,7 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase batch::Inner(jv4, frankSeq), batch::Inner(jv5, frankSeq + 1), batch::Inner(jv6, bobSeq + 2), - batch::Sig(erin, frank), + batch::Sig(erin, frank, bob), Ter(tesSUCCESS)); env.close(); @@ -2283,7 +2282,10 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase BEAST_EXPECT(mpt.getIssuanceConfidentialBalance() == 175); } - // Test invalid scenarios for delegation with tickets. + // Test invalid scenarios for delegation with tickets. ConfidentialMPTConvert + // is not delegable, so ConfidentialMPTConvertBack (which is delegable and + // whose ZK proof also binds to the transaction/ticket sequence) is used as + // the delegated operation. Carol acts as bob's delegate throughout. void testInvalidDelegationWithTickets(FeatureBitset features) { @@ -2309,33 +2311,52 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase mptAlice.generateKeyPair(bob); mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); - // Bob grants carol permissions. - env(delegate::set(bob, carol, {"ConfidentialMPTConvert"})); + // Give bob a confidential spending balance to convert back from. + mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)}); + mptAlice.mergeInbox({.account = bob}); + + // Bob delegates ConfidentialMPTConvertBack to carol. + env(delegate::set(bob, carol, {"ConfidentialMPTConvertBack"})); env.close(); uint64_t const amt = 10; - auto const bf = generateBlindingFactor(); - auto const holderCt = mptAlice.encryptAmount(bob, amt, bf); - auto const issuerCt = mptAlice.encryptAmount(alice, amt, bf); + + // Every case below fails, so bob's spending balance and version never + // change; capture the crypto material needed to build proofs once. + auto const spendingBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing spending balance."); + auto const encSpending = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing encrypted spending balance."); + auto const version = mptAlice.getMPTokenVersion(bob); + auto const pcBf = generateBlindingFactor(); + auto const pc = mptAlice.getPedersenCommitment(spendingBalance, pcBf); + + // Build a ConvertBack proof bound to a given sequence. + auto proofForSeq = [&](std::uint32_t seq) { + return mptAlice.getConvertBackProof( + bob, + amt, + getConvertBackContextHash(bob, mptAlice.issuanceID(), seq, version), + { + .pedersenCommitment = pc, + .amt = spendingBalance, + .encryptedAmt = encSpending, + .blindingFactor = pcBf, + }); + }; // Invalid: proof built with wrong ticket sequence (ticketSeq + 1). { auto const ticketSeq = env.seq(bob) + 1; env(ticket::create(bob, 1)); - auto const badCtxHash = - getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq + 1); - auto const badProof = requireOptional( - mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof."); - - mptAlice.convert({ + mptAlice.convertBack({ .account = bob, .amt = amt, - .proof = strHex(badProof), - .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = holderCt, - .issuerEncryptedAmt = issuerCt, - .blindingFactor = bf, + .proof = proofForSeq(ticketSeq + 1), + .pedersenCommitment = pc, .delegate = carol, .ticketSeq = ticketSeq, .err = tecBAD_PROOF, @@ -2346,18 +2367,12 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase { auto const ticketSeq = env.seq(bob) + 1; env(ticket::create(bob, 1)); - auto const badCtxHash = getConvertContextHash(bob, mptAlice.issuanceID(), env.seq(bob)); - auto const badProof = requireOptional( - mptAlice.getSchnorrProof(bob, badCtxHash), "Missing Schnorr Proof."); - mptAlice.convert({ + mptAlice.convertBack({ .account = bob, .amt = amt, - .proof = strHex(badProof), - .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = holderCt, - .issuerEncryptedAmt = issuerCt, - .blindingFactor = bf, + .proof = proofForSeq(env.seq(bob)), + .pedersenCommitment = pc, .delegate = carol, .ticketSeq = ticketSeq, .err = tecBAD_PROOF, @@ -2366,13 +2381,9 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase // Invalid: ticket sequence is far in the future and hasn't been created yet. { - mptAlice.convert({ + mptAlice.convertBack({ .account = bob, .amt = amt, - .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = holderCt, - .issuerEncryptedAmt = issuerCt, - .blindingFactor = bf, .delegate = carol, .ticketSeq = env.seq(bob) + 100, .err = terPRE_TICKET, @@ -2381,13 +2392,9 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase // Invalid: ticket sequence is in the past but was never created. { - mptAlice.convert({ + mptAlice.convertBack({ .account = bob, .amt = amt, - .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = holderCt, - .issuerEncryptedAmt = issuerCt, - .blindingFactor = bf, .delegate = carol, .ticketSeq = 1, .err = tefNO_TICKET, @@ -2395,17 +2402,14 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase } // Invalid: the delegated account, carol, creates a ticket and uses it. + // The ticket must belong to the delegator (bob), not the delegate. { auto const carolTicketSeq = env.seq(carol) + 1; env(ticket::create(carol, 1)); - mptAlice.convert({ + mptAlice.convertBack({ .account = bob, .amt = amt, - .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = holderCt, - .issuerEncryptedAmt = issuerCt, - .blindingFactor = bf, .delegate = carol, .ticketSeq = carolTicketSeq, .err = tefNO_TICKET, @@ -2418,25 +2422,31 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase auto const ticketSeq = env.seq(bob) + 1; env(ticket::create(bob, 1)); - // Build proof using ticketSeq. - auto const ctxHashForTicket = - getConvertContextHash(bob, mptAlice.issuanceID(), ticketSeq); - auto const proof = requireOptional( - mptAlice.getSchnorrProof(bob, ctxHashForTicket), "Missing Schnorr Proof."); - - // Submit without ticket. - mptAlice.convert({ + // Submit without a ticket; proof is bound to ticketSeq. + mptAlice.convertBack({ .account = bob, .amt = amt, - .proof = strHex(proof), - .holderPubKey = mptAlice.getPubKey(bob), - .holderEncryptedAmt = holderCt, - .issuerEncryptedAmt = issuerCt, - .blindingFactor = bf, + .proof = proofForSeq(ticketSeq), + .pedersenCommitment = pc, .delegate = carol, .err = tecBAD_PROOF, }); } + + // Valid: carol converts back on bob's behalf using a ticket owned by bob, + // with a proof correctly bound to that ticket sequence. bob's spending + // balance drops from 100 to 90. + { + auto const ticketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 1)); + + mptAlice.convertBack({ + .account = bob, + .amt = amt, + .delegate = carol, + .ticketSeq = ticketSeq, + }); + } } // Verifies that delegation works correctly when the delegating account uses @@ -2471,19 +2481,16 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase mptAlice.generateKeyPair(carol); mptAlice.set({.issuerPubKey = mptAlice.getPubKey(alice)}); - // Bob grants dave permissions. + // Bob grants dave permissions (Convert is not delegable). env(delegate::set( bob, dave, - {"ConfidentialMPTConvert", - "ConfidentialMPTMergeInbox", - "ConfidentialMPTSend", - "ConfidentialMPTConvertBack"})); + {"ConfidentialMPTMergeInbox", "ConfidentialMPTSend", "ConfidentialMPTConvertBack"})); // Alice grants dave permission to clawback on her behalf. env(delegate::set(alice, dave, {"ConfidentialMPTClawback"})); env.close(); - // Dave executes Convert on behalf of bob using ticket. + // Bob converts using a ticket. auto ticketSeq = env.seq(bob) + 1; env(ticket::create(bob, 1)); BEAST_EXPECT(env.seq(bob) != ticketSeq); @@ -2491,7 +2498,6 @@ class ConfidentialTransferExtended_test : public ConfidentialTransferTestBase .account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob), - .delegate = dave, .ticketSeq = ticketSeq, }); env.require(MptBalance(mptAlice, bob, 100)); diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index a3fa2ac0b5..d61220a1ef 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2750,7 +2750,7 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - std::size_t const expectedDelegableCount = 57; + std::size_t const expectedDelegableCount = 56; BEAST_EXPECTS( delegableCount == expectedDelegableCount, From faca302adfb4dec96b36d850d906b5df6708f859 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Fri, 17 Jul 2026 17:08:57 -0400 Subject: [PATCH 20/68] fix: Check transaction type before RawTransactions --- src/libxrpl/protocol/STTx.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 17d7617590..6aadefee27 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -812,16 +812,19 @@ invalidMPTAmountInTx(STObject const& tx) static bool isBatchRawTransactionOkay(STTx const& tx, std::string& reason) { - if (!tx.isFieldPresent(sfRawTransactions)) + XRPL_ASSERT( + tx.getTxnType() == ttBATCH || !tx.isFieldPresent(sfRawTransactions), + "xrpl::isBatchRawTransactionOkay : raw transactions only on batch"); + + if (tx.getTxnType() != ttBATCH) return true; - // sfRawTransactions only appears on a Batch. passesLocalChecks runs on - // unverified user and peer input, so reject (rather than assert) a non-batch - // transaction that carries it. - if (tx.getTxnType() != ttBATCH) + if (!tx.isFieldPresent(sfRawTransactions)) { - reason = "Only Batch transactions may contain raw transactions."; + // LCOV_EXCL_START + reason = "Batch transactions must contain raw transactions."; return false; + // LCOV_EXCL_STOP } if (tx.isFieldPresent(sfBatchSigners) && From 982bf36dd8f162a504182a0c78d2c8b19d99c4cd Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 17 Jul 2026 17:09:13 -0400 Subject: [PATCH 21/68] chore: Bump version to 3.3.0-rc2 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 6ac352f3e1..a5462d9c09 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc1" +char const* const versionString = "3.3.0-rc2" // clang-format on ; From 9cd531659ad12b7778f7566f0bea36a8b82b65da Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:53:15 +0100 Subject: [PATCH 22/68] fix: Revert "fix: Reject oversized SHAMap nodes in gotStaleData and fetch-pack path" --- src/xrpld/app/ledger/InboundLedgers.h | 31 ------------------- .../app/ledger/detail/InboundLedgers.cpp | 3 -- src/xrpld/overlay/detail/PeerImp.cpp | 13 -------- 3 files changed, 47 deletions(-) diff --git a/src/xrpld/app/ledger/InboundLedgers.h b/src/xrpld/app/ledger/InboundLedgers.h index 97182644e7..e288201c66 100644 --- a/src/xrpld/app/ledger/InboundLedgers.h +++ b/src/xrpld/app/ledger/InboundLedgers.h @@ -4,7 +4,6 @@ #include #include -#include #include #include #include @@ -21,36 +20,6 @@ namespace xrpl { -// Per-node cap for AS state leaves stashed via `gotStaleData`. -// -// `gotStaleData` only handles `liAS_NODE` payloads, which carry -// SHAMap state-map leaves (ledger objects). -// -// Sizing: worst-case serialized size across all 31 ledger entry -// types is ~53 KB (`XChainOwnedCreateAccountClaimID`, 256 -// attestations x ~209 B, capped by `kMaxAttestations` in -// `include/xrpl/protocol/XChainAttestations.h`), followed by -// `XChainOwnedClaimID` ~40 KB, `NFTokenPage` ~9.5 KB, and -// `LedgerHashes` ~8.2 KB. 256 KiB leaves ~4.8x headroom over the -// current worst case. -// -// Future-proofing: this cap is NOT derived from a single protocol -// constant — it is a soft bound over independently-tuned caps -// (`kMaxAttestations`, `kDirMaxTokensPerPage`, `kMaxTokenUriLength`, -// etc.). Two types (`Amendments`, `NegativeUNL`) have no hard schema -// cap and grow with network state. Revisit if a new object type or -// a lifted array cap approaches ~256 KiB. The downstream -// `SHAMapAccountStateLeafNode` construction rejects anything above -// the 16 MiB SHAMapItem invariant regardless. -inline constexpr std::size_t kMaxFetchPackNodeBytes = 256 * 1024; - -// Aggregate cap on the sum of `nodedata().size()` across all entries -// in a single `TMLedgerData` message. Rejects amplification-shaped -// payloads (many nodes, each individually under `kMaxFetchPackNodeBytes`, -// that together dwarf the per-message budget) at ingress in PeerImp, -// before dispatch into `InboundLedger::gotData` or `gotStaleData`. -inline constexpr std::size_t kMaxLedgerDataBytes = megabytes(1); - /** * Manages the lifetime of inbound ledgers. * diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index 81544fd234..dc361694cf 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -259,9 +259,6 @@ public: if (!node.has_nodeid() || !node.has_nodedata()) return; - if (node.nodedata().size() > kMaxFetchPackNodeBytes) - return; - auto newNode = SHAMapTreeNode::makeFromWire(makeSlice(node.nodedata())); if (!newNode) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 0b969792ba..962ab0f408 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1699,19 +1699,6 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - { - std::size_t totalNodeBytes = 0; - for (int i = 0; i < m->nodes_size(); ++i) - totalNodeBytes += m->nodes(i).nodedata().size(); - if (totalNodeBytes > kMaxLedgerDataBytes) - { - JLOG(pJournal_.warn()) - << "Ledger data: oversized nodes (" << totalNodeBytes << " bytes)"; - fee_.update(Resource::kFeeInvalidData, "oversized ledger nodes"); - return; - } - } - // If there is a request cookie, attempt to relay the message if (m->has_requestcookie()) { From c50edf507c3ddec833750ee27ee4348ea1ac24d3 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:27:45 +0100 Subject: [PATCH 23/68] fix: Reduce untrusted manifest cache cap to 100 --- include/xrpl/server/Manifest.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 19dfbfd54f..452047ecd7 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -346,7 +346,7 @@ private: * * Once reached, a manifest for a brand-new unlisted key is rejected. */ - static constexpr std::size_t kMaxUntrustedCount = 50000; + static constexpr std::size_t kMaxUntrustedCount = 100; /** * Running count of manifests rejected because the untrusted cap was full. From 1653f0c80ffee49426bc99eaddeaeff63cfc2f34 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 23 Jul 2026 16:39:50 -0400 Subject: [PATCH 24/68] chore: Bump version to 3.3.0-rc3 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index a5462d9c09..bf058a455f 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc2" +char const* const versionString = "3.3.0-rc3" // clang-format on ; From a5cc339d7b8d097a0ae3792420225565e2525699 Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 24 Jul 2026 18:39:35 -0400 Subject: [PATCH 25/68] fix: Revert "fix: Set request size limits and differential pricing for get-object-by-hash calls" --- src/test/overlay/TMGetObjectByHash_test.cpp | 18 +- src/xrpld/overlay/detail/PeerImp.cpp | 217 +++++--------------- src/xrpld/overlay/detail/PeerImp.h | 70 ------- src/xrpld/overlay/detail/Tuning.h | 103 ---------- 4 files changed, 49 insertions(+), 359 deletions(-) diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index e579989181..961e1b7eb4 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -100,17 +100,6 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite return lastSentMessage_; } - // Synchronous test access to the JobQueue-dispatched processor. - // The production path runs this on JtLedgerReq; tests need a - // synchronous entry point to inspect the reply via send(). - // PeerImp::processGetObjectByHash is `protected` so the derived - // test subclass can call it directly. - void - runProcessGetObjectByHash(std::shared_ptr const& m) - { - processGetObjectByHash(m); - } - static void resetId() { @@ -190,10 +179,6 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite /** * Test that reply is limited to hardMaxReplyNodes when more objects * are requested than the limit allows. - * - * `onMessage(TMGetObjectByHash)` dispatches the generic-query path - * to the JobQueue, so tests invoke the synchronous processor - * directly via `runProcessGetObjectByHash`. */ void testReplyLimit(size_t const numObjects, int const expectedReplySize) @@ -206,7 +191,8 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite auto peer = createPeer(env); auto request = createRequest(numObjects, env); - peer->runProcessGetObjectByHash(request); + // Call the onMessage handler + peer->onMessage(request); // Verify that a reply was sent auto sentMessage = peer->getLastSentMessage(); diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 962ab0f408..d5c5141317 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include #include @@ -81,7 +80,6 @@ #include #include -#include #include #include #include @@ -378,19 +376,9 @@ PeerImp::charge(Resource::Charge const& fee, std::string const& context) if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) && self->usage_.disconnect(self->pJournal_)) { - // Idempotent: only the first worker to observe Drop counts the - // metric and posts fail(). Without the guard, several queued - // workers can all see Drop before fail() lands on the strand, - // overcounting peerDisconnectsCharges_ and posting duplicate - // shutdowns. fail(std::string const&) self-posts to strand_ - // when invoked off-strand. - bool expected = false; - if (self->chargeDisconnectFired_.compare_exchange_strong( - expected, true, std::memory_order_acq_rel)) - { - self->overlay_.incPeerDisconnectCharges(); - self->fail("charge: Resources"); - } + // Sever the connection. + self->overlay_.incPeerDisconnectCharges(); + self->fail("charge: Resources"); } }); } @@ -2485,63 +2473,62 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } + protocol::TMGetObjectByHash reply; + reply.set_query(false); + reply.set_type(packet.type()); + if (packet.has_ledgerhash()) { if (!stringIsUInt256Sized(packet.ledgerhash())) { - JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_; + JLOG(pJournal_.debug()) << "GetObj: malformed ledger hash from peer " << id_; fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash"); return; } - } - // Reject oversized requests before touching the NodeStore. - // The legitimate upper bound (InboundLedger::getNeededHashes()) - // is 8 hashes; anything beyond kHardMaxReplyNodes is non-conforming. - if (packet.objects_size() > Tuning::kHardMaxReplyNodes) - { - JLOG(pJournal_.warn()) - << "GetObj: oversized request from peer " << id_ << " (" << packet.objects_size() - << " > " << Tuning::kHardMaxReplyNodes << ")"; - fee_.update(Resource::kFeeInvalidData, "oversized get object request"); - return; + + reply.set_ledgerhash(packet.ledgerhash()); } - // Dispatch heavy synchronous NodeStore lookups off the peer's - // I/O strand and onto the bounded job queue, mirroring the pattern - // used by processLedgerRequest. - std::weak_ptr const weak = shared_from_this(); - bool const queued = app_.getJobQueue().addJob(JtLedgerReq, "RcvGetObjByHash", [weak, m]() { - auto peer = weak.lock(); - if (!peer) - return; - try - { - peer->processGetObjectByHash(m); - } - catch (std::exception const& e) - { - // Surface backend failures (NodeStore I/O, allocation) - // back through the resource model so a misbehaving peer - // is still accountable rather than silently dropped. - JLOG(peer->pJournal_.warn()) << "GetObj: handler threw: " << e.what(); - peer->charge(Resource::kFeeRequestNoReply, "get object handler exception"); - } - }); - if (!queued) + fee_.update(Resource::kFeeModerateBurdenPeer, " received a get object by hash request"); + + // This is a very minimal implementation + for (int i = 0; i < packet.objects_size(); ++i) { - // The JobQueue is no longer accepting new work (typically - // because it is shutting down / has been joined). - JLOG(pJournal_.warn()) << "GetObj: job queue refused request from peer " << id_; - return; + auto const& obj = packet.objects(i); + if (obj.has_hash() && stringIsUInt256Sized(obj.hash())) + { + uint256 const hash = uint256::fromRaw(obj.hash()); + // VFALCO TODO Move this someplace more sensible so we dont + // need to inject the NodeStore interfaces. + std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; + auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)}; + if (nodeObject) + { + protocol::TMIndexedObject& newObj = *reply.add_objects(); + newObj.set_hash(hash.begin(), hash.size()); + newObj.set_data(&nodeObject->getData().front(), nodeObject->getData().size()); + + if (obj.has_nodeid()) + newObj.set_index(obj.nodeid()); + if (obj.has_ledgerseq()) + newObj.set_ledgerseq(obj.ledgerseq()); + + // Check if by adding this object, reply has reached its + // limit + if (reply.objects_size() >= Tuning::kHardMaxReplyNodes) + { + fee_.update( + Resource::kFeeModerateBurdenPeer, + "Reply limit reached. Truncating reply."); + break; + } + } + } } - // Admission-time charge: a peer that floods enqueues would - // otherwise be billed only the trivial onMessageEnd fee per - // message until the JobQueue catches up, re-creating an - // uncharged DoS window. Charge the base burden up-front (after - // a successful enqueue); the per-lookup differential is added - // in the worker. - fee_.update(Resource::kFeeModerateBurdenPeer, "received a get object by hash request"); + JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " + << packet.objects_size(); + send(std::make_shared(reply, protocol::mtGET_OBJECTS)); } else { @@ -2597,69 +2584,6 @@ PeerImp::onMessage(std::shared_ptr const& m) } } -void -PeerImp::processGetObjectByHash(std::shared_ptr const& m) -{ - protocol::TMGetObjectByHash const& packet = *m; - - protocol::TMGetObjectByHash reply; - reply.set_query(false); - reply.set_type(packet.type()); - - if (packet.has_ledgerhash()) - { - reply.set_ledgerhash(packet.ledgerhash()); - } - - // Defense in depth: caller (onMessage) already validates cheap - // structural properties of the request before dispatching here: - // - objects_size() <= kHardMaxReplyNodes (oversize gate) - // - if has_ledgerhash() then ledgerhash is uint256-sized - // The iteration cap below mirrors the oversize gate so this method - // remains safe if invoked directly by tests or future callers, and - // a peer cannot drive unbounded NodeStore lookups by sending - // non-existent hashes. - int const requested = packet.objects_size(); - int const iterLimit = std::min(requested, Tuning::kHardMaxReplyNodes); - - for (int i = 0; i < iterLimit; ++i) - { - auto const& obj = packet.objects(i); - if (!obj.has_hash() || !stringIsUInt256Sized(obj.hash())) - continue; - - uint256 const hash = uint256::fromRaw(obj.hash()); - // VFALCO TODO Move this someplace more sensible so we don't - // need to inject the NodeStore interfaces. - std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; - auto const nodeObject = app_.getNodeStore().fetchNodeObject(hash, seq); - if (!nodeObject) - continue; - - protocol::TMIndexedObject& newObj = *reply.add_objects(); - newObj.set_hash(hash.begin(), hash.size()); - auto const& data = nodeObject->getData(); - newObj.set_data(data.data(), data.size()); - if (obj.has_nodeid()) - newObj.set_index(obj.nodeid()); - if (obj.has_ledgerseq()) - newObj.set_ledgerseq(obj.ledgerseq()); - } - - // Apply work-proportional charge. `charge()` posts the disconnect - // step (if any) back to strand_, so it is safe to call from this - // JobQueue worker thread. - charge( - // We pass `requested` directly here, instead of actual lookups done. Which could be - // std::min(packet.objects_size(), static_cast(Tuning::kHardMaxReplyNodes)); - // Because we want to charge as per the request size, to discourage large requests. - computeGetObjectByHashFee(requested, reply.objects_size()), - "processed get object by hash request"); - - JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " << requested; - send(std::make_shared(reply, protocol::mtGET_OBJECTS)); -} - void PeerImp::onMessage(std::shared_ptr const& m) { @@ -3484,53 +3408,6 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) send(std::make_shared(ledgerData, protocol::mtLEDGER_DATA)); } -// Differential pricing helper. Returns only the *dynamic* component -// of the per-message charge — the base `kFeeModerateBurdenPeer` is -// applied at admission time in `onMessage(TMGetObjectByHash)` so a -// high traffic client pays for the message regardless of when (or -// whether) the worker runs. -// -// Dynamic charge model: -// -// billable = max(0, requested - kFreeObjectsPerRequest) -// missed = max(0, requested - found) -// billableMisses = min(missed, billable) // misses billed first -// billableHits = billable - billableMisses -// sizeBand = (requested > kBandMediumMax) ? kCostBandLarge -// : (requested > kBandSmallMax) ? kCostBandMedium -// : kCostBandSmall -// dynamic = billableHits * kCostPerLookupHit -// + billableMisses * kCostPerLookupMiss -// + sizeBand -// -// Misses are billed first against the billable budget because a node store -// seek dominates a cache hit and because invalid hashes are ~100% miss by construction. -Resource::Charge -PeerImp::computeGetObjectByHashFee(int const requested, int const found) -{ - int const billable = std::max(0, requested - static_cast(Tuning::kFreeObjectsPerRequest)); - // Clamp `missed` so a future caller passing found > requested cannot - // produce a negative value that flips the hits/misses split. - int const missed = std::max(0, requested - found); - int const billableMisses = std::min(missed, billable); - int const billableHits = billable - billableMisses; - - int sizeBand = Tuning::kCostBandSmall; - if (requested > Tuning::kBandMediumMax) - { - sizeBand = Tuning::kCostBandLarge; - } - else if (requested > Tuning::kBandSmallMax) - { - sizeBand = Tuning::kCostBandMedium; - } - - int const dynamic = (billableHits * Tuning::kCostPerLookupHit) + - (billableMisses * Tuning::kCostPerLookupMiss) + sizeBand; - - return Resource::Charge(dynamic, "GetObject differential"); -} - int PeerImp::getScore(bool haveItem) const { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 0085927550..a9a246452d 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -180,12 +180,6 @@ private: protocol::TMStatusChange lastStatus_; Resource::Consumer usage_; ChargeWithContext fee_; - - // One-shot guard so concurrent JobQueue workers cannot double-count - // the per-connection peer-disconnect-by-charge metric (and cannot - // post duplicate fail() calls) when several queued requests cross - // kDropThreshold before the first fail() lands on the strand. - std::atomic chargeDisconnectFired_{false}; std::shared_ptr const slot_; boost::beast::multi_buffer readBuffer_; http_request_type request_; @@ -681,70 +675,6 @@ private: void processLedgerRequest(std::shared_ptr const& m); - -protected: - // Kept `protected` so test subclasses (see - // TMGetObjectByHash_test) can drive the - // synchronous processor and the differential-pricing helper without - // routing through the JobQueue or going through `friend` plumbing. - // Production callers reach these members only via - // `onMessage(TMGetObjectByHash)` → JobQueue → `processGetObjectByHash`. - - /** - * Process a generic-query TMGetObjectByHash message. - * - * Dispatched from `onMessage(TMGetObjectByHash)` to the JobQueue - * (`JtLedgerReq`) so synchronous NodeStore lookups do not block the - * peer's I/O strand. Caps iteration at `Tuning::kHardMaxReplyNodes` - * regardless of hit/miss outcome and applies differential pricing - * via `computeGetObjectByHashFee()` after the fetch loop completes. - * - * @param m The protocol message containing requested object hashes. - */ - void - processGetObjectByHash(std::shared_ptr const& m); - - /** - * Compute the per-message resource charge for a TMGetObjectByHash - * request based on how much work was actually performed. - * - * The charge has three components on top of the base - * `Resource::kFeeModerateBurdenPeer`: - * - per-hit lookup cost (cheap; usually served from cache) - * - per-miss lookup cost (expensive node store seeks) - * - request-size band surcharge (escalates abusive batch sizes) - * - * The first `Tuning::kFreeObjectsPerRequest` objects are free so - * that legitimate `InboundLedger::getNeededHashes()` traffic - * (at most 8 objects) is unaffected. - * - * @param requested Number of objects requested by the message. This - * value is used for request-size pricing and may - * exceed `Tuning::kHardMaxReplyNodes` when this - * helper is called directly, even though processing - * caps the iterations to `Tuning::kHardMaxReplyNodes`. - * @param found Number of objects successfully returned in the - * reply. - * @return A `Resource::Charge` whose cost reflects the work performed. - */ - static Resource::Charge - computeGetObjectByHashFee(int const requested, int const found); - - /** - * Read-only accessor for the accumulated peer-message charge. - * - * Exposed at `protected` scope so test subclasses can verify the - * oversized-request rejection path (Layer 1) without invoking the - * full JobQueue handler. Production callers should never read this back — - * the value is consumed by `charge()`/`disconnect()` internally. - * - * @return The current `Resource::Charge` accumulated on `fee_`. - */ - Resource::Charge - currentFeeCharge() const - { - return fee_.fee; - } }; //------------------------------------------------------------------------------ diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index 5488fab07b..a27e8b56ad 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include @@ -64,105 +62,4 @@ static constexpr auto kMaxQueryDepth = 3; */ constexpr std::size_t kReadBufferBytes = 16384; -/** - * TMGetObjectByHash differential pricing. - * - * Honest peers ask for at most 8 hashes per call (the header, or up to - * 4 state + 4 tx hashes from `InboundLedger::getNeededHashes()`). The - * free tier covers them at zero cost. Beyond that, each lookup is billed: - * 'misses' cost much more than 'hits' because a miss does a node store seek - * while a hit is usually served from cache. On top of that, a size-band - * surcharge kicks in for larger requests so an attacker who crams a - * single message with thousands of hashes blows past - * `Resource::kDropThreshold` and gets disconnected. - * - * The numbers below are picked to keep three things true given - * `kDropThreshold = 25000`: - * - * - Honest traffic (<= 8 objects per request) is free. - * - A single all-miss request at `kHardMaxReplyNodes` (12288) costs - * more than the drop threshold, so an attacker gets dropped in one - * message. - * - A peer spamming 1024-object hit-only requests gets dropped in - * ~19 messages — fast enough to be useful, slow enough that an - * honest peer momentarily sending oversized requests has time to - * back off. - */ - -/** - * How many objects a request can ask for before per-lookup billing - * begins? - * Twice the honest peak (8) so a peer that occasionally retries a hash - * never trips pricing. Same value as `SHAMapInnerNode::kBranchFactor`; - * that's a coincidence, not a requirement. - */ -static constexpr auto kFreeObjectsPerRequest = 16; - -/** - * Cost of one cache-hit lookup. The unit; everything else is a - * multiple of this. - */ -static constexpr auto kCostPerLookupHit = 1; - -/** - * Cost of one node-store miss, in units of `kCostPerLookupHit`. - * - * A miss does a node store disk seek; a hit usually comes from cache. - * The 8x ratio is an order-of-magnitude guess at the latency gap on - * SSD-backed nodes, not a measured number. The math only requires this - * to be at least 2 — any smaller and a full-miss request at the hard - * cap wouldn't trip the drop threshold. 8 leaves headroom: if - * `kDropThreshold` goes up or `kHardMaxReplyNodes` comes down, the - * drop-on-attack property still holds without a code change. - */ -static constexpr auto kCostPerLookupMiss = 8; - -/** - * Size-band surcharges. Whichever band a request's size falls into, - * its surcharge is added once on top of the per-lookup cost. - * - * The job of the surcharge is to make crossing a band edge feel like - * a step, not a slope. With these values, the cost roughly doubles or triples at each cliff: - * - * n=64: costs 48 => n=65 costs 149 (~3x jump) - * n=1024: costs 1108 => n=1025 costs 2009 (~2x jump) - * - * The 10x step between medium and large mirrors the ~16x step - * between the band edges (64 -> 1024) so the cliff feels comparable - * at both scales. - */ -static constexpr auto kCostBandSmall = 0; -static constexpr auto kCostBandMedium = 100; -static constexpr auto kCostBandLarge = 1000; - -/** - * How many hashes per type an honest peer asks for at a time. - * - * Matches the `4` passed to `neededStateHashes(4)` and - * `neededTxHashes(4)` in `InboundLedger::getNeededHashes()`. Kept here - * instead of imported from the ledger module so overlay stays - * self-contained; if that `4` ever changes, update this in lockstep or - * the band thresholds below will start charging honest peers. - */ -static constexpr auto kLegitHashesPerType = 4; - -/** - * Cutoffs that decide which size band a request falls into. - * - * A SHAMap inner node has 16 children; an honest peer asks for 4 - * hashes per type. So: - * - * kBandSmallMax = 4 * 16 = 64 // one inner node's worth - * kBandMediumMax = 4 * 16^2 = 1024 // a depth-2 subtree's worth - * - * A request up to 64 objects is small (no surcharge); up to 1024 is - * medium; anything larger is large. The bounds are inclusive: a - * request of exactly 64 is small, 65 is medium. Anything past 1024 is - * well beyond what the honest sync path produces, so it's billed at - * the large rate to drive attack-shaped traffic over the drop - * threshold quickly. - */ -static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor; -static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor; - } // namespace xrpl::Tuning From 6668b7e8d05cea0ca395031718deed55b6593d9b Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 24 Jul 2026 18:40:02 -0400 Subject: [PATCH 26/68] chore: Bump version to 3.3.0-rc4 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index bf058a455f..56cbc1c5ce 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc3" +char const* const versionString = "3.3.0-rc4" // clang-format on ; From e290005db5a43bc99e7a87e43c1cc337f5be2e70 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 28 Jul 2026 14:02:49 -0400 Subject: [PATCH 27/68] fix: Re-revert "fix: Set request size limits and differential pricing for get-object-by-hash calls" --- src/test/overlay/TMGetObjectByHash_test.cpp | 18 +- src/xrpld/overlay/detail/PeerImp.cpp | 219 +++++++++++++++----- src/xrpld/overlay/detail/PeerImp.h | 70 +++++++ src/xrpld/overlay/detail/Tuning.h | 103 +++++++++ 4 files changed, 360 insertions(+), 50 deletions(-) diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index 961e1b7eb4..e579989181 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -100,6 +100,17 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite return lastSentMessage_; } + // Synchronous test access to the JobQueue-dispatched processor. + // The production path runs this on JtLedgerReq; tests need a + // synchronous entry point to inspect the reply via send(). + // PeerImp::processGetObjectByHash is `protected` so the derived + // test subclass can call it directly. + void + runProcessGetObjectByHash(std::shared_ptr const& m) + { + processGetObjectByHash(m); + } + static void resetId() { @@ -179,6 +190,10 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite /** * Test that reply is limited to hardMaxReplyNodes when more objects * are requested than the limit allows. + * + * `onMessage(TMGetObjectByHash)` dispatches the generic-query path + * to the JobQueue, so tests invoke the synchronous processor + * directly via `runProcessGetObjectByHash`. */ void testReplyLimit(size_t const numObjects, int const expectedReplySize) @@ -191,8 +206,7 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite auto peer = createPeer(env); auto request = createRequest(numObjects, env); - // Call the onMessage handler - peer->onMessage(request); + peer->runProcessGetObjectByHash(request); // Verify that a reply was sent auto sentMessage = peer->getLastSentMessage(); diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index d5c5141317..962ab0f408 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -80,6 +81,7 @@ #include #include +#include #include #include #include @@ -376,9 +378,19 @@ PeerImp::charge(Resource::Charge const& fee, std::string const& context) if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) && self->usage_.disconnect(self->pJournal_)) { - // Sever the connection. - self->overlay_.incPeerDisconnectCharges(); - self->fail("charge: Resources"); + // Idempotent: only the first worker to observe Drop counts the + // metric and posts fail(). Without the guard, several queued + // workers can all see Drop before fail() lands on the strand, + // overcounting peerDisconnectsCharges_ and posting duplicate + // shutdowns. fail(std::string const&) self-posts to strand_ + // when invoked off-strand. + bool expected = false; + if (self->chargeDisconnectFired_.compare_exchange_strong( + expected, true, std::memory_order_acq_rel)) + { + self->overlay_.incPeerDisconnectCharges(); + self->fail("charge: Resources"); + } } }); } @@ -2473,62 +2485,63 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - protocol::TMGetObjectByHash reply; - reply.set_query(false); - reply.set_type(packet.type()); - if (packet.has_ledgerhash()) { if (!stringIsUInt256Sized(packet.ledgerhash())) { - JLOG(pJournal_.debug()) << "GetObj: malformed ledger hash from peer " << id_; + JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_; fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash"); return; } - - reply.set_ledgerhash(packet.ledgerhash()); } - - fee_.update(Resource::kFeeModerateBurdenPeer, " received a get object by hash request"); - - // This is a very minimal implementation - for (int i = 0; i < packet.objects_size(); ++i) + // Reject oversized requests before touching the NodeStore. + // The legitimate upper bound (InboundLedger::getNeededHashes()) + // is 8 hashes; anything beyond kHardMaxReplyNodes is non-conforming. + if (packet.objects_size() > Tuning::kHardMaxReplyNodes) { - auto const& obj = packet.objects(i); - if (obj.has_hash() && stringIsUInt256Sized(obj.hash())) - { - uint256 const hash = uint256::fromRaw(obj.hash()); - // VFALCO TODO Move this someplace more sensible so we dont - // need to inject the NodeStore interfaces. - std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; - auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)}; - if (nodeObject) - { - protocol::TMIndexedObject& newObj = *reply.add_objects(); - newObj.set_hash(hash.begin(), hash.size()); - newObj.set_data(&nodeObject->getData().front(), nodeObject->getData().size()); - - if (obj.has_nodeid()) - newObj.set_index(obj.nodeid()); - if (obj.has_ledgerseq()) - newObj.set_ledgerseq(obj.ledgerseq()); - - // Check if by adding this object, reply has reached its - // limit - if (reply.objects_size() >= Tuning::kHardMaxReplyNodes) - { - fee_.update( - Resource::kFeeModerateBurdenPeer, - "Reply limit reached. Truncating reply."); - break; - } - } - } + JLOG(pJournal_.warn()) + << "GetObj: oversized request from peer " << id_ << " (" << packet.objects_size() + << " > " << Tuning::kHardMaxReplyNodes << ")"; + fee_.update(Resource::kFeeInvalidData, "oversized get object request"); + return; } - JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " - << packet.objects_size(); - send(std::make_shared(reply, protocol::mtGET_OBJECTS)); + // Dispatch heavy synchronous NodeStore lookups off the peer's + // I/O strand and onto the bounded job queue, mirroring the pattern + // used by processLedgerRequest. + std::weak_ptr const weak = shared_from_this(); + bool const queued = app_.getJobQueue().addJob(JtLedgerReq, "RcvGetObjByHash", [weak, m]() { + auto peer = weak.lock(); + if (!peer) + return; + try + { + peer->processGetObjectByHash(m); + } + catch (std::exception const& e) + { + // Surface backend failures (NodeStore I/O, allocation) + // back through the resource model so a misbehaving peer + // is still accountable rather than silently dropped. + JLOG(peer->pJournal_.warn()) << "GetObj: handler threw: " << e.what(); + peer->charge(Resource::kFeeRequestNoReply, "get object handler exception"); + } + }); + if (!queued) + { + // The JobQueue is no longer accepting new work (typically + // because it is shutting down / has been joined). + JLOG(pJournal_.warn()) << "GetObj: job queue refused request from peer " << id_; + return; + } + + // Admission-time charge: a peer that floods enqueues would + // otherwise be billed only the trivial onMessageEnd fee per + // message until the JobQueue catches up, re-creating an + // uncharged DoS window. Charge the base burden up-front (after + // a successful enqueue); the per-lookup differential is added + // in the worker. + fee_.update(Resource::kFeeModerateBurdenPeer, "received a get object by hash request"); } else { @@ -2584,6 +2597,69 @@ PeerImp::onMessage(std::shared_ptr const& m) } } +void +PeerImp::processGetObjectByHash(std::shared_ptr const& m) +{ + protocol::TMGetObjectByHash const& packet = *m; + + protocol::TMGetObjectByHash reply; + reply.set_query(false); + reply.set_type(packet.type()); + + if (packet.has_ledgerhash()) + { + reply.set_ledgerhash(packet.ledgerhash()); + } + + // Defense in depth: caller (onMessage) already validates cheap + // structural properties of the request before dispatching here: + // - objects_size() <= kHardMaxReplyNodes (oversize gate) + // - if has_ledgerhash() then ledgerhash is uint256-sized + // The iteration cap below mirrors the oversize gate so this method + // remains safe if invoked directly by tests or future callers, and + // a peer cannot drive unbounded NodeStore lookups by sending + // non-existent hashes. + int const requested = packet.objects_size(); + int const iterLimit = std::min(requested, Tuning::kHardMaxReplyNodes); + + for (int i = 0; i < iterLimit; ++i) + { + auto const& obj = packet.objects(i); + if (!obj.has_hash() || !stringIsUInt256Sized(obj.hash())) + continue; + + uint256 const hash = uint256::fromRaw(obj.hash()); + // VFALCO TODO Move this someplace more sensible so we don't + // need to inject the NodeStore interfaces. + std::uint32_t const seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0}; + auto const nodeObject = app_.getNodeStore().fetchNodeObject(hash, seq); + if (!nodeObject) + continue; + + protocol::TMIndexedObject& newObj = *reply.add_objects(); + newObj.set_hash(hash.begin(), hash.size()); + auto const& data = nodeObject->getData(); + newObj.set_data(data.data(), data.size()); + if (obj.has_nodeid()) + newObj.set_index(obj.nodeid()); + if (obj.has_ledgerseq()) + newObj.set_ledgerseq(obj.ledgerseq()); + } + + // Apply work-proportional charge. `charge()` posts the disconnect + // step (if any) back to strand_, so it is safe to call from this + // JobQueue worker thread. + charge( + // We pass `requested` directly here, instead of actual lookups done. Which could be + // std::min(packet.objects_size(), static_cast(Tuning::kHardMaxReplyNodes)); + // Because we want to charge as per the request size, to discourage large requests. + computeGetObjectByHashFee(requested, reply.objects_size()), + "processed get object by hash request"); + + JLOG(pJournal_.trace()) << "GetObj: " << reply.objects_size() << " of " << requested; + send(std::make_shared(reply, protocol::mtGET_OBJECTS)); +} + void PeerImp::onMessage(std::shared_ptr const& m) { @@ -3408,6 +3484,53 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) send(std::make_shared(ledgerData, protocol::mtLEDGER_DATA)); } +// Differential pricing helper. Returns only the *dynamic* component +// of the per-message charge — the base `kFeeModerateBurdenPeer` is +// applied at admission time in `onMessage(TMGetObjectByHash)` so a +// high traffic client pays for the message regardless of when (or +// whether) the worker runs. +// +// Dynamic charge model: +// +// billable = max(0, requested - kFreeObjectsPerRequest) +// missed = max(0, requested - found) +// billableMisses = min(missed, billable) // misses billed first +// billableHits = billable - billableMisses +// sizeBand = (requested > kBandMediumMax) ? kCostBandLarge +// : (requested > kBandSmallMax) ? kCostBandMedium +// : kCostBandSmall +// dynamic = billableHits * kCostPerLookupHit +// + billableMisses * kCostPerLookupMiss +// + sizeBand +// +// Misses are billed first against the billable budget because a node store +// seek dominates a cache hit and because invalid hashes are ~100% miss by construction. +Resource::Charge +PeerImp::computeGetObjectByHashFee(int const requested, int const found) +{ + int const billable = std::max(0, requested - static_cast(Tuning::kFreeObjectsPerRequest)); + // Clamp `missed` so a future caller passing found > requested cannot + // produce a negative value that flips the hits/misses split. + int const missed = std::max(0, requested - found); + int const billableMisses = std::min(missed, billable); + int const billableHits = billable - billableMisses; + + int sizeBand = Tuning::kCostBandSmall; + if (requested > Tuning::kBandMediumMax) + { + sizeBand = Tuning::kCostBandLarge; + } + else if (requested > Tuning::kBandSmallMax) + { + sizeBand = Tuning::kCostBandMedium; + } + + int const dynamic = (billableHits * Tuning::kCostPerLookupHit) + + (billableMisses * Tuning::kCostPerLookupMiss) + sizeBand; + + return Resource::Charge(dynamic, "GetObject differential"); +} + int PeerImp::getScore(bool haveItem) const { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index a9a246452d..0085927550 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -180,6 +180,12 @@ private: protocol::TMStatusChange lastStatus_; Resource::Consumer usage_; ChargeWithContext fee_; + + // One-shot guard so concurrent JobQueue workers cannot double-count + // the per-connection peer-disconnect-by-charge metric (and cannot + // post duplicate fail() calls) when several queued requests cross + // kDropThreshold before the first fail() lands on the strand. + std::atomic chargeDisconnectFired_{false}; std::shared_ptr const slot_; boost::beast::multi_buffer readBuffer_; http_request_type request_; @@ -675,6 +681,70 @@ private: void processLedgerRequest(std::shared_ptr const& m); + +protected: + // Kept `protected` so test subclasses (see + // TMGetObjectByHash_test) can drive the + // synchronous processor and the differential-pricing helper without + // routing through the JobQueue or going through `friend` plumbing. + // Production callers reach these members only via + // `onMessage(TMGetObjectByHash)` → JobQueue → `processGetObjectByHash`. + + /** + * Process a generic-query TMGetObjectByHash message. + * + * Dispatched from `onMessage(TMGetObjectByHash)` to the JobQueue + * (`JtLedgerReq`) so synchronous NodeStore lookups do not block the + * peer's I/O strand. Caps iteration at `Tuning::kHardMaxReplyNodes` + * regardless of hit/miss outcome and applies differential pricing + * via `computeGetObjectByHashFee()` after the fetch loop completes. + * + * @param m The protocol message containing requested object hashes. + */ + void + processGetObjectByHash(std::shared_ptr const& m); + + /** + * Compute the per-message resource charge for a TMGetObjectByHash + * request based on how much work was actually performed. + * + * The charge has three components on top of the base + * `Resource::kFeeModerateBurdenPeer`: + * - per-hit lookup cost (cheap; usually served from cache) + * - per-miss lookup cost (expensive node store seeks) + * - request-size band surcharge (escalates abusive batch sizes) + * + * The first `Tuning::kFreeObjectsPerRequest` objects are free so + * that legitimate `InboundLedger::getNeededHashes()` traffic + * (at most 8 objects) is unaffected. + * + * @param requested Number of objects requested by the message. This + * value is used for request-size pricing and may + * exceed `Tuning::kHardMaxReplyNodes` when this + * helper is called directly, even though processing + * caps the iterations to `Tuning::kHardMaxReplyNodes`. + * @param found Number of objects successfully returned in the + * reply. + * @return A `Resource::Charge` whose cost reflects the work performed. + */ + static Resource::Charge + computeGetObjectByHashFee(int const requested, int const found); + + /** + * Read-only accessor for the accumulated peer-message charge. + * + * Exposed at `protected` scope so test subclasses can verify the + * oversized-request rejection path (Layer 1) without invoking the + * full JobQueue handler. Production callers should never read this back — + * the value is consumed by `charge()`/`disconnect()` internally. + * + * @return The current `Resource::Charge` accumulated on `fee_`. + */ + Resource::Charge + currentFeeCharge() const + { + return fee_.fee; + } }; //------------------------------------------------------------------------------ diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index a27e8b56ad..5488fab07b 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -62,4 +64,105 @@ static constexpr auto kMaxQueryDepth = 3; */ constexpr std::size_t kReadBufferBytes = 16384; +/** + * TMGetObjectByHash differential pricing. + * + * Honest peers ask for at most 8 hashes per call (the header, or up to + * 4 state + 4 tx hashes from `InboundLedger::getNeededHashes()`). The + * free tier covers them at zero cost. Beyond that, each lookup is billed: + * 'misses' cost much more than 'hits' because a miss does a node store seek + * while a hit is usually served from cache. On top of that, a size-band + * surcharge kicks in for larger requests so an attacker who crams a + * single message with thousands of hashes blows past + * `Resource::kDropThreshold` and gets disconnected. + * + * The numbers below are picked to keep three things true given + * `kDropThreshold = 25000`: + * + * - Honest traffic (<= 8 objects per request) is free. + * - A single all-miss request at `kHardMaxReplyNodes` (12288) costs + * more than the drop threshold, so an attacker gets dropped in one + * message. + * - A peer spamming 1024-object hit-only requests gets dropped in + * ~19 messages — fast enough to be useful, slow enough that an + * honest peer momentarily sending oversized requests has time to + * back off. + */ + +/** + * How many objects a request can ask for before per-lookup billing + * begins? + * Twice the honest peak (8) so a peer that occasionally retries a hash + * never trips pricing. Same value as `SHAMapInnerNode::kBranchFactor`; + * that's a coincidence, not a requirement. + */ +static constexpr auto kFreeObjectsPerRequest = 16; + +/** + * Cost of one cache-hit lookup. The unit; everything else is a + * multiple of this. + */ +static constexpr auto kCostPerLookupHit = 1; + +/** + * Cost of one node-store miss, in units of `kCostPerLookupHit`. + * + * A miss does a node store disk seek; a hit usually comes from cache. + * The 8x ratio is an order-of-magnitude guess at the latency gap on + * SSD-backed nodes, not a measured number. The math only requires this + * to be at least 2 — any smaller and a full-miss request at the hard + * cap wouldn't trip the drop threshold. 8 leaves headroom: if + * `kDropThreshold` goes up or `kHardMaxReplyNodes` comes down, the + * drop-on-attack property still holds without a code change. + */ +static constexpr auto kCostPerLookupMiss = 8; + +/** + * Size-band surcharges. Whichever band a request's size falls into, + * its surcharge is added once on top of the per-lookup cost. + * + * The job of the surcharge is to make crossing a band edge feel like + * a step, not a slope. With these values, the cost roughly doubles or triples at each cliff: + * + * n=64: costs 48 => n=65 costs 149 (~3x jump) + * n=1024: costs 1108 => n=1025 costs 2009 (~2x jump) + * + * The 10x step between medium and large mirrors the ~16x step + * between the band edges (64 -> 1024) so the cliff feels comparable + * at both scales. + */ +static constexpr auto kCostBandSmall = 0; +static constexpr auto kCostBandMedium = 100; +static constexpr auto kCostBandLarge = 1000; + +/** + * How many hashes per type an honest peer asks for at a time. + * + * Matches the `4` passed to `neededStateHashes(4)` and + * `neededTxHashes(4)` in `InboundLedger::getNeededHashes()`. Kept here + * instead of imported from the ledger module so overlay stays + * self-contained; if that `4` ever changes, update this in lockstep or + * the band thresholds below will start charging honest peers. + */ +static constexpr auto kLegitHashesPerType = 4; + +/** + * Cutoffs that decide which size band a request falls into. + * + * A SHAMap inner node has 16 children; an honest peer asks for 4 + * hashes per type. So: + * + * kBandSmallMax = 4 * 16 = 64 // one inner node's worth + * kBandMediumMax = 4 * 16^2 = 1024 // a depth-2 subtree's worth + * + * A request up to 64 objects is small (no surcharge); up to 1024 is + * medium; anything larger is large. The bounds are inclusive: a + * request of exactly 64 is small, 65 is medium. Anything past 1024 is + * well beyond what the honest sync path produces, so it's billed at + * the large rate to drive attack-shaped traffic over the drop + * threshold quickly. + */ +static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor; +static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor; + } // namespace xrpl::Tuning From 24b6dad287dd3307a2ae689e36c9d6c0c29f0fbf Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 29 Jul 2026 14:24:55 -0400 Subject: [PATCH 28/68] fix: Switch SponsorshipSet to use a delta for sfFeeAmount --- include/xrpl/protocol/detail/sfields.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 4 +- .../transactions/SponsorshipSet.h | 42 +-- .../tx/transactors/sponsor/SponsorshipSet.h | 13 +- .../tx/transactors/sponsor/SponsorshipSet.cpp | 335 +++++++++++------- src/test/app/MPToken_test.cpp | 4 +- src/test/app/Sponsor_test.cpp | 272 ++++++++++++-- src/test/jtx/impl/sponsor.cpp | 12 +- src/test/jtx/sponsor.h | 12 +- .../transactions/SponsorshipSetTests.cpp | 60 ++-- 10 files changed, 522 insertions(+), 234 deletions(-) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 56527628c9..29e97b2371 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -238,6 +238,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset // int32 TYPED_SFIELD(sfLoanScale, INT32, 1) +TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -277,6 +278,7 @@ TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) TYPED_SFIELD(sfMaxFee, AMOUNT, 33) +TYPED_SFIELD(sfFeeAmountDelta, AMOUNT, 34) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6d809f7ab5..1f9603dbae 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1189,9 +1189,9 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, ({ {sfCounterpartySponsor, SoeOptional}, {sfSponsee, SoeOptional}, - {sfFeeAmount, SoeOptional}, + {sfFeeAmountDelta, SoeOptional}, {sfMaxFee, SoeOptional}, - {sfRemainingOwnerCount, SoeOptional}, + {sfRemainingOwnerCountDelta, SoeOptional}, })) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h index 0124da5e58..dfd12a329f 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -100,29 +100,29 @@ public: } /** - * @brief Get sfFeeAmount (SoeOptional) + * @brief Get sfFeeAmountDelta (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] protocol_autogen::Optional - getFeeAmount() const + getFeeAmountDelta() const { - if (hasFeeAmount()) + if (hasFeeAmountDelta()) { - return this->tx_->at(sfFeeAmount); + return this->tx_->at(sfFeeAmountDelta); } return std::nullopt; } /** - * @brief Check if sfFeeAmount is present. + * @brief Check if sfFeeAmountDelta is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasFeeAmount() const + hasFeeAmountDelta() const { - return this->tx_->isFieldPresent(sfFeeAmount); + return this->tx_->isFieldPresent(sfFeeAmountDelta); } /** @@ -152,29 +152,29 @@ public: } /** - * @brief Get sfRemainingOwnerCount (SoeOptional) + * @brief Get sfRemainingOwnerCountDelta (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] - protocol_autogen::Optional - getRemainingOwnerCount() const + protocol_autogen::Optional + getRemainingOwnerCountDelta() const { - if (hasRemainingOwnerCount()) + if (hasRemainingOwnerCountDelta()) { - return this->tx_->at(sfRemainingOwnerCount); + return this->tx_->at(sfRemainingOwnerCountDelta); } return std::nullopt; } /** - * @brief Check if sfRemainingOwnerCount is present. + * @brief Check if sfRemainingOwnerCountDelta is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasRemainingOwnerCount() const + hasRemainingOwnerCountDelta() const { - return this->tx_->isFieldPresent(sfRemainingOwnerCount); + return this->tx_->isFieldPresent(sfRemainingOwnerCountDelta); } }; @@ -243,13 +243,13 @@ public: } /** - * @brief Set sfFeeAmount (SoeOptional) + * @brief Set sfFeeAmountDelta (SoeOptional) * @return Reference to this builder for method chaining. */ SponsorshipSetBuilder& - setFeeAmount(std::decay_t const& value) + setFeeAmountDelta(std::decay_t const& value) { - object_[sfFeeAmount] = value; + object_[sfFeeAmountDelta] = value; return *this; } @@ -265,13 +265,13 @@ public: } /** - * @brief Set sfRemainingOwnerCount (SoeOptional) + * @brief Set sfRemainingOwnerCountDelta (SoeOptional) * @return Reference to this builder for method chaining. */ SponsorshipSetBuilder& - setRemainingOwnerCount(std::decay_t const& value) + setRemainingOwnerCountDelta(std::decay_t const& value) { - object_[sfRemainingOwnerCount] = value; + object_[sfRemainingOwnerCountDelta] = value; return *this; } diff --git a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h index 3310c995ae..1100c5352a 100644 --- a/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h +++ b/include/xrpl/tx/transactors/sponsor/SponsorshipSet.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -16,7 +18,7 @@ namespace xrpl { class SponsorshipSet : public Transactor { public: - static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Custom; explicit SponsorshipSet(ApplyContext& ctx) : Transactor(ctx) { @@ -47,6 +49,15 @@ public: XRPAmount fee, ReadView const& view, beast::Journal const& j) override; + +private: + TER + createSponsorship( + Keylet const& sponsorshipKeylet, + AccountID const& sponsorID, + AccountID const& sponseeID, + SLE::ref sponsorAccSle, + SLE::ref reserveSponsorAccSle); }; } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp index 24bfaad2f8..e717c626e4 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipSet.cpp @@ -3,13 +3,16 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include +#include #include #include #include @@ -17,36 +20,62 @@ #include #include +#include #include +#include #include #include namespace xrpl { +// Compute the resulting RemainingOwnerCount using signed 64-bit arithmetic to +// avoid unsigned wraparound. A missing SLE (object creation) or absent field +// counts as zero. Callers handle the out-of-range results: a negative value is +// clamped to zero (field absent) and overflow is rejected in preclaim. +static std::int64_t +totalRemainingOwnerCount( + SLE::const_ref sponsorshipSle, + std::optional const& remainingOwnerCountDelta) +{ + std::uint32_t const currentCount = + sponsorshipSle ? (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0u) : 0u; + return static_cast(currentCount) + remainingOwnerCountDelta.value_or(0); +} + static bool hasSponsorshipBudget( SLE::const_ref sponsorshipSle, - std::optional const& feeAmount, - std::optional const& remainingOwnerCount) + std::optional const& feeAmountDelta, + std::optional const& remainingOwnerCountDelta) { - // A field the transaction omits keeps whatever the existing object holds, + // sfFeeAmountDelta and sfRemainingOwnerCountDelta must be non-negative when creating a new + // Sponsorship object. + if (!sponsorshipSle) + { + if (feeAmountDelta.has_value() && *feeAmountDelta <= beast::kZero) + return false; + + if (remainingOwnerCountDelta.has_value() && *remainingOwnerCountDelta <= 0) + return false; + } + // If the transaction omits a field, it keeps whatever the existing object holds, // so fall back to the current SLE value when the tx does not set it. - bool const hasFeeAmount = feeAmount - ? *feeAmount > beast::kZero - : sponsorshipSle && (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) > beast::kZero; + STAmount const currentFee = + sponsorshipSle ? (*sponsorshipSle)[~sfFeeAmount].value_or(STAmount{0}) : STAmount{0}; + STAmount const newFee = currentFee + feeAmountDelta.value_or(STAmount{0}); - bool const hasRemainingOwnerCount = remainingOwnerCount - ? *remainingOwnerCount > 0 - : sponsorshipSle && (*sponsorshipSle)[~sfRemainingOwnerCount].value_or(0) > 0; + std::int64_t const newCount = + totalRemainingOwnerCount(sponsorshipSle, remainingOwnerCountDelta); - return hasFeeAmount || hasRemainingOwnerCount; + return newFee > beast::kZero || newCount > 0; } TxConsequences SponsorshipSet::makeTxConsequences(PreflightContext const& ctx) { - auto const feeAmount = ctx.tx[~sfFeeAmount]; - return TxConsequences{ctx.tx, feeAmount.has_value() ? feeAmount->xrp() : beast::kZero}; + auto const feeAmount = ctx.tx[~sfFeeAmountDelta]; + auto const feeAmountDelta = std::max(STAmount{0}, feeAmount.value_or(STAmount{0})); + return TxConsequences{ctx.tx, feeAmountDelta.xrp()}; } std::uint32_t @@ -90,8 +119,8 @@ SponsorshipSet::preflight(PreflightContext const& ctx) return temINVALID_FLAG; // Transactions deleting `Sponsorship` cannot include modification fields. - if (ctx.tx.isFieldPresent(sfFeeAmount) || ctx.tx.isFieldPresent(sfRemainingOwnerCount) || - ctx.tx.isFieldPresent(sfMaxFee)) + if (ctx.tx.isFieldPresent(sfFeeAmountDelta) || + ctx.tx.isFieldPresent(sfRemainingOwnerCountDelta) || ctx.tx.isFieldPresent(sfMaxFee)) return temMALFORMED; } else @@ -101,27 +130,26 @@ SponsorshipSet::preflight(PreflightContext const& ctx) if (account != sponsorID) return temMALFORMED; - // FeeAmount and MaxFee must be non-negative XRP amounts when present. - auto const checkOptionalAmountField = [&](SField const& field) -> NotTEC { - if (!ctx.tx.isFieldPresent(field)) - return tesSUCCESS; + // FeeAmountDelta must be a non-zero XRP amount when present. + if (auto const feeAmt = ctx.tx[~sfFeeAmountDelta]; + feeAmt && (!isXRP(*feeAmt) || *feeAmt == beast::kZero)) + return temBAD_AMOUNT; - auto const amount = ctx.tx.getFieldAmount(field); + // MaxFee must be a non-negative XRP amount when present. + if (auto const maxFee = ctx.tx[~sfMaxFee]; + maxFee && (!isXRP(*maxFee) || *maxFee < beast::kZero)) + return temBAD_AMOUNT; - if (!isXRP(amount)) - return temBAD_AMOUNT; + // RemainingOwnerCountDelta must be a non-zero integer when present. + if (auto const remainingOwnerCountDelta = ctx.tx[~sfRemainingOwnerCountDelta]; + remainingOwnerCountDelta && *remainingOwnerCountDelta == 0) + return temINVALID; - if (amount.xrp() < beast::kZero) - return temBAD_AMOUNT; - - return tesSUCCESS; - }; - - if (auto const ret = checkOptionalAmountField(sfFeeAmount); !isTesSuccess(ret)) - return ret; - - if (auto const ret = checkOptionalAmountField(sfMaxFee); !isTesSuccess(ret)) - return ret; + // nothing specified in the tx + if (!ctx.tx.isFieldPresent(sfRemainingOwnerCountDelta) && + !ctx.tx.isFieldPresent(sfFeeAmountDelta) && !ctx.tx.isFieldPresent(sfMaxFee) && + ((ctx.tx.getFlags() & tfUniversalMask) == 0)) + return temREDUNDANT; } return tesSUCCESS; @@ -154,12 +182,21 @@ SponsorshipSet::preclaim(PreclaimContext const& ctx) if (ctx.tx.isFlag(tfDeleteObject) && !sponsorshipSle) return tecNO_ENTRY; - // Reject creating or updating a Sponsorship that would be left with no - // budget (neither a positive FeeAmount nor a positive RemainingOwnerCount). - // Such an object is unusable yet still consumes the sponsor's reserve. - if (!ctx.tx.isFlag(tfDeleteObject) && - !hasSponsorshipBudget(sponsorshipSle, ctx.tx[~sfFeeAmount], ctx.tx[~sfRemainingOwnerCount])) - return tecNO_PERMISSION; + if (!ctx.tx.isFlag(tfDeleteObject)) + { + // Reject if applying the delta would overflow uint32_t. A negative delta + // that underflows is clamped to zero (field absent) rather than erroring. + if (totalRemainingOwnerCount(sponsorshipSle, ctx.tx[~sfRemainingOwnerCountDelta]) > + static_cast(std::numeric_limits::max())) + return tecLIMIT_EXCEEDED; + + // Reject creating or updating a Sponsorship that would be left with no + // budget (neither a positive FeeAmount nor a positive RemainingOwnerCount). + // Such an object is unusable yet still consumes the sponsor's reserve. + if (!hasSponsorshipBudget( + sponsorshipSle, ctx.tx[~sfFeeAmountDelta], ctx.tx[~sfRemainingOwnerCountDelta])) + return tecNO_PERMISSION; + } return tesSUCCESS; } @@ -208,6 +245,91 @@ deleteSponsorship(ApplyView& view, SLE::ref sle, beast::Journal j) return tesSUCCESS; } +TER +SponsorshipSet::createSponsorship( + Keylet const& sponsorshipKeylet, + AccountID const& sponsorID, + AccountID const& sponseeID, + SLE::ref sponsorAccSle, + SLE::ref reserveSponsorAccSle) +{ + auto const feeAmountDelta = ctx_.tx[~sfFeeAmountDelta]; + auto const maxFee = ctx_.tx[~sfMaxFee]; + auto const remainingOwnerCountDelta = ctx_.tx[~sfRemainingOwnerCountDelta]; + + bool const hasPositiveFeeAmount = feeAmountDelta.has_value() && *feeAmountDelta > beast::kZero; + + // Create a new Sponsorship object between the sponsor and sponsee. + auto newSle = std::make_shared(sponsorshipKeylet); + STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; + // sfFeeAmountDelta must be positive if the sponsorship object doesn't exist. This is + // checked in preclaim. + XRPL_ASSERT( + !feeAmountDelta.has_value() || *feeAmountDelta > beast::kZero, + "xrpl::SponsorshipSet::doApply : new sponsorship has positive fee amount"); + + (*newSle)[sfOwner] = sponsorID; + (*newSle)[sfSponsee] = sponseeID; + if (feeAmountDelta && feeAmountDelta->xrp() > sponsorBalanceAfterFee.xrp()) + return tecUNFUNDED; + + if (hasPositiveFeeAmount) + sponsorBalanceAfterFee -= *feeAmountDelta; + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sponsorAccSle, + sponsorBalanceAfterFee.xrp(), + reserveSponsorAccSle, + {.ownerCountDelta = 1}, + ctx_.journal, + tecUNFUNDED); + !isTesSuccess(ret)) + { + return ret; + } + + if (hasPositiveFeeAmount) + { + // New object: FeeAmount starts absent, so deduct and record the full amount + (*newSle)[sfFeeAmount] = *feeAmountDelta; + (*sponsorAccSle)[sfBalance] -= *feeAmountDelta; + } + + if (maxFee && *maxFee > beast::kZero) + (*newSle)[sfMaxFee] = *maxFee; + if (remainingOwnerCountDelta && *remainingOwnerCountDelta > 0) + (*newSle)[sfRemainingOwnerCount] = *remainingOwnerCountDelta; + + std::uint32_t flags = 0; + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + (*newSle)[sfFlags] = flags; + + auto const sponsorPage = view().dirInsert( + keylet::ownerDir(sponsorID), sponsorshipKeylet, describeOwnerDir(sponsorID)); + if (!sponsorPage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfOwnerNode] = *sponsorPage; + + auto const sponseePage = view().dirInsert( + keylet::ownerDir(sponseeID), sponsorshipKeylet, describeOwnerDir(sponseeID)); + if (!sponseePage) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*newSle)[sfSponseeNode] = *sponseePage; + + // NOLINTNEXTLINE(readability-suspicious-call-argument) + increaseOwnerCount(view(), sponsorAccSle, reserveSponsorAccSle, 1, ctx_.journal); + addSponsorToLedgerEntry(newSle, reserveSponsorAccSle); + + ctx_.view().insert(newSle); + return tesSUCCESS; +} + TER SponsorshipSet::doApply() { @@ -224,8 +346,8 @@ SponsorshipSet::doApply() if (!ctx_.view().exists(keylet::account(sponseeID))) return tecINTERNAL; // LCOV_EXCL_LINE - auto const sponsorKeylet = keylet::sponsorship(sponsorID, sponseeID); - auto const sponsorshipSle = ctx_.view().peek(sponsorKeylet); + auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID); + auto const sponsorshipSle = ctx_.view().peek(sponsorshipKeylet); if (ctx_.tx.isFlag(tfDeleteObject)) { @@ -235,11 +357,9 @@ SponsorshipSet::doApply() return deleteSponsorship(ctx_.view(), sponsorshipSle, ctx_.journal); } - auto const feeAmount = ctx_.tx[~sfFeeAmount]; + auto const feeAmountDelta = ctx_.tx[~sfFeeAmountDelta]; auto const maxFee = ctx_.tx[~sfMaxFee]; - auto const remainingOwnerCount = ctx_.tx[~sfRemainingOwnerCount]; - - bool const hasPositiveFeeAmount = feeAmount.has_value() && *feeAmount > beast::kZero; + auto const remainingOwnerCountDelta = ctx_.tx[~sfRemainingOwnerCountDelta]; auto reserveSponsorAccSle = getTxReserveSponsor(ctx_.getApplyViewContext()); if (!reserveSponsorAccSle) @@ -247,24 +367,33 @@ SponsorshipSet::doApply() if (!sponsorshipSle) { - // Create a new Sponsorship object between the sponsor and sponsee. - auto newSle = std::make_shared(sponsorKeylet); + return createSponsorship( + sponsorshipKeylet, sponsorID, sponseeID, sponsorAccSle, *reserveSponsorAccSle); + } - (*newSle)[sfOwner] = sponsorID; - (*newSle)[sfSponsee] = sponseeID; - if (feeAmount && (*feeAmount).xrp() > (*sponsorAccSle)[sfBalance]) + // Update the existing Sponsorship object. + if (feeAmountDelta) + { + auto actualDelta = feeAmountDelta.value(); + auto const currentFee = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount{0}); + + // Clamp negative delta to avoid underflow. + if (actualDelta < beast::kZero && -actualDelta > currentFee) + actualDelta = -currentFee; + // Reject if the sponsor cannot afford the (positive) delta. + if (actualDelta > beast::kZero && actualDelta > (*sponsorAccSle)[sfBalance]) return tecUNFUNDED; - STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; - if (hasPositiveFeeAmount) - sponsorBalanceAfterFee -= *feeAmount; + // Move the FeeAmount delta between the sponsor balance and Sponsorship + // object. + (*sponsorAccSle)[sfBalance] -= actualDelta; if (auto const ret = checkReserve( ctx_.getApplyViewContext(), sponsorAccSle, - sponsorBalanceAfterFee.xrp(), + (*sponsorAccSle)[sfBalance]->xrp(), *reserveSponsorAccSle, - {.ownerCountDelta = 1}, + {}, ctx_.journal, tecUNFUNDED); !isTesSuccess(ret)) @@ -272,87 +401,19 @@ SponsorshipSet::doApply() return ret; } - if (hasPositiveFeeAmount) + STAmount const newFee = currentFee + actualDelta; + // checked in preclaim + XRPL_ASSERT( + newFee >= beast::kZero, "xrpl::SponsorshipSet::doApply : new fee is non-negative"); + if (newFee == beast::kZero) { - // New object: FeeAmount starts absent, so deduct and record the full amount - (*newSle)[sfFeeAmount] = *feeAmount; - (*sponsorAccSle)[sfBalance] -= *feeAmount; + sponsorshipSle->makeFieldAbsent(sfFeeAmount); } - - if (maxFee && *maxFee > beast::kZero) - (*newSle)[sfMaxFee] = *maxFee; - if (remainingOwnerCount && *remainingOwnerCount > 0) - (*newSle)[sfRemainingOwnerCount] = *remainingOwnerCount; - - std::uint32_t flags = 0; - if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) - flags |= lsfSponsorshipRequireSignForFee; - - if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) - flags |= lsfSponsorshipRequireSignForReserve; - - (*newSle)[sfFlags] = flags; - - auto const sponsorPage = view().dirInsert( - keylet::ownerDir(sponsorID), sponsorKeylet, describeOwnerDir(sponsorID)); - if (!sponsorPage) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*newSle)[sfOwnerNode] = *sponsorPage; - - auto const sponseePage = view().dirInsert( - keylet::ownerDir(sponseeID), sponsorKeylet, describeOwnerDir(sponseeID)); - if (!sponseePage) - return tecDIR_FULL; // LCOV_EXCL_LINE - (*newSle)[sfSponseeNode] = *sponseePage; - - // NOLINTNEXTLINE(readability-suspicious-call-argument) - increaseOwnerCount(view(), sponsorAccSle, *reserveSponsorAccSle, 1, ctx_.journal); - addSponsorToLedgerEntry(newSle, *reserveSponsorAccSle); - - ctx_.view().insert(newSle); - return tesSUCCESS; - } - - // Update the existing Sponsorship object. - if (feeAmount) - { - auto const currentFeeAmount = (*sponsorshipSle)[~sfFeeAmount].valueOr(XRPAmount{0}); - auto const feeAmountDelta = XRPAmount(*feeAmount - currentFeeAmount); - - if (feeAmountDelta > beast::kZero && feeAmountDelta > (*sponsorAccSle)[sfBalance]) - return tecUNFUNDED; - - // Move the FeeAmount delta between the sponsor balance and Sponsorship - // object. - if (feeAmountDelta != beast::kZero) + else { - STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance]; - sponsorBalanceAfterFee -= feeAmountDelta; - - if (auto const ret = checkReserve( - ctx_.getApplyViewContext(), - sponsorAccSle, - sponsorBalanceAfterFee.xrp(), - *reserveSponsorAccSle, - {}, - ctx_.journal, - tecUNFUNDED); - !isTesSuccess(ret)) - { - return ret; - } - - (*sponsorAccSle)[sfBalance] -= feeAmountDelta; - if (*feeAmount == beast::kZero) - { - (*sponsorshipSle).makeFieldAbsent(sfFeeAmount); - } - else - { - (*sponsorshipSle).setFieldAmount(sfFeeAmount, *feeAmount); - } - ctx_.view().update(sponsorAccSle); + (*sponsorshipSle)[sfFeeAmount] = newFee; } + ctx_.view().update(sponsorAccSle); } if (maxFee) @@ -367,15 +428,21 @@ SponsorshipSet::doApply() } } - if (remainingOwnerCount) + if (remainingOwnerCountDelta) { - if (*remainingOwnerCount == 0) + std::int64_t const newCount = + totalRemainingOwnerCount(sponsorshipSle, remainingOwnerCountDelta); + // Overflow is rejected in preclaim; underflow clamps to zero (field absent). + XRPL_ASSERT( + newCount <= static_cast(std::numeric_limits::max()), + "xrpl::SponsorshipSet::doApply : RemainingOwnerCount does not overflow"); + if (newCount <= 0) { sponsorshipSle->makeFieldAbsent(sfRemainingOwnerCount); } else { - sponsorshipSle->at(sfRemainingOwnerCount) = *remainingOwnerCount; + sponsorshipSle->at(sfRemainingOwnerCount) = static_cast(newCount); } } diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index f3f967af81..c9adce0305 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -2116,8 +2116,8 @@ class MPToken_test : public beast::unit_test::Suite jv[jss::TransactionType] = jss::SponsorshipSet; jv[jss::Account] = alice.human(); jv[sfSponsee.fieldName] = carol.human(); - jv[sfFeeAmount.fieldName] = mpt.getJson(JsonOptions::Values::None); - test(jv, sfFeeAmount.fieldName); + jv[sfFeeAmountDelta.fieldName] = mpt.getJson(JsonOptions::Values::None); + test(jv, sfFeeAmountDelta.fieldName); } } BEAST_EXPECT(txWithAmounts.empty()); diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 1012a7d0b6..5ddc3aa6a8 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -58,6 +58,7 @@ #include #include +#include #include #include #include @@ -197,10 +198,12 @@ public: sponsor::SponseeAcc(alice), Ter(temMALFORMED)); - // Invalid feeAmount - for (auto const& amt : {XRP(-1), usd(1)}) + // Invalid FeeAmountDelta + for (auto const& amt : {XRP(0), usd(1)}) { - env(sponsor::set_fee(sponsor, 0, amt), sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT)); + env(sponsor::set_fee(sponsor, 0, amt, XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temBAD_AMOUNT)); } // Invalid MaxFee for (auto const& amt : {XRP(-1), usd(1)}) @@ -209,6 +212,10 @@ public: sponsor::SponseeAcc(alice), Ter(temBAD_AMOUNT)); } + // Invalid RemainingOwnerCountDelta + env(sponsor::set(sponsor, 0, 0, XRP(2), XRP(1)), + sponsor::SponseeAcc(alice), + Ter(temINVALID)); // Invalid Delete operation env(sponsor::set_reserve(sponsor, tfDeleteObject, 1), @@ -229,12 +236,15 @@ public: sponsor::CounterpartySponsor(alice), Ter(temMALFORMED)); + // Redundant tx + env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(temREDUNDANT)); + // // preclaim // // Invalid Sponsee - env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST)); + env(sponsor::set(sponsor, 0, 1), sponsor::SponseeAcc(noFunded), Ter(tecNO_DST)); env.close(); // Invalid Sponsor @@ -290,7 +300,7 @@ public: // Decreasing feeAmount should succeed (refund, negative delta) adjustAccountXRPBalance(env, sponsor, XRP(500)); - env(sponsor::set_fee(sponsor, 0, XRP(800)), + env(sponsor::set_fee(sponsor, 0, XRP(-200)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); @@ -299,7 +309,7 @@ public: // Increasing feeAmount within delta budget should succeed adjustAccountXRPBalance(env, sponsor, XRP(500)); - env(sponsor::set_fee(sponsor, 0, XRP(850)), + env(sponsor::set_fee(sponsor, 0, XRP(50)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); @@ -308,18 +318,15 @@ public: // Increasing feeAmount where delta exceeds balance should fail adjustAccountXRPBalance(env, sponsor, XRP(310)); - env(sponsor::set_fee(sponsor, 0, XRP(1200)), + env(sponsor::set_fee(sponsor, 0, XRP(350)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tecUNFUNDED)); env.close(); // Increasing feeAmount to reach insufficient reserve - auto const currentFeeAmount = env.le(keylet::sponsorship(sponsor.id(), alice.id())) - ->getFieldAmount(sfFeeAmount) - .xrp(); adjustAccountXRPBalance(env, sponsor, XRP(310)); - env(sponsor::set_fee(sponsor, 0, currentFeeAmount + XRP(309)), + env(sponsor::set_fee(sponsor, 0, XRP(309)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tecUNFUNDED)); @@ -543,7 +550,7 @@ public: BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(1)); // update sponsorship (decrement) - env(sponsor::set(sponsor, 0, 50, XRP(50), XRP(0.5)), + env(sponsor::set(sponsor, 0, -50, XRP(-50), XRP(0.5)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); @@ -557,7 +564,7 @@ public: BEAST_EXPECT(env.balance(sponsor) == XRP(10000) - sle->at(sfFeeAmount) - XRP(2)); // update sponsorship (increment) - env(sponsor::set(sponsor, 0, 200, XRP(200), XRP(2)), + env(sponsor::set(sponsor, 0, 150, XRP(150), XRP(2)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); @@ -591,26 +598,32 @@ public: env.close(); BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); - // Cannot create sponsorship with no fee or reserve budget. MaxFee - // and flags do not make a sponsorship object useful by themselves. - env(sponsor::set(sponsor, 0), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); - env.close(); - BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); - env(sponsor::set_max_fee(sponsor, 0, XRP(1)), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); - env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + env(sponsor::set(sponsor, 0, std::nullopt, std::nullopt, XRP(0)), sponsor::SponseeAcc(alice), Ter(tecNO_PERMISSION)); env.close(); BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); - // update sponsorship with non-zero value - env(sponsor::set(sponsor, 0, 100, XRP(100), XRP(1)), + // create sponsorship with negative values + env(sponsor::set_reserve(sponsor, 0, -100), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + env(sponsor::set_fee(sponsor, 0, XRP(-100)), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + BEAST_EXPECT(!env.le(keylet::sponsorship(sponsor, alice))); + + // create sponsorship with non-zero value + env(sponsor::set(sponsor, 0, 100, XRP(101), XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1))); env.close(); @@ -618,7 +631,7 @@ public: sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); - BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(101)); BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); // update sponsorship flags @@ -648,7 +661,7 @@ public: lsfSponsorshipRequireSignForReserve); // Cannot update sponsorship so both fee and reserve budgets are absent. - env(sponsor::set(sponsor, 0, 0, XRP(0), XRP(0)), + env(sponsor::set(sponsor, 0, -100, XRP(-101), std::nullopt), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tecNO_PERMISSION)); @@ -657,17 +670,17 @@ public: sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); - BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(101)); BEAST_EXPECT(sle->at(sfMaxFee) == XRP(1)); } { // Removing one budget field while the other remains keeps the // Sponsorship valid. Starting state (from above): - // RemainingOwnerCount = 100, FeeAmount = XRP(100). + // RemainingOwnerCount = 100, FeeAmount = XRP(101). // Remove only FeeAmount (set to 0); RemainingOwnerCount remains. - env(sponsor::set_fee(sponsor, 0, XRP(0)), + env(sponsor::set_fee(sponsor, 0, XRP(-101)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); @@ -686,12 +699,51 @@ public: Ter(tesSUCCESS)); env.close(); - env(sponsor::set_reserve(sponsor, 0, 0), + // A negative FeeAmountDelta larger than the current FeeAmount is + // clamped, so only the current FeeAmount is refunded and the field + // is removed. RemainingOwnerCount keeps the Sponsorship valid. + auto const balanceBefore = env.balance(sponsor); + env(sponsor::set_fee(sponsor, 0, XRP(-500)), sponsor::SponseeAcc(alice), Fee(XRP(1)), Ter(tesSUCCESS)); env.close(); + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfFeeAmount)); + BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 100); + BEAST_EXPECT(env.balance(sponsor) == balanceBefore + XRP(100) - XRP(1)); + + // Restore FeeAmount for the checks below. + env(sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, -100), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tesSUCCESS)); + env.close(); + + sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + + // Decreasing FeeAmount below zero must fail with tecNO_PERMISSION + // when there is no RemainingOwnerCount (the budget would become + // entirely empty). Current state: FeeAmount = XRP(100), no + // RemainingOwnerCount. + env(sponsor::set_fee(sponsor, 0, XRP(-101)), + sponsor::SponseeAcc(alice), + Fee(XRP(1)), + Ter(tecNO_PERMISSION)); + env.close(); + + // Confirm that the sponsorship is unchanged. sle = env.le(keylet::sponsorship(sponsor, alice)); BEAST_EXPECT(sle); BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); @@ -748,6 +800,160 @@ public: } } + void + testRemainingOwnerCountOverflow() + { + testcase("RemainingOwnerCount overflow and underflow clamping"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + constexpr std::int32_t kInt32Max = std::numeric_limits::max(); + + // --- Positive overflow: delta causes count to exceed UINT32_MAX --- + { + // Create with count = INT32_MAX. + env(sponsor::set_reserve(sponsor, 0, kInt32Max), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == + static_cast(kInt32Max)); + + // Add INT32_MAX again: count = 2 * INT32_MAX = 4294967294 (<= UINT32_MAX, still ok). + env(sponsor::set_reserve(sponsor, 0, kInt32Max), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == + 2u * static_cast(kInt32Max)); + + // Adding 2 more pushes count to 4294967296, exceeding UINT32_MAX: reject. + env(sponsor::set_reserve(sponsor, 0, 2), + sponsor::SponseeAcc(alice), + Ter(tecLIMIT_EXCEEDED)); + env.close(); + + // SLE is unchanged. + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == + 2u * static_cast(kInt32Max)); + + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + } + + // --- Negative underflow: clamps to 0; fee budget survives --- + { + // Create with count=10 and a fee budget. + env(sponsor::set(sponsor, 0, 10, XRP(100)), + sponsor::SponseeAcc(alice), + Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10u); + + // Delta of -20 produces count = -10; clamps to 0 (field absent). + env(sponsor::set_reserve(sponsor, 0, -20), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + + auto sle = env.le(keylet::sponsorship(sponsor, alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(!sle->isFieldPresent(sfRemainingOwnerCount)); + BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(100)); + + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + } + + // --- Negative underflow: clamped count=0 with no fee budget → no budget --- + { + // Create with count=10, no fee. + env(sponsor::set_reserve(sponsor, 0, 10), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10u); + + // Delta of -20 would clamp count to 0 with no fee → empty budget → tecNO_PERMISSION. + env(sponsor::set_reserve(sponsor, 0, -20), + sponsor::SponseeAcc(alice), + Ter(tecNO_PERMISSION)); + env.close(); + + // SLE is unchanged. + BEAST_EXPECT( + env.le(keylet::sponsorship(sponsor, alice))->at(sfRemainingOwnerCount) == 10u); + + env(sponsor::del(sponsor), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env.close(); + } + } + + void + testConsequences() + { + testcase("Consequences"); + using namespace test::jtx; + Env env{*this, testableAmendments()}; + auto const baseFee = env.current()->fees().base; + + Account const alice("alice"); + Account const sponsor("sponsor"); + env.memoize(alice); + env.memoize(sponsor); + + { + // A positive FeeAmountDelta is the maximum XRP the tx can spend. + auto const jt = env.jt( + sponsor::set_fee(sponsor, 0, XRP(100)), + sponsor::SponseeAcc(alice), + Seq(1), + Fee(baseFee)); + auto const pf = + preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(baseFee)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(100)); + } + + { + // A negative FeeAmountDelta withdraws from the sponsorship, so the + // transaction cannot spend anything. + auto const jt = env.jt( + sponsor::set_fee(sponsor, 0, XRP(-100)), + sponsor::SponseeAcc(alice), + Seq(1), + Fee(baseFee)); + auto const pf = + preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(baseFee)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); + } + + { + // No FeeAmountDelta at all. + auto const jt = env.jt( + sponsor::set_reserve(sponsor, 0, 10), + sponsor::SponseeAcc(alice), + Seq(1), + Fee(baseFee)); + auto const pf = + preflight(env.app(), env.current()->rules(), *jt.stx, TapNone, env.journal); + BEAST_EXPECT(isTesSuccess(pf.ter)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(baseFee)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); + } + } + void testPreFundAndCosign() { @@ -810,7 +1016,7 @@ public: Ter(terINSUF_FEE_B)); env.close(); - env(sponsor::set_reserve(sponsor, 0, 0), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); + env(sponsor::set_reserve(sponsor, 0, -1), sponsor::SponseeAcc(alice), Ter(tesSUCCESS)); env.close(); // reserve insufficient @@ -2090,7 +2296,7 @@ public: XRP(10)); // clear flag - env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + env(sponsor::set(sponsor, tfSponsorshipClearRequireSignForFee), sponsor::SponseeAcc(alice)); env.close(); @@ -2322,7 +2528,7 @@ public: XRP(10)); // clear flag - env(sponsor::set_fee(sponsor, tfSponsorshipClearRequireSignForFee, XRP(10)), + env(sponsor::set(sponsor, tfSponsorshipClearRequireSignForFee), sponsor::SponseeAcc(alice)); env.close(); @@ -4939,7 +5145,7 @@ public: env.close(); // Create pre-funded sponsorship - env(sponsor::set(sponsor, 0, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1))); + env(sponsor::set_fee(sponsor, 0, XRP(1)), sponsor::SponseeAcc(alice), Fee(XRP(1))); env.close(); auto const seq = env.seq(alice); @@ -5443,6 +5649,8 @@ protected: testInvalidSponsorField(); testSimpleSponsorshipSet(); + testRemainingOwnerCountOverflow(); + testConsequences(); testPreFundAndCosign(); testSponsoredFreeTierReserve(); diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp index cdf68800f5..453ccebcb9 100644 --- a/src/test/jtx/impl/sponsor.cpp +++ b/src/test/jtx/impl/sponsor.cpp @@ -21,18 +21,18 @@ namespace xrpl::test::jtx::sponsor { json::Value set(jtx::Account const& account, uint32_t flags, - std::optional const reserveCount, - std::optional const feeAmount, + std::optional const reserveCountDelta, + std::optional const feeAmountDelta, std::optional const maxFee) { json::Value jv; jv[jss::TransactionType] = jss::SponsorshipSet; jv[jss::Account] = account.human(); jv[sfFlags.jsonName] = flags; - if (reserveCount) - jv[sfRemainingOwnerCount.jsonName] = *reserveCount; - if (feeAmount) - jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::Values::None); + if (reserveCountDelta) + jv[sfRemainingOwnerCountDelta.jsonName] = *reserveCountDelta; + if (feeAmountDelta) + jv[sfFeeAmountDelta.jsonName] = feeAmountDelta->getJson(JsonOptions::Values::None); if (maxFee) jv[sfMaxFee.jsonName] = maxFee->getJson(JsonOptions::Values::None); return jv; diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h index 43d55d7246..f87a13c462 100644 --- a/src/test/jtx/sponsor.h +++ b/src/test/jtx/sponsor.h @@ -18,24 +18,24 @@ namespace xrpl::test::jtx::sponsor { json::Value set(jtx::Account const& account, std::uint32_t flags, - std::optional const reserveCount = std::nullopt, - std::optional const feeAmount = std::nullopt, + std::optional const reserveCountDelta = std::nullopt, + std::optional const feeAmountDelta = std::nullopt, std::optional const maxFee = std::nullopt); inline json::Value set_fee( jtx::Account const& account, std::uint32_t flags, - STAmount feeAmount, + STAmount feeAmountDelta, std::optional maxFee = std::nullopt) { - return set(account, flags, std::nullopt, std::move(feeAmount), std::move(maxFee)); + return set(account, flags, std::nullopt, std::move(feeAmountDelta), std::move(maxFee)); } inline json::Value -set_reserve(jtx::Account const& account, std::uint32_t flags, std::uint32_t reserveCount) +set_reserve(jtx::Account const& account, std::uint32_t flags, std::int32_t reserveCountDelta) { - return set(account, flags, reserveCount); + return set(account, flags, reserveCountDelta); } inline json::Value diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp index dce8cfca3f..c5bc41c6e6 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/SponsorshipSetTests.cpp @@ -31,9 +31,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) // Transaction-specific field values auto const counterpartySponsorValue = canonical_ACCOUNT(); auto const sponseeValue = canonical_ACCOUNT(); - auto const feeAmountValue = canonical_AMOUNT(); + auto const feeAmountDeltaValue = canonical_AMOUNT(); auto const maxFeeValue = canonical_AMOUNT(); - auto const remainingOwnerCountValue = canonical_UINT32(); + auto const remainingOwnerCountDeltaValue = canonical_INT32(); SponsorshipSetBuilder builder{ accountValue, @@ -44,9 +44,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) // Set optional fields builder.setCounterpartySponsor(counterpartySponsorValue); builder.setSponsee(sponseeValue); - builder.setFeeAmount(feeAmountValue); + builder.setFeeAmountDelta(feeAmountDeltaValue); builder.setMaxFee(maxFeeValue); - builder.setRemainingOwnerCount(remainingOwnerCountValue); + builder.setRemainingOwnerCountDelta(remainingOwnerCountDeltaValue); auto tx = builder.build(publicKey, secretKey); @@ -81,11 +81,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) } { - auto const& expected = feeAmountValue; - auto const actualOpt = tx.getFeeAmount(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present"; - expectEqualField(expected, *actualOpt, "sfFeeAmount"); - EXPECT_TRUE(tx.hasFeeAmount()); + auto const& expected = feeAmountDeltaValue; + auto const actualOpt = tx.getFeeAmountDelta(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmountDelta should be present"; + expectEqualField(expected, *actualOpt, "sfFeeAmountDelta"); + EXPECT_TRUE(tx.hasFeeAmountDelta()); } { @@ -97,11 +97,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip) } { - auto const& expected = remainingOwnerCountValue; - auto const actualOpt = tx.getRemainingOwnerCount(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present"; - expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); - EXPECT_TRUE(tx.hasRemainingOwnerCount()); + auto const& expected = remainingOwnerCountDeltaValue; + auto const actualOpt = tx.getRemainingOwnerCountDelta(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCountDelta should be present"; + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCountDelta"); + EXPECT_TRUE(tx.hasRemainingOwnerCountDelta()); } } @@ -122,9 +122,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) // Transaction-specific field values auto const counterpartySponsorValue = canonical_ACCOUNT(); auto const sponseeValue = canonical_ACCOUNT(); - auto const feeAmountValue = canonical_AMOUNT(); + auto const feeAmountDeltaValue = canonical_AMOUNT(); auto const maxFeeValue = canonical_AMOUNT(); - auto const remainingOwnerCountValue = canonical_UINT32(); + auto const remainingOwnerCountDeltaValue = canonical_INT32(); // Build an initial transaction SponsorshipSetBuilder initialBuilder{ @@ -135,9 +135,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) initialBuilder.setCounterpartySponsor(counterpartySponsorValue); initialBuilder.setSponsee(sponseeValue); - initialBuilder.setFeeAmount(feeAmountValue); + initialBuilder.setFeeAmountDelta(feeAmountDeltaValue); initialBuilder.setMaxFee(maxFeeValue); - initialBuilder.setRemainingOwnerCount(remainingOwnerCountValue); + initialBuilder.setRemainingOwnerCountDelta(remainingOwnerCountDeltaValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -171,10 +171,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) } { - auto const& expected = feeAmountValue; - auto const actualOpt = rebuiltTx.getFeeAmount(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present"; - expectEqualField(expected, *actualOpt, "sfFeeAmount"); + auto const& expected = feeAmountDeltaValue; + auto const actualOpt = rebuiltTx.getFeeAmountDelta(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmountDelta should be present"; + expectEqualField(expected, *actualOpt, "sfFeeAmountDelta"); } { @@ -185,10 +185,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip) } { - auto const& expected = remainingOwnerCountValue; - auto const actualOpt = rebuiltTx.getRemainingOwnerCount(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present"; - expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount"); + auto const& expected = remainingOwnerCountDeltaValue; + auto const actualOpt = rebuiltTx.getRemainingOwnerCountDelta(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCountDelta should be present"; + expectEqualField(expected, *actualOpt, "sfRemainingOwnerCountDelta"); } } @@ -250,12 +250,12 @@ TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getCounterpartySponsor().has_value()); EXPECT_FALSE(tx.hasSponsee()); EXPECT_FALSE(tx.getSponsee().has_value()); - EXPECT_FALSE(tx.hasFeeAmount()); - EXPECT_FALSE(tx.getFeeAmount().has_value()); + EXPECT_FALSE(tx.hasFeeAmountDelta()); + EXPECT_FALSE(tx.getFeeAmountDelta().has_value()); EXPECT_FALSE(tx.hasMaxFee()); EXPECT_FALSE(tx.getMaxFee().has_value()); - EXPECT_FALSE(tx.hasRemainingOwnerCount()); - EXPECT_FALSE(tx.getRemainingOwnerCount().has_value()); + EXPECT_FALSE(tx.hasRemainingOwnerCountDelta()); + EXPECT_FALSE(tx.getRemainingOwnerCountDelta().has_value()); } } From ccb9db0bc71553a2bac85fc8fa1c37d6de553cb3 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 29 Jul 2026 14:25:13 -0400 Subject: [PATCH 29/68] chore: Bump version to 3.3.0-rc5 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 56cbc1c5ce..6bb2c40cff 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc4" +char const* const versionString = "3.3.0-rc5" // clang-format on ; From 3ad6ce236eeeb72fd1208c8e225eedcca9b798c6 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 30 Jul 2026 16:29:37 +0100 Subject: [PATCH 30/68] feat: Package validator-keys inside rippled --- .cspell.config.yaml | 3 ++ .github/scripts/strategy-matrix/generate.py | 10 +++- .github/scripts/strategy-matrix/linux.json | 6 ++- .../workflows/reusable-build-test-config.yml | 19 ++++++- .github/workflows/reusable-package.yml | 26 +++++---- CMakeLists.txt | 4 +- cmake/PatchNixBinary.cmake | 47 ++++++++++++---- cmake/XrplPackaging.cmake | 15 +++++- cmake/XrplValidatorKeys.cmake | 33 +++++++++--- package/README.md | 53 +++++++++++++------ package/build_pkg.sh | 48 ++++++++++++++--- package/debian/control | 8 +-- package/debian/copyright | 19 +++++++ package/debian/rules | 1 + package/debian/xrpld.docs | 1 + package/rpm/xrpld.spec | 7 +++ 16 files changed, 238 insertions(+), 62 deletions(-) diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 13da132b90..78ba979948 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -130,6 +130,7 @@ words: - godexsoft - gpgcheck - gpgkey + - Hinnant - hotwallet - hwaddress - hwrap @@ -163,6 +164,7 @@ words: - llection - LOCALGOOD - logwstream + - Lombrozo - lseq - lsmf - ltype @@ -200,6 +202,7 @@ words: - nftokens - nftpage - nikb + - Nikolaos - nixfmt - nixos - nixpkgs diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index c783f32fb7..b0a7b4b321 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -136,7 +136,8 @@ class MatrixEntry: class PackagingEntry: """One entry in the generated packaging strategy matrix.""" - artifact_name: str + xrpld_artifact_name: str + validator_keys_artifact_name: str image: str distro: str # e.g. "debian" or "rhel"; drives package-format-specific steps @@ -210,14 +211,19 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]: the nix-based build images, because deb/rpm tooling (debhelper, rpm-build) is taken from the distro's archive rather than from nixpkgs. Each config entry carries its own 'image'. + + The artifact names must match what the build job uploads: one artifact per + binary, each named after the build config. """ entries = [] for distro, configs in linux.package_configs.items(): for cfg in configs: for compiler, build_type in itertools.product(cfg.compiler, cfg.build_type): + config_name = f"{distro}-{compiler}-{build_type.lower()}-amd64" entries.append( PackagingEntry( - artifact_name=f"xrpld-{distro}-{compiler}-{build_type.lower()}-amd64", + xrpld_artifact_name=f"xrpld-{config_name}", + validator_keys_artifact_name=f"validator-keys-{config_name}", image=cfg.image, distro=distro, ) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 03ac1c6334..62aead518a 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -68,7 +68,8 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], - "minimal": false + "minimal": false, + "extra_cmake_args": "-Dvalidator_keys=ON" } ], @@ -77,7 +78,8 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], - "minimal": false + "minimal": false, + "extra_cmake_args": "-Dvalidator_keys=ON" } ] }, diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index a1daeed5fe..60f11bed4b 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -100,9 +100,10 @@ jobs: # header files are copied into separate directories by CMake, which will # otherwise result in cache misses. CCACHE_SLOPPINESS: include_file_ctime,include_file_mtime - # Determine if coverage and voidstar should be enabled. + # Determine if coverage, voidstar and validator-keys should be enabled. COVERAGE_ENABLED: ${{ contains(inputs.cmake_args, '-Dcoverage=ON') }} VOIDSTAR_ENABLED: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }} + VALIDATOR_KEYS_ENABLED: ${{ contains(inputs.cmake_args, '-Dvalidator_keys=ON') }} SANITIZERS_ENABLED: ${{ inputs.sanitizers != '' }} steps: - name: Cleanup workspace (macOS and Windows) @@ -247,6 +248,22 @@ jobs: retention-days: 3 if-no-files-found: error + - name: Run the validator-keys tests + if: ${{ env.VALIDATOR_KEYS_ENABLED == 'true' }} + working-directory: ${{ env.BUILD_DIR }} + run: ./validator-keys --unittest + + - name: Upload the validator-keys binary + if: ${{ github.event.repository.visibility == 'public' && env.VALIDATOR_KEYS_ENABLED == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: validator-keys-${{ inputs.config_name }} + path: | + ${{ env.BUILD_DIR }}/validator-keys + ${{ env.BUILD_DIR }}/validator-keys-LICENSE + retention-days: 3 + if-no-files-found: error + - name: Upload the test binary (Linux) if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 6feecbfb75..0a0c96e7dd 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -1,7 +1,7 @@ -# Build Linux packages (DEB and RPM) from pre-built binary artifacts. -# Discovers which configurations to package from linux.json (configs in -# "package_configs") and fans out one job per distro. Only linux/amd64 is -# supported; the runner is hardcoded in the job below. +# Build Linux packages (DEB and RPM) from pre-built binary artifacts (xrpld and +# validator-keys). Discovers which configurations to package from linux.json +# (configs in "package_configs") and fans out one job per distro. Only +# linux/amd64 is supported; the runner is hardcoded in the job below. name: Package on: @@ -45,7 +45,7 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} - name: "${{ matrix.artifact_name }}" + name: "${{ matrix.xrpld_artifact_name }}" permissions: contents: read runs-on: ["self-hosted", "Linux", "X64", "heavy"] @@ -56,14 +56,20 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Download pre-built binary + - name: Download pre-built xrpld binary uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ matrix.artifact_name }} + name: ${{ matrix.xrpld_artifact_name }} path: ${{ env.BUILD_DIR }} - - name: Make binary executable - run: chmod +x "${BUILD_DIR}/xrpld" + - name: Download pre-built validator-keys binary + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ matrix.validator_keys_artifact_name }} + path: ${{ env.BUILD_DIR }} + + - name: Make binaries executable + run: chmod +x "${BUILD_DIR}/xrpld" "${BUILD_DIR}/validator-keys" - name: Build package env: @@ -73,7 +79,7 @@ jobs: - name: Upload package artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: ${{ matrix.artifact_name }}-pkg + name: ${{ matrix.xrpld_artifact_name }}-pkg path: | ${{ env.BUILD_DIR }}/debbuild/*.deb ${{ env.BUILD_DIR }}/debbuild/*.ddeb diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e8befcc8f..ac2bed1aa8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,8 +138,10 @@ endif() include(XrplCore) include(XrplProtocolAutogen) include(XrplInstall) -include(XrplPackaging) include(XrplValidatorKeys) +# Must come after XrplValidatorKeys: the 'package' target depends on the +# validator-keys target existing. +include(XrplPackaging) if(tests) include(CTest) diff --git a/cmake/PatchNixBinary.cmake b/cmake/PatchNixBinary.cmake index 79ca0b150c..18d992305f 100644 --- a/cmake/PatchNixBinary.cmake +++ b/cmake/PatchNixBinary.cmake @@ -3,9 +3,9 @@ The Nix-based CI image links binaries against an ELF interpreter (loader) that lives in the Nix store, so the resulting binaries don't run elsewhere - (including once installed from the .deb package). `patch_nix_binary` adds a - POST_BUILD step that resets the interpreter to the system default loader and - drops the rpath. + (including once installed from the .deb package). `patch_nix_binary` resets + the interpreter to the system default loader and drops the rpath, once the + binary has been linked. This is only active inside the Nix-based image, detected by the presence of /tmp/loader-path.sh (shipped by that image, resolves the default loader). It @@ -41,13 +41,38 @@ function(patch_nix_binary target) if(NOT PATCH_NIX_BINARIES) return() endif() - add_custom_command( - TARGET ${target} - POST_BUILD - COMMAND - "${PATCHELF_COMMAND}" --set-interpreter "${DEFAULT_LOADER_PATH}" - --remove-rpath "$" - COMMENT "Patching ${target}: set default loader, remove rpath" - VERBATIM + + set(patch_command + "${PATCHELF_COMMAND}" + --set-interpreter + "${DEFAULT_LOADER_PATH}" + --remove-rpath + "$" ) + set(comment "Patching ${target}: set default loader, remove rpath") + + # POST_BUILD is the cheap way to do this: it runs only when the binary is + # relinked. It is also only available in the directory that defined the + # target, so for a target from elsewhere (e.g. a FetchContent subproject) + # fall back to a custom target that runs after the binary is linked. That + # one runs on every build, which is harmless because patchelf is idempotent. + get_target_property(target_source_dir ${target} SOURCE_DIR) + if("${target_source_dir}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${patch_command} + COMMENT "${comment}" + VERBATIM + ) + else() + add_custom_target( + ${target}-patch-nix + ALL + COMMAND ${patch_command} + COMMENT "${comment}" + VERBATIM + ) + add_dependencies(${target}-patch-nix ${target}) + endif() endfunction() diff --git a/cmake/XrplPackaging.cmake b/cmake/XrplPackaging.cmake index 8e3861925d..bee7b15791 100644 --- a/cmake/XrplPackaging.cmake +++ b/cmake/XrplPackaging.cmake @@ -25,6 +25,19 @@ if(NOT (RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE)) return() endif() +if(NOT TARGET xrpld) + message(STATUS "xrpld=ON is required; 'package' target not available") + return() +endif() + +if(NOT TARGET validator-keys) + message( + STATUS + "validator_keys=ON is required; 'package' target not available" + ) + return() +endif() + set(package_env SRC_DIR=${CMAKE_SOURCE_DIR} BUILD_DIR=${CMAKE_BINARY_DIR} @@ -37,7 +50,7 @@ add_custom_target( ${CMAKE_COMMAND} -E env ${package_env} ${CMAKE_SOURCE_DIR}/package/build_pkg.sh WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - DEPENDS xrpld + DEPENDS xrpld validator-keys COMMENT "Building Linux package (deb/rpm inferred from host tooling)" VERBATIM ) diff --git a/cmake/XrplValidatorKeys.cmake b/cmake/XrplValidatorKeys.cmake index 0e511b6a88..0acaed1a56 100644 --- a/cmake/XrplValidatorKeys.cmake +++ b/cmake/XrplValidatorKeys.cmake @@ -5,22 +5,39 @@ option( ) if(validator_keys) - git_branch(current_branch) - # default to tracking VK master branch unless we are on release - if(NOT (current_branch STREQUAL "release")) - set(current_branch "master") - endif() - message(STATUS "Tracking ValidatorKeys branch: ${current_branch}") + # Own the install destination below rather than relying on another module + # having pulled this in first. + include(GNUInstallDirs) + + # Pinned to an exact commit, not a branch: the tool ships inside our + # packages, so the same xrpld version must always package the same + # validator-keys. Bump this deliberately. + set(validator_keys_commit "4c0fb75eec9601c711645998c904507e87e910ae") + message(STATUS "Using ValidatorKeys commit: ${validator_keys_commit}") FetchContent_Declare( validator_keys GIT_REPOSITORY https://github.com/ripple/validator-keys-tool.git - GIT_TAG "${current_branch}" + GIT_TAG "${validator_keys_commit}" ) FetchContent_MakeAvailable(validator_keys) + # The tool's own CMakeLists excludes the target from 'all' when it is built + # as a subproject. Undo that, so validator_keys=ON really does build it. set_target_properties( validator-keys - PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + EXCLUDE_FROM_ALL OFF + EXCLUDE_FROM_DEFAULT_BUILD OFF + ) + # We ship this binary, so like xrpld it must not keep the Nix store's ELF + # loader, or it cannot run on the target distro at all. + patch_nix_binary(validator-keys) + + configure_file( + "${validator_keys_SOURCE_DIR}/LICENSE" + "${CMAKE_BINARY_DIR}/validator-keys-LICENSE" + COPYONLY ) install(TARGETS validator-keys RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() diff --git a/package/README.md b/package/README.md index 4b78106c4c..887509b60b 100644 --- a/package/README.md +++ b/package/README.md @@ -1,6 +1,8 @@ # Linux Packaging -This directory contains all files needed to build RPM and Debian packages for `xrpld`. +This directory contains all files needed to build RPM and Debian packages for +`xrpld`. The packages also ship the `validator-keys` tool, so packaging requires +a build configured with `-Dvalidator_keys=ON`. ## Directory layout @@ -46,17 +48,28 @@ To print the full packaging matrix (artifact names and images) for the current Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call `reusable-package.yml`. That workflow generates its own packaging matrix from `package_configs` in `linux.json` (via `generate.py --packaging`) and fans out -one job per distro. Each job downloads the pre-built `xrpld` binary artifact and -runs in that distro's container, so the package format follows from the -container's package manager. The packaging script derives the package version -from the downloaded binary's `xrpld --version` output; no CMake configure or -build step is needed inside the packaging job. +one job per distro. Each job downloads the pre-built `xrpld` and `validator-keys` +binary artifacts and runs in that distro's container, so the package format +follows from the container's package manager. The packaging script derives the +package version from the downloaded binary's `xrpld --version` output; no CMake +configure or build step is needed inside the packaging job. + +The binaries come from the `debian` and `rhel` build configurations in +`linux.json`'s `configs` section, which pass `-Dvalidator_keys=ON` so that the +build job produces `validator-keys` next to `xrpld` and uploads it as the +`validator-keys-` artifact. The packaging entry for a distro names +both artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`), so a +packaged configuration must keep `-Dvalidator_keys=ON`. + +`validator-keys` is fetched from an exact commit pinned in +[`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given +`xrpld` version always packages the same tool; bump that commit deliberately. ### Locally (mirrors CI) -With an `xrpld` binary already built at `build/xrpld`, run the packaging step -inside the same container CI uses. The image tag is derived from `linux.json` -so you don't need to hardcode a SHA. +With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and +`build/validator-keys`, run the packaging step inside the same container CI uses. +The image tag is derived from `linux.json` so you don't need to hardcode a SHA. ```bash # From the repo root. Each distro's container image is the `image` field of its @@ -87,6 +100,7 @@ needed, but the host toolchain replaces the pinned CI image: ```bash cmake \ -Dxrpld=ON \ + -Dvalidator_keys=ON \ -Dpkg_release=1 \ -Dtests=OFF \ .. @@ -95,9 +109,11 @@ cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL ``` The `cmake/XrplPackaging.cmake` module defines the `package` target only if at -least one of `rpmbuild` / `dpkg-buildpackage` is present; `build_pkg.sh` then -infers the package format from the host's package manager. The packaging script -installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of +least one of `rpmbuild` / `dpkg-buildpackage` is present and both the `xrpld` and +`validator-keys` targets exist (`-Dxrpld=ON -Dvalidator_keys=ON`); the target +builds both binaries before packaging. `build_pkg.sh` then infers the package +format from the host's package manager. The packaging script installs to +FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of `CMAKE_INSTALL_PREFIX`. The package version is not a CMake input on this path: `build_pkg.sh` derives it @@ -156,13 +172,17 @@ CMake/CI integration. The CI workflow and the CMake `package` target both invoke and lets the script use defaults for the rest. It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls -`stage_common()` to copy the binary, config files, and shared support files -into the staging area, and invokes the platform build tool. +`stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files, +and shared support files into the staging area, and invokes the platform build +tool. Both binaries must be present in `BUILD_DIR` and must run in the packaging +environment; a missing or non-runnable one fails early. That runtime check is +what catches a binary still linked against the Nix store's ELF loader (see +`patch_nix_binary` in `cmake/PatchNixBinary.cmake`). ### RPM 1. Creates the standard `rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}` tree inside the build directory. -2. Copies `xrpld.spec` and all shared source files (binary, configs, service files) into `SOURCES/`. +2. Copies `xrpld.spec` and all shared source files (binaries, configs, service files) into `SOURCES/`. 3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the `pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro. The spec uses manual `install` commands to place files, disables `dwz`, and @@ -182,7 +202,8 @@ service restart. ### DEB 1. Creates a staging source tree at `debbuild/source/` inside the build directory. -2. Stages the binary, configs, `README.md`, and `LICENSE.md`. +2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and + `validator-keys-LICENSE`. 3. Copies `package/debian/` control files into `debbuild/source/debian/`. 4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically. 5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`, diff --git a/package/build_pkg.sh b/package/build_pkg.sh index 3684fc096a..d853bf95b7 100755 --- a/package/build_pkg.sh +++ b/package/build_pkg.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -# Build an RPM or Debian package from a pre-built xrpld binary. +# Build an RPM or Debian package from the pre-built xrpld and validator-keys +# binaries. # # Flags override env vars; env vars override defaults. @@ -11,7 +12,9 @@ Usage: build_pkg.sh [options] Options (each can also be set via the env var shown): --src-dir DIR repo root [SRC_DIR; default: ${PWD}] - --build-dir DIR directory holding xrpld [BUILD_DIR; default: ${PWD}/build] + --build-dir DIR directory holding the + xrpld and validator-keys + binaries [BUILD_DIR; default: ${PWD}/build] --pkg-release N package release iteration [PKG_RELEASE; default: 1] --source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; latest git ctime; fallback: current time] -h, --help show this help and exit @@ -69,15 +72,44 @@ SRC_DIR="$(cd "${SRC_DIR:-${PWD}}" && pwd)" BUILD_DIR="${BUILD_DIR:-${PWD}/build}" if [[ ! -d "${BUILD_DIR}" ]]; then echo "build_pkg.sh: build directory not found: ${BUILD_DIR}" >&2 - echo "Build xrpld before packaging, or set BUILD_DIR to the directory containing xrpld." >&2 + echo "Build the binaries before packaging, or set BUILD_DIR to the directory containing them." >&2 exit 1 fi BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)" xrpld_binary="${BUILD_DIR}/xrpld" -if [[ ! -x "${xrpld_binary}" ]]; then - echo "build_pkg.sh: expected executable xrpld binary at ${xrpld_binary}." >&2 - echo "Build xrpld before packaging, or set BUILD_DIR to the directory containing xrpld." >&2 +validator_keys_binary="${BUILD_DIR}/validator-keys" + +# Report both binaries at once: they share a single BUILD_DIR, so telling the +# reader to point it at one of them in isolation is advice they cannot follow. +missing=() +[[ -x "${xrpld_binary}" ]] || missing+=(xrpld) +[[ -x "${validator_keys_binary}" ]] || missing+=(validator-keys) + +if [[ ${#missing[@]} -gt 0 ]]; then + echo "build_pkg.sh: missing or not executable in ${BUILD_DIR}: ${missing[*]}" >&2 + echo "Both binaries come from a single CMake build directory configured with" >&2 + echo "-Dxrpld=ON -Dvalidator_keys=ON. Build them, then point BUILD_DIR at that" >&2 + echo "directory." >&2 + exit 1 +fi + +# Shipping validator-keys means shipping its notice, so treat it as required +# rather than letting a package go out without the attribution. +validator_keys_license="${BUILD_DIR}/validator-keys-LICENSE" +if [[ ! -f "${validator_keys_license}" ]]; then + echo "build_pkg.sh: missing ${validator_keys_license}." >&2 + echo "cmake/XrplValidatorKeys.cmake copies it out of the fetched" >&2 + echo "validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON." >&2 + exit 1 +fi + +# The binary must also *run* here. Packaging happens in a vanilla distro +# container, so this is what catches a binary still pointing at the Nix store's +# ELF loader (see patch_nix_binary in cmake/PatchNixBinary.cmake); xrpld is +# covered implicitly by the version query below. +if ! "${validator_keys_binary}" --version >/dev/null; then + echo "build_pkg.sh: ${validator_keys_binary} exists but does not run here." >&2 exit 1 fi @@ -150,7 +182,9 @@ stage_common() { local dest="$1" mkdir -p "${dest}" - cp "${BUILD_DIR}/xrpld" "${dest}/xrpld" + cp "${xrpld_binary}" "${dest}/xrpld" + cp "${validator_keys_binary}" "${dest}/validator-keys" + cp "${validator_keys_license}" "${dest}/validator-keys-LICENSE" cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg" cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt" cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md" diff --git a/package/debian/control b/package/debian/control index 45d2acbbea..62e5d79ef1 100644 --- a/package/debian/control +++ b/package/debian/control @@ -18,6 +18,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends} Description: XRP Ledger daemon - Reference implementation of the XRP Ledger protocol. - Participates in the peer-to-peer network, processes transactions, - and maintains a local ledger copy. + xrpld is the reference implementation of the XRP Ledger protocol. It + participates in the peer-to-peer XRP Ledger network, processes + transactions, and maintains the ledger database. + This package also includes the validator-keys tool for validator key + management. diff --git a/package/debian/copyright b/package/debian/copyright index ddaa719e3a..2cf673854a 100644 --- a/package/debian/copyright +++ b/package/debian/copyright @@ -4,6 +4,25 @@ Source: https://github.com/XRPLF/rippled Files: * Copyright: 2011-present, the XRP Ledger developers +License: ISC + +Files: validator-keys +Copyright: 2016, Ripple Labs Inc. + 2011, Arthur Britto, David Schwartz, Jed McCaleb, Vinnie Falco, Bob Way, + Eric Lombrozo, Nikolaos D. Bougalis, Howard Hinnant + 2013, Raw Material Software Ltd. + 2003-2011, Christopher M. Kohlhoff + 2009-2010, Satoshi Nakamoto + 2011, The Bitcoin developers + 2003-2005, Tom Wu +License: ISC +Comment: Built from https://github.com/ripple/validator-keys-tool at the commit + pinned in cmake/XrplValidatorKeys.cmake. Besides ISC-licensed code it + incorporates work under the Boost Software License 1.0 (ASIO), the MIT/X11 + license (Bitcoin) and Tom Wu's license, whose terms require its notice to be + retained intact. The complete upstream notice is therefore shipped verbatim as + /usr/share/doc/xrpld/validator-keys-LICENSE. + License: ISC Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/package/debian/rules b/package/debian/rules index 16574bca3f..8f880b8192 100644 --- a/package/debian/rules +++ b/package/debian/rules @@ -18,6 +18,7 @@ override_dh_installsysusers: override_dh_install: install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld + install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt diff --git a/package/debian/xrpld.docs b/package/debian/xrpld.docs index b43bf86b50..77681ddc6e 100644 --- a/package/debian/xrpld.docs +++ b/package/debian/xrpld.docs @@ -1 +1,2 @@ README.md +validator-keys-LICENSE diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec index 61c2d61ec6..0e3ee2a968 100644 --- a/package/rpm/xrpld.spec +++ b/package/rpm/xrpld.spec @@ -32,6 +32,8 @@ BuildRequires: systemd-rpm-macros xrpld is the reference implementation of the XRP Ledger protocol. It participates in the peer-to-peer XRP Ledger network, processes transactions, and maintains the ledger database. +This package also includes the validator-keys tool for validator key +management. %prep : @@ -41,6 +43,7 @@ transactions, and maintains the ledger database. %install install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name} +install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt @@ -59,6 +62,8 @@ install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/lo # Docs install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md install -Dm0644 %{_sourcedir}/README.md %{buildroot}%{_docdir}/%{name}/README.md +# Upstream notice for the bundled validator-keys tool. +install -Dm0644 %{_sourcedir}/validator-keys-LICENSE %{buildroot}%{_docdir}/%{name}/validator-keys-LICENSE # Legacy compatibility for pre-FHS package layouts. # TODO: remove after rippled fully deprecated. @@ -80,11 +85,13 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || : %files %license %{_docdir}/%{name}/LICENSE.md +%license %{_docdir}/%{name}/validator-keys-LICENSE %doc %{_docdir}/%{name}/README.md %dir %{_sysconfdir}/%{name} %{_bindir}/%{name} +%{_bindir}/validator-keys %config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg %config(noreplace) %{_sysconfdir}/%{name}/validators.txt From 4eece4003d7dd19fb717977d659aec0001057d82 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 30 Jul 2026 11:34:46 -0400 Subject: [PATCH 31/68] chore: Bump version to 3.3.0-rc6 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 6bb2c40cff..87956a12fd 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc5" +char const* const versionString = "3.3.0-rc6" // clang-format on ; From 587505ef186c3dc1937570a5911caab851c467e2 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:07:57 +0100 Subject: [PATCH 32/68] fix: Bound untrusted manifest cache --- include/xrpl/server/Manifest.h | 91 +++++++++++++++-- src/libxrpl/server/Manifest.cpp | 105 ++++++++++++++++---- src/libxrpl/server/Wallet.cpp | 21 +++- src/test/app/Manifest_test.cpp | 72 ++++++++++---- src/test/app/ValidatorList_test.cpp | 59 +++++++---- src/xrpld/app/misc/detail/ValidatorList.cpp | 10 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 30 ++++-- 7 files changed, 310 insertions(+), 78 deletions(-) diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index eed1c14dae..ddd4f503fe 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -35,12 +35,15 @@ namespace xrpl { dynamically generates the signatureless form when it needs to verify the signature. - An instance of ManifestCache stores, for each trusted validator, (a) its + An instance of ManifestCache stores, for each known validator, (a) its master public key, and (b) the most senior of all valid manifests it has seen for that validator, if any. On startup, the [validator_token] config entry (which contains the manifest for this validator) is decoded and added to the manifest cache. Other manifests are added as "gossip" - received from xrpld peers. + received from xrpld peers, including ones for validators this node does not + list. Manifests for unlisted validators are capped (kMaxUntrustedCount) + so peer gossip cannot grow the cache without bound; listed validators are + not capped. Entries are never evicted, so a stored revocation is permanent. When an ephemeral key is compromised, a new signing key pair is created, along with a new manifest vouching for it (with a higher sequence number), @@ -206,7 +209,10 @@ enum class ManifestDisposition { BadEphemeralKey, /// Timely, but invalid signature - Invalid + Invalid, + + /// Unlisted and limit reached + UntrustedCapacity }; inline std::string @@ -224,11 +230,25 @@ to_string(ManifestDisposition m) return "badEphemeralKey"; case ManifestDisposition::Invalid: return "invalid"; + case ManifestDisposition::UntrustedCapacity: + return "untrustedCapacity"; default: return "unknown"; } } +/** + * Whether a manifest counts against the 'untrusted' cache cap. + * + * Passed to `ManifestCache::applyManifest` with no default, so every caller + * must choose. `Capped` is the safe, flood-resistant value; only listed or + * configured keys should use `Uncapped`. + */ +enum class ManifestRateLimitCap : std::uint8_t { + Capped, ///< Subject to the untrusted cap (unlisted peer gossip) + Uncapped ///< Bypasses the cap (listed/trusted or config manifests) +}; + class DatabaseCon; /** Remembers manifests with the highest sequence number. */ @@ -246,6 +266,38 @@ private: std::atomic seq_{0}; + /** + * Master keys of cached manifests for validators this node does not list. + * + * One entry per capped key in `map_`; its size enforces the cap below. + * A key is added when first cached under `Capped` and removed when it + * becomes listed (see `promoteToTrusted`) or an `Uncapped` update arrives, + * never re-added on de-listing. Uncapped keys are not tracked here. + */ + hash_set untrustedKeys_; + + /** + * Maximum number of untrusted master keys kept in the cache. + * + * Once reached, a manifest for a brand-new unlisted key is rejected. + */ + static constexpr std::size_t kMaxUntrustedCount = 50000; + + /** + * Running count of manifests rejected because the untrusted cap was full. + * + * Drives throttled logging (see `kUntrustedRejectCount`). Atomic because + * `applyManifest` may run concurrently. + */ + std::atomic untrustedRejectCount_{0}; + + /** + * Number of cap rejections between summary warnings. + * + * @see untrustedRejectCount_ + */ + static constexpr std::uint64_t kUntrustedRejectCount = 10000; + public: explicit ManifestCache(beast::Journal j = beast::Journal(beast::Journal::getNullSink())) : j_(j) { @@ -321,17 +373,44 @@ public: /** Add manifest to cache. + A brand-new unlisted key is rejected once the untrusted cap is full; + updates to a cached key and `Uncapped` manifests bypass the cap. The + caller decides `cap` before calling so the cache lock is not held while + consulting the validator list, which would risk a lock-ordering deadlock. + @param m Manifest to add - @return `ManifestDisposition::accepted` if successful, or - `stale` or `invalid` otherwise + @param cap `Uncapped` skips the untrusted cap; use it for keys that are + listed, configured, or loaded from the DB. Note `Uncapped` does + not assert the key is currently trusted (a DB entry may predate a + de-listing). Callers must state this explicitly so a manifest is + never left uncapped by omission. + + @return `Accepted` if stored, `Stale` if superseded, `Invalid`/ + `BadEphemeralKey` if malformed, or `UntrustedCapacity` if the + untrusted cap is full. @par Thread Safety May be called concurrently */ ManifestDisposition - applyManifest(Manifest m); + applyManifest(Manifest m, ManifestRateLimitCap cap); + + /** + * Stop counting a master key against the untrusted cap. + * + * Called when a cached untrusted key becomes listed, freeing its slot. + * Idempotent and a no-op for keys that were never counted. + * + * @param pk Master public key that is now listed/trusted + * + * @par Thread Safety + * + * May be called concurrently + */ + void + promoteToTrusted(PublicKey const& pk); /** Populate manifest cache with manifests in database and config. diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index b26c67e531..b18fdadbe2 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -377,16 +377,20 @@ ManifestCache::revoked(PublicKey const& pk) const } ManifestDisposition -ManifestCache::applyManifest(Manifest m) +ManifestCache::applyManifest(Manifest m, ManifestRateLimitCap const cap) { + bool const uncapped = cap == ManifestRateLimitCap::Uncapped; + + // The signature is checked only on the first `prewriteCheck` run (under the + // read lock). It is expensive, so `checkSignature` is cleared the first + // time it is read; the second run (under the write lock) skips it. + bool checkSignature = true; + // Check the manifest against the conditions that do not require a - // `unique_lock` (write lock) on the `mutex_`. Since the signature can be - // relatively expensive, the `checkSignature` parameter determines if the - // signature should be checked. Since `prewriteCheck` is run twice (see - // comment below), `checkSignature` only needs to be set to true on the - // first run. - auto prewriteCheck = [this, &m](auto const& iter, bool checkSignature, auto const& lock) - -> std::optional { + // `unique_lock` (write lock) on the `mutex_`. + auto prewriteCheck = [this, &m, &checkSignature]( + auto const& iter, + auto const& lock) -> std::optional { XRPL_ASSERT(lock.owns_lock(), "xrpl::ManifestCache::applyManifest::prewriteCheck : locked"); (void)lock; // not used. parameter is present to ensure the mutex is // locked when the lambda is called. @@ -401,11 +405,15 @@ ManifestCache::applyManifest(Manifest m) return ManifestDisposition::Stale; } - if (checkSignature && !m.verify()) + if (checkSignature) { - if (auto stream = j_.warn()) - logMftAct(stream, "Invalid", m.masterKey, m.sequence); - return ManifestDisposition::Invalid; + checkSignature = false; + if (!m.verify()) + { + if (auto stream = j_.warn()) + logMftAct(stream, "Invalid", m.masterKey, m.sequence); + return ManifestDisposition::Invalid; + } } // If the master key associated with a manifest is or might be @@ -465,14 +473,51 @@ ManifestCache::applyManifest(Manifest m) return std::nullopt; }; + // Reject a brand-new manifest for an unlisted key once the untrusted cap + // is full. Updates to a cached key and uncapped manifests always pass. + // Called under both the read and write lock, since the cap can be reached + // between the two. The lock param enforces that. + auto atUntrustedCap = [this, &m, uncapped](auto const& iter, auto const& lock) { + XRPL_ASSERT( + lock.owns_lock(), "xrpl::ManifestCache::applyManifest::atUntrustedCap : locked"); + (void)lock; // not used. parameter is present to ensure the mutex is + // locked when the lambda is called. + if (iter == map_.end() && !uncapped && untrustedKeys_.size() >= kMaxUntrustedCount) + { + // Log each rejection at debug, but warn only once per interval so a + // flood does not fill the log. + if (auto stream = j_.debug()) + logMftAct(stream, "UntrustedCapacity", m.masterKey, m.sequence); + if (auto const n = untrustedRejectCount_.fetch_add(1) + 1; + n % kUntrustedRejectCount == 0) + { + JLOG(j_.warn()) << "Untrusted manifest cap reached; " << n + << " manifests rejected so far"; + } + return true; + } + return false; + }; + { std::shared_lock const sl{mutex_}; - if (auto d = prewriteCheck(map_.find(m.masterKey), /*checkSig*/ true, sl)) + auto const iter = map_.find(m.masterKey); + + if (atUntrustedCap(iter, sl)) + return ManifestDisposition::UntrustedCapacity; + + if (auto d = prewriteCheck(iter, sl); d.has_value()) return *d; } std::unique_lock const sl{mutex_}; auto const iter = map_.find(m.masterKey); + + // Re-check the cap under the write lock: the cache may have grown while the + // read lock above was released. + if (atUntrustedCap(iter, sl)) + return ManifestDisposition::UntrustedCapacity; + // Since we released the previously held read lock, it's possible that the // collections have been written to. This means we need to run // `prewriteCheck` again. This re-does work, but `prewriteCheck` is @@ -482,7 +527,7 @@ ManifestCache::applyManifest(Manifest m) // doesn't need to happen again (signature checks are somewhat expensive). // Note: It's a mistake to use an upgradable lock. This is a recipe for // deadlock. - if (auto d = prewriteCheck(iter, /*checkSig*/ false, sl)) + if (auto d = prewriteCheck(iter, sl); d.has_value()) return *d; bool const revoked = m.revoked(); @@ -501,6 +546,12 @@ ManifestCache::applyManifest(Manifest m) } auto masterKey = m.masterKey; + + // Count this key against the untrusted cap. Uncapped keys (listed, + // configured, or DB-loaded) are not tracked. + if (!uncapped) + untrustedKeys_.insert(masterKey); + map_.emplace(std::move(masterKey), std::move(m)); // Something has changed. Keep track of it. @@ -514,6 +565,11 @@ ManifestCache::applyManifest(Manifest m) if (auto stream = j_.info()) logMftAct(stream, "AcceptedUpdate", m.masterKey, m.sequence, iter->second.sequence); + // If this key was counted against the cap but now arrives uncapped, free + // its slot without waiting for promoteToTrusted. + if (uncapped) + untrustedKeys_.erase(m.masterKey); + signingToMasterKeys_.erase( *iter->second.signingKey); // NOLINT(bugprone-unchecked-optional-access) prewriteCheck // ensures old manifest is not revoked @@ -521,8 +577,8 @@ ManifestCache::applyManifest(Manifest m) if (!revoked) { signingToMasterKeys_.emplace( - *m.signingKey, m.masterKey); // NOLINT(bugprone-unchecked-optional-access) non-revoked - // manifest always has signingKey + *m.signingKey, m.masterKey); // NOLINT(bugprone-unchecked-optional-access) + // non-revoked manifest always has signingKey } iter->second = std::move(m); @@ -533,6 +589,16 @@ ManifestCache::applyManifest(Manifest m) return ManifestDisposition::Accepted; } +void +ManifestCache::promoteToTrusted(PublicKey const& pk) +{ + // Frees the key's untrusted slot; a no-op (and idempotent) if the key was + // never counted. Not re-added on de-listing, so list/de-list cannot grow + // the count. + std::unique_lock const sl{mutex_}; + untrustedKeys_.erase(pk); +} + void ManifestCache::load(DatabaseCon& dbCon, std::string const& dbTable) { @@ -563,7 +629,8 @@ ManifestCache::load( JLOG(j_.warn()) << "Configured manifest revokes public key"; } - if (applyManifest(std::move(*mo)) == ManifestDisposition::Invalid) + if (applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + ManifestDisposition::Invalid) { JLOG(j_.error()) << "Manifest in config was rejected"; return false; @@ -585,7 +652,9 @@ ManifestCache::load( auto mo = deserializeManifest(base64Decode(revocationStr)); - if (!mo || !mo->revoked() || applyManifest(std::move(*mo)) == ManifestDisposition::Invalid) + if (!mo || !mo->revoked() || + applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + ManifestDisposition::Invalid) { JLOG(j_.error()) << "Invalid validator key revocation in config"; return false; diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp index f3ae9dc925..f3ef3cf832 100644 --- a/src/libxrpl/server/Wallet.cpp +++ b/src/libxrpl/server/Wallet.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -75,7 +76,9 @@ getManifests( continue; } - cache.applyManifest(std::move(*mo)); + // Only trusted manifests are persisted (see saveManifests), so + // anything loaded from the DB bypasses the untrusted cap. + cache.applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped); } else { @@ -105,19 +108,27 @@ saveManifests( { soci::transaction tr(session); session << "DELETE FROM " << dbTable; + // Count skipped untrusted manifests and log one summary afterwards, since + // the cache can hold many and per-entry logging would flood at shutdown. + std::size_t skipped = 0; for (auto const& v : map) { - // Save all revocation manifests, - // but only save trusted non-revocation manifests. - if (!v.second.revoked() && !isTrusted(v.second.masterKey)) + // Persist only trusted keys. Untrusted gossip is left out so a flood + // cannot survive a restart on disk. + if (!isTrusted(v.second.masterKey)) { - JLOG(j.info()) << "Untrusted manifest in cache not saved to db"; + ++skipped; continue; } saveManifest(session, dbTable, v.second.serialized); } tr.commit(); + + if (skipped != 0) + { + JLOG(j.info()) << skipped << " untrusted manifest(s) in cache not saved to db"; + } } void diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index d559ecd7b5..c1c55c4a5d 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -399,7 +399,8 @@ public: BEAST_EXPECT( ManifestDisposition::Accepted == cache.applyManifest( - makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0))); + makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0), + ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first); BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk); @@ -411,7 +412,8 @@ public: BEAST_EXPECT( ManifestDisposition::Accepted == cache.applyManifest( - makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1))); + makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1), + ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -421,7 +423,8 @@ public: BEAST_EXPECT( ManifestDisposition::BadEphemeralKey == cache.applyManifest( - makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2))); + makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2), + ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -431,7 +434,8 @@ public: // key from a revoked master public key BEAST_EXPECT( ManifestDisposition::Accepted == - cache.applyManifest(makeRevocation(sk, KeyType::Ed25519))); + cache.applyManifest( + makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCap::Capped)); BEAST_EXPECT(cache.revoked(pk)); BEAST_EXPECT(cache.getSigningKey(pk) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -902,39 +906,69 @@ public: // applyManifest should accept new manifests with // higher sequence numbers auto const seq0 = cache.sequence(); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); BEAST_EXPECT(cache.sequence() > seq0); auto const seq1 = cache.sequence(); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); BEAST_EXPECT(cache.sequence() == seq1); - BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Accepted); - BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA2)) == ManifestDisposition::BadEphemeralKey); + BEAST_EXPECT( + cache.applyManifest(clone(sA2), ManifestRateLimitCap::Capped) == + ManifestDisposition::BadEphemeralKey); // applyManifest should accept manifests with max sequence numbers // that revoke the master public key BEAST_EXPECT(!cache.revoked(pkA)); BEAST_EXPECT(sAMax.revoked()); - BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Accepted); - BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale); - BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); BEAST_EXPECT(cache.revoked(pkA)); // applyManifest should reject manifests with invalid signatures - BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Accepted); - BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Stale); + BEAST_EXPECT( + cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + ManifestDisposition::Stale); BEAST_EXPECT(!deserializeManifest(fake)); - BEAST_EXPECT(cache.applyManifest(clone(sB1)) == ManifestDisposition::Invalid); - BEAST_EXPECT(cache.applyManifest(clone(sB2)) == ManifestDisposition::Accepted); + BEAST_EXPECT( + cache.applyManifest(clone(sB1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Invalid); + BEAST_EXPECT( + cache.applyManifest(clone(sB2), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); auto const sC0 = makeManifest( kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47); - BEAST_EXPECT(cache.applyManifest(clone(sC0)) == ManifestDisposition::BadMasterKey); + BEAST_EXPECT( + cache.applyManifest(clone(sC0), ManifestRateLimitCap::Capped) == + ManifestDisposition::BadMasterKey); } testLoadStore(cache); diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 20a3557db5..3fede86637 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -277,8 +277,10 @@ private: trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers)); BEAST_EXPECT(trustedKeys->listed(localSigningPublicOuter)); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifests.applyManifest(*deserializeManifest(cfgManifest)); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + manifests.applyManifest( + *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT( trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers)); @@ -368,8 +370,10 @@ private: app.config().legacy("database_path"), env.journal); - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifests.applyManifest(*deserializeManifest(cfgManifest)); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + manifests.applyManifest( + *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT(trustedKeys->load(localSigningPublicOuter, cfgKeys, emptyCfgPublishers)); @@ -454,13 +458,16 @@ private: auto const pubRevokedSigning = randomKeyPair(KeyType::Secp256k1); // make this manifest revoked (seq num = max) // -- thus should not be loaded - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - pubManifests.applyManifest(*deserializeManifest(makeManifestString( - pubRevokedPublic, - pubRevokedSecret, - pubRevokedSigning.first, - pubRevokedSigning.second, - std::numeric_limits::max()))); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + pubManifests.applyManifest( + *deserializeManifest(makeManifestString( + pubRevokedPublic, + pubRevokedSecret, + pubRevokedSigning.first, + pubRevokedSigning.second, + std::numeric_limits::max())), + ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) // these two are not revoked (and not in the manifest cache at all.) auto legitKey1 = randomMasterKey(); @@ -493,13 +500,16 @@ private: auto const pubRevokedSigning = randomKeyPair(KeyType::Secp256k1); // make this manifest revoked (seq num = max) // -- thus should not be loaded - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - pubManifests.applyManifest(*deserializeManifest(makeManifestString( - pubRevokedPublic, - pubRevokedSecret, - pubRevokedSigning.first, - pubRevokedSigning.second, - std::numeric_limits::max()))); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + pubManifests.applyManifest( + *deserializeManifest(makeManifestString( + pubRevokedPublic, + pubRevokedSecret, + pubRevokedSigning.first, + pubRevokedSigning.second, + std::numeric_limits::max())), + ManifestRateLimitCap::Capped); + // NOLINTEND(bugprone-unchecked-optional-access) // this one is not revoked (and not in the manifest cache at all.) auto legitKey = randomMasterKey(); @@ -1164,7 +1174,8 @@ private: BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m1)) == ManifestDisposition::Accepted); + manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); BEAST_EXPECT(trustedKeysOuter->listed(signingPublic1)); @@ -1178,7 +1189,8 @@ private: masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2)); BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m2)) == ManifestDisposition::Accepted); + manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); BEAST_EXPECT(trustedKeysOuter->listed(signingPublic2)); @@ -1195,7 +1207,8 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) BEAST_EXPECT(max->revoked()); BEAST_EXPECT( - manifestsOuter.applyManifest(std::move(*max)) == ManifestDisposition::Accepted); + manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCap::Capped) == + ManifestDisposition::Accepted); // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT(manifestsOuter.getSigningKey(masterPublic) == masterPublic); @@ -2612,7 +2625,9 @@ private: auto threshold = listThreshold > 0 ? std::optional(listThreshold) : std::nullopt; if (self) { - valManifests.applyManifest(*deserializeManifest(base64Decode(self->manifest))); + valManifests.applyManifest( + *deserializeManifest(base64Decode(self->manifest)), + ManifestRateLimitCap::Capped); BEAST_EXPECT( result->load(self->signingPublic, emptyCfgKeys, cfgPublishers, threshold)); } diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 57b65814e1..ce07e0ec77 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1065,6 +1065,8 @@ ValidatorList::updatePublisherList( { // Increment list count for added keys ++keyListings_[*iNew]; + // Key is now listed: free its untrusted slot if it had one. + validatorManifests_.promoteToTrusted(*iNew); ++iNew; } else if (iNew == publisherList.end() || (iOld != oldList.end() && *iOld < *iNew)) @@ -1103,7 +1105,8 @@ ValidatorList::updatePublisherList( continue; } - if (auto const r = validatorManifests_.applyManifest(std::move(*m)); + if (auto const r = + validatorManifests_.applyManifest(std::move(*m), ManifestRateLimitCap::Uncapped); r == ManifestDisposition::Invalid) { JLOG(j_.warn()) << "List for " << strHex(pubKey) @@ -1348,7 +1351,10 @@ ValidatorList::verify( PublicKey masterPubKey = manifest.masterKey; auto const revoked = manifest.revoked(); - auto const result = publisherManifests_.applyManifest(std::move(manifest)); + // Publisher keys are configured/trusted (checked above), so bypass the + // untrusted cap. + auto const result = + publisherManifests_.applyManifest(std::move(manifest), ManifestRateLimitCap::Uncapped); if (revoked && result == ManifestDisposition::Accepted) { diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index b31f54058a..452d0f0339 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -673,13 +673,22 @@ OverlayImpl::onManifests( if (auto mo = deserializeManifest(s)) { auto const serialized = mo->serialized; + // Resolve trust before applyManifest takes the manifest-cache + // lock: listed() takes the validator-list lock, so ordering it + // first avoids holding the two locks in opposite orders. + bool const isTrusted = app_.getValidators().listed(mo->masterKey); + // Updates to a known key are relayed even when untrusted. Use + // getSequence, not getManifest, to avoid copying the cached payload + // on this hot path. + bool const isKnown = + app_.getValidatorManifests().getSequence(mo->masterKey).has_value(); - auto const result = app_.getValidatorManifests().applyManifest(std::move(*mo)); + auto const result = app_.getValidatorManifests().applyManifest( + std::move(*mo), + isTrusted ? ManifestRateLimitCap::Uncapped : ManifestRateLimitCap::Capped); if (result == ManifestDisposition::Accepted) { - relay.add_list()->set_stobject(s); - // N.B.: this is important; the applyManifest call above moves // the loaded Manifest out of the optional so we need to // reload it here. @@ -691,10 +700,19 @@ OverlayImpl::onManifests( // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above app_.getOPs().pubManifest(*mo); - if (app_.getValidators().listed(mo->masterKey)) + // Relay only trusted manifests or updates to known keys, so + // untrusted gossip for a brand-new key cannot be amplified. + // Persist to the wallet DB only for trusted keys, so untrusted + // gossip never survives a restart. + if (isTrusted || isKnown) { - auto db = app_.getWalletDB().checkoutDb(); - addValidatorManifest(*db, serialized); + relay.add_list()->set_stobject(s); + + if (isTrusted) + { + auto db = app_.getWalletDB().checkoutDb(); + addValidatorManifest(*db, serialized); + } } // NOLINTEND(bugprone-unchecked-optional-access) } From 32a9cc4038e62ee87c7a2eb5e03f1f028507a8a7 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:27:45 +0100 Subject: [PATCH 33/68] fix: Reduce untrusted manifest cache cap to 100 --- include/xrpl/server/Manifest.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index ddd4f503fe..70fc8f6f8f 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -281,7 +281,7 @@ private: * * Once reached, a manifest for a brand-new unlisted key is rejected. */ - static constexpr std::size_t kMaxUntrustedCount = 50000; + static constexpr std::size_t kMaxUntrustedCount = 100; /** * Running count of manifests rejected because the untrusted cap was full. From 0cce5a06d994cb1c45419e2b7016843ba5817748 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 16 Jul 2026 16:21:56 -0400 Subject: [PATCH 34/68] fix: Reject oversized validator manifest before decoding --- include/xrpl/basics/base64.h | 29 ++++++++++++ include/xrpl/server/Manifest.h | 52 ++++++++++++++++++--- src/libxrpl/basics/base64.cpp | 14 ------ src/libxrpl/server/Manifest.cpp | 5 ++ src/xrpld/app/misc/detail/ValidatorList.cpp | 9 ++++ 5 files changed, 88 insertions(+), 21 deletions(-) diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h index ed30e40a36..6b4cd26604 100644 --- a/include/xrpl/basics/base64.h +++ b/include/xrpl/basics/base64.h @@ -39,6 +39,35 @@ namespace xrpl { +namespace base64 { + +/** + * Returns the maximum number of characters needed to base64-encode @p nBytes bytes. + * + * @param nBytes Number of input bytes. + * @return Size of the encoded string, including padding. + */ +constexpr std::size_t +encodedSize(std::size_t const nBytes) +{ + return 4 * ((nBytes + 2) / 3); +} + +/** + * Returns the maximum number of bytes a base64 string of @p nChars characters + * decodes to. + * + * @param nChars Number of base64 characters. + * @return Upper bound on the number of decoded bytes. + */ +constexpr std::size_t +decodedSize(std::size_t const nChars) +{ + return ((nChars / 4) * 3) + 2; +} + +} // namespace base64 + std::string base64Encode(std::uint8_t const* data, std::size_t len); diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 70fc8f6f8f..fe5f1db985 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -1,10 +1,16 @@ #pragma once #include +#include +#include #include #include #include +#include +#include +#include +#include #include #include #include @@ -135,15 +141,47 @@ struct Manifest std::string to_string(Manifest const& m); -/** Constructs Manifest from serialized string +/** + *Largest a valid manifest can be, in decoded bytes. + * + * A manifest has a fixed set of fields. Each is serialized as a field header + * (1-2 bytes), an optional length prefix (1 byte for these sizes), and the + * field body. Taking every field at its largest gives the maximum below, so + * anything larger cannot be a valid manifest. + * + * Field header + length + body = bytes + * sfVersion (U16) 2 0 2 4 + * sfSequence (U32) 1 0 4 5 + * sfPublicKey (33) 1 1 33 35 + * sfSigningPubKey (33) 1 1 33 35 + * sfSignature (72) 1 1 72 74 + * sfMasterSignature (72) 2 1 72 75 + * sfDomain (128) 1 1 128 130 + * ----- + * 358 + */ +constexpr std::size_t kMaxManifestBytes = 358; - @param s Serialized manifest string +/** + * Largest a valid manifest can be, in base64 characters. + * + * base64 encodes 3 bytes as 4 characters, so this is the encoded form of + * @ref kMaxManifestBytes. Callers that receive a base64 manifest should + * reject anything longer than this before decoding, to avoid allocating + * memory for an oversized input. + */ +constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes); - @return `std::nullopt` if string is invalid - - @note This does not verify manifest signatures. - `Manifest::verify` should be called after constructing manifest. -*/ +/** + * Constructs Manifest from serialized string + * + * @param s Serialized manifest string + * + * @return `std::nullopt` if string is invalid + * + * @note This does not verify manifest signatures. + * `Manifest::verify` should be called after constructing manifest. + */ /** @{ */ std::optional deserializeManifest(Slice s, beast::Journal journal); diff --git a/src/libxrpl/basics/base64.cpp b/src/libxrpl/basics/base64.cpp index 541ddd0839..7772752f40 100644 --- a/src/libxrpl/basics/base64.cpp +++ b/src/libxrpl/basics/base64.cpp @@ -76,20 +76,6 @@ getInverse() return &kTab[0]; } -/// Returns max chars needed to encode a base64 string -constexpr std::size_t -encodedSize(std::size_t n) -{ - return 4 * ((n + 2) / 3); -} - -/// Returns max bytes needed to decode a base64 string -constexpr std::size_t -decodedSize(std::size_t n) -{ - return ((n / 4) * 3) + 2; -} - /** Encode a series of octets as a padded, base64 string. The resulting string will not be null terminated. diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index b18fdadbe2..b34955dc28 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -62,6 +62,11 @@ deserializeManifest(Slice s, beast::Journal journal) if (s.empty()) return std::nullopt; + // A valid manifest has a fixed maximum size, so reject anything larger + // before parsing it. + if (s.size() > kMaxManifestBytes) + return std::nullopt; + static SOTemplate const kManifestFormat{ // A manifest must include: // - the master public key diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index ce07e0ec77..5c95eb553c 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1130,6 +1130,15 @@ ValidatorList::applyList( json::Value list; auto const& manifest = localManifest ? *localManifest : globalManifest; + // Reject an oversized manifest before decoding it, so we do not allocate + // memory for an input that cannot be a valid manifest. deserializeManifest + // also enforces the decoded-byte limit, but checking here avoids the + // base64 decode entirely. + if (manifest.size() > kMaxManifestBase64) + { + JLOG(j_.warn()) << "UNL manifest exceeds maximum size"; + return PublisherListStats{ListDisposition::Invalid}; + } auto m = deserializeManifest(base64Decode(manifest)); if (!m) { From 4bd1d1ca2f01952b9ef533bc4bd8abc2046bfce8 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:18:28 +0100 Subject: [PATCH 35/68] fix: Cap untrusted manifests per message and drop oversized ones Bound the number of manifests carried in a single TMManifests message (kMaxManifestsPerMessage). Trusted manifests are always included and processed; untrusted gossip is capped per message on both send and receive, and the sender is charged only when untrusted entries are actually skipped. Oversized TMManifests messages are dropped without penalty at the protocol layer so an unpatched peer is not disconnected. Complements the cache bound from #276/#323. --- include/xrpl/basics/base64.h | 8 +- include/xrpl/server/Manifest.h | 45 ++++++---- src/libxrpl/server/Manifest.cpp | 8 +- src/libxrpl/server/Wallet.cpp | 2 +- src/test/app/Manifest_test.cpp | 38 ++++----- src/test/app/ValidatorList_test.cpp | 16 ++-- src/xrpld/app/misc/detail/ValidatorList.cpp | 8 +- src/xrpld/overlay/Message.h | 8 ++ src/xrpld/overlay/detail/OverlayImpl.cpp | 95 +++++++++++++++++++-- src/xrpld/overlay/detail/PeerImp.cpp | 4 + src/xrpld/overlay/detail/ProtocolMessage.h | 10 +++ 11 files changed, 178 insertions(+), 64 deletions(-) diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h index 6b4cd26604..34b2cbc5a0 100644 --- a/include/xrpl/basics/base64.h +++ b/include/xrpl/basics/base64.h @@ -54,16 +54,16 @@ encodedSize(std::size_t const nBytes) } /** - * Returns the maximum number of bytes a base64 string of @p nChars characters + * Returns the maximum number of bytes a base64 string of @p numChars characters * decodes to. * - * @param nChars Number of base64 characters. + * @param numChars Number of base64 characters. * @return Upper bound on the number of decoded bytes. */ constexpr std::size_t -decodedSize(std::size_t const nChars) +decodedSize(std::size_t const numChars) { - return ((nChars / 4) * 3) + 2; + return ((numChars / 4) * 3) + 2; } } // namespace base64 diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index fe5f1db985..eec07ac3ee 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -47,8 +47,8 @@ namespace xrpl { entry (which contains the manifest for this validator) is decoded and added to the manifest cache. Other manifests are added as "gossip" received from xrpld peers, including ones for validators this node does not - list. Manifests for unlisted validators are capped (kMaxUntrustedCount) - so peer gossip cannot grow the cache without bound; listed validators are + trust. Manifests for untrusted validators are capped (kMaxUntrustedCount) + so peer gossip cannot grow the cache without bound; trusted validators are not capped. Entries are never evicted, so a stored revocation is permanent. When an ephemeral key is compromised, a new signing key pair is created, @@ -149,7 +149,7 @@ to_string(Manifest const& m); * field body. Taking every field at its largest gives the maximum below, so * anything larger cannot be a valid manifest. * - * Field header + length + body = bytes + * Field header + length + body = bytes * sfVersion (U16) 2 0 2 4 * sfSequence (U32) 1 0 4 5 * sfPublicKey (33) 1 1 33 35 @@ -172,16 +172,31 @@ constexpr std::size_t kMaxManifestBytes = 358; */ constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes); -/** - * Constructs Manifest from serialized string - * - * @param s Serialized manifest string - * - * @return `std::nullopt` if string is invalid - * - * @note This does not verify manifest signatures. - * `Manifest::verify` should be called after constructing manifest. - */ +/** Maximum number of manifests carried in a single TMManifests message. + + Outbound, the TMManifests message sent to a peer includes every trusted + manifest and fills the rest of this budget with untrusted gossip, so it + never exceeds this size. Inbound, trusted manifests are always processed + and untrusted ones are processed up to this many, so a peer sending its + whole cache cannot force unbounded work. + + The trusted set is tiny relative to this bound, so trusted manifests are + not dropped in practice. This is a transitional per-message cap; the cache + already bounds untrusted manifests (see kMaxUntrustedCount), so it is no + longer needed once the network has upgraded past nodes that send their + whole cache in one message. +*/ +constexpr std::size_t kMaxManifestsPerMessage = 200; + +/** Constructs Manifest from serialized string + + @param s Serialized manifest string + + @return `std::nullopt` if string is invalid + + @note This does not verify manifest signatures. + `Manifest::verify` should be called after constructing manifest. +*/ /** @{ */ std::optional deserializeManifest(Slice s, beast::Journal journal); @@ -282,7 +297,7 @@ to_string(ManifestDisposition m) * must choose. `Capped` is the safe, flood-resistant value; only listed or * configured keys should use `Uncapped`. */ -enum class ManifestRateLimitCap : std::uint8_t { +enum class ManifestRateLimitCapPolicy : std::uint8_t { Capped, ///< Subject to the untrusted cap (unlisted peer gossip) Uncapped ///< Bypasses the cap (listed/trusted or config manifests) }; @@ -433,7 +448,7 @@ public: May be called concurrently */ ManifestDisposition - applyManifest(Manifest m, ManifestRateLimitCap cap); + applyManifest(Manifest m, ManifestRateLimitCapPolicy cap); /** * Stop counting a master key against the untrusted cap. diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index b34955dc28..3da3f9e9cd 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -382,9 +382,9 @@ ManifestCache::revoked(PublicKey const& pk) const } ManifestDisposition -ManifestCache::applyManifest(Manifest m, ManifestRateLimitCap const cap) +ManifestCache::applyManifest(Manifest m, ManifestRateLimitCapPolicy const cap) { - bool const uncapped = cap == ManifestRateLimitCap::Uncapped; + bool const uncapped = cap == ManifestRateLimitCapPolicy::Uncapped; // The signature is checked only on the first `prewriteCheck` run (under the // read lock). It is expensive, so `checkSignature` is cleared the first @@ -634,7 +634,7 @@ ManifestCache::load( JLOG(j_.warn()) << "Configured manifest revokes public key"; } - if (applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + if (applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped) == ManifestDisposition::Invalid) { JLOG(j_.error()) << "Manifest in config was rejected"; @@ -658,7 +658,7 @@ ManifestCache::load( auto mo = deserializeManifest(base64Decode(revocationStr)); if (!mo || !mo->revoked() || - applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped) == ManifestDisposition::Invalid) { JLOG(j_.error()) << "Invalid validator key revocation in config"; diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp index f3ef3cf832..ac5f0ace76 100644 --- a/src/libxrpl/server/Wallet.cpp +++ b/src/libxrpl/server/Wallet.cpp @@ -78,7 +78,7 @@ getManifests( // Only trusted manifests are persisted (see saveManifests), so // anything loaded from the DB bypasses the untrusted cap. - cache.applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped); + cache.applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped); } else { diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index c1c55c4a5d..9218b073d4 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -400,7 +400,7 @@ public: ManifestDisposition::Accepted == cache.applyManifest( makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0), - ManifestRateLimitCap::Capped)); + ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first); BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk); @@ -413,7 +413,7 @@ public: ManifestDisposition::Accepted == cache.applyManifest( makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1), - ManifestRateLimitCap::Capped)); + ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -424,7 +424,7 @@ public: ManifestDisposition::BadEphemeralKey == cache.applyManifest( makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2), - ManifestRateLimitCap::Capped)); + ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -435,7 +435,7 @@ public: BEAST_EXPECT( ManifestDisposition::Accepted == cache.applyManifest( - makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCap::Capped)); + makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.revoked(pk)); BEAST_EXPECT(cache.getSigningKey(pk) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -907,28 +907,28 @@ public: // higher sequence numbers auto const seq0 = cache.sequence(); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT(cache.sequence() > seq0); auto const seq1 = cache.sequence(); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT(cache.sequence() == seq1); BEAST_EXPECT( - cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT( - cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA2), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA2), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::BadEphemeralKey); // applyManifest should accept manifests with max sequence numbers @@ -936,38 +936,38 @@ public: BEAST_EXPECT(!cache.revoked(pkA)); BEAST_EXPECT(sAMax.revoked()); BEAST_EXPECT( - cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sAMax), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT( - cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sAMax), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT(cache.revoked(pkA)); // applyManifest should reject manifests with invalid signatures BEAST_EXPECT( - cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT( - cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT(!deserializeManifest(fake)); BEAST_EXPECT( - cache.applyManifest(clone(sB1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Invalid); BEAST_EXPECT( - cache.applyManifest(clone(sB2), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB2), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); auto const sC0 = makeManifest( kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47); BEAST_EXPECT( - cache.applyManifest(clone(sC0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sC0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::BadMasterKey); } diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 3fede86637..d39789f183 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -279,7 +279,7 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) manifests.applyManifest( - *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + *deserializeManifest(cfgManifest), ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT( trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers)); @@ -372,7 +372,7 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) manifests.applyManifest( - *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + *deserializeManifest(cfgManifest), ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT(trustedKeys->load(localSigningPublicOuter, cfgKeys, emptyCfgPublishers)); @@ -466,7 +466,7 @@ private: pubRevokedSigning.first, pubRevokedSigning.second, std::numeric_limits::max())), - ManifestRateLimitCap::Capped); + ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) // these two are not revoked (and not in the manifest cache at all.) @@ -508,7 +508,7 @@ private: pubRevokedSigning.first, pubRevokedSigning.second, std::numeric_limits::max())), - ManifestRateLimitCap::Capped); + ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) // this one is not revoked (and not in the manifest cache at all.) @@ -1174,7 +1174,7 @@ private: BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCap::Capped) == + manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); @@ -1189,7 +1189,7 @@ private: masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2)); BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCap::Capped) == + manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); @@ -1207,7 +1207,7 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) BEAST_EXPECT(max->revoked()); BEAST_EXPECT( - manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCap::Capped) == + manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); // NOLINTEND(bugprone-unchecked-optional-access) @@ -2627,7 +2627,7 @@ private: { valManifests.applyManifest( *deserializeManifest(base64Decode(self->manifest)), - ManifestRateLimitCap::Capped); + ManifestRateLimitCapPolicy::Capped); BEAST_EXPECT( result->load(self->signingPublic, emptyCfgKeys, cfgPublishers, threshold)); } diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 5c95eb553c..0e29149e9c 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1105,8 +1105,8 @@ ValidatorList::updatePublisherList( continue; } - if (auto const r = - validatorManifests_.applyManifest(std::move(*m), ManifestRateLimitCap::Uncapped); + if (auto const r = validatorManifests_.applyManifest( + std::move(*m), ManifestRateLimitCapPolicy::Uncapped); r == ManifestDisposition::Invalid) { JLOG(j_.warn()) << "List for " << strHex(pubKey) @@ -1362,8 +1362,8 @@ ValidatorList::verify( // Publisher keys are configured/trusted (checked above), so bypass the // untrusted cap. - auto const result = - publisherManifests_.applyManifest(std::move(manifest), ManifestRateLimitCap::Uncapped); + auto const result = publisherManifests_.applyManifest( + std::move(manifest), ManifestRateLimitCapPolicy::Uncapped); if (revoked && result == ManifestDisposition::Accepted) { diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h index cd21ca40c6..30c30a5f2c 100644 --- a/src/xrpld/overlay/Message.h +++ b/src/xrpld/overlay/Message.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -13,6 +14,13 @@ namespace xrpl { constexpr std::size_t kMaximumMessageSize = megabytes(64); +// Upper bound on the wire size of a TMManifests message: kMaxManifestsPerMessage entries +// of at most kMaxManifestBytes each, plus a small allowance for protobuf +// framing per entry. +constexpr std::size_t kManifestFramingBytes = 8; +constexpr std::size_t kMaximumManifestsMessageSize = + kMaxManifestsPerMessage * (kMaxManifestBytes + kManifestFramingBytes); + // VFALCO NOTE If we forward declare Message and write out shared_ptr // instead of using the in-class type alias, we can remove the // entire ripple.pb.h from the main headers. diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 452d0f0339..a82f7a286d 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -661,12 +662,17 @@ OverlayImpl::onManifests( std::shared_ptr const& m, std::shared_ptr const& from) { - auto const n = m->list_size(); auto const& journal = from->pJournal(); + // Process every trusted manifest, but stop processing untrusted ones once + // kMaxManifestsPerMessage of them have been handled, so the work stays bounded. + auto const total = static_cast(m->list_size()); + std::size_t untrusted = 0; + bool skippedUntrusted = false; + protocol::TMManifests relay; - for (std::size_t i = 0; i < n; ++i) + for (std::size_t i = 0; i < total; ++i) { auto& s = m->list().Get(i).stobject(); @@ -677,6 +683,19 @@ OverlayImpl::onManifests( // lock: listed() takes the validator-list lock, so ordering it // first avoids holding the two locks in opposite orders. bool const isTrusted = app_.getValidators().listed(mo->masterKey); + + // Bound untrusted work: process at most kMaxManifestsPerMessage + // untrusted manifests, but never skip a trusted one. Trusted + // manifests are not counted against the cap. + if (!isTrusted) + { + if (untrusted >= kMaxManifestsPerMessage) + { + skippedUntrusted = true; + continue; + } + ++untrusted; + } // Updates to a known key are relayed even when untrusted. Use // getSequence, not getManifest, to avoid copying the cached payload // on this hot path. @@ -685,7 +704,8 @@ OverlayImpl::onManifests( auto const result = app_.getValidatorManifests().applyManifest( std::move(*mo), - isTrusted ? ManifestRateLimitCap::Uncapped : ManifestRateLimitCap::Capped); + isTrusted ? ManifestRateLimitCapPolicy::Uncapped + : ManifestRateLimitCapPolicy::Capped); if (result == ManifestDisposition::Accepted) { @@ -724,6 +744,18 @@ OverlayImpl::onManifests( } } + if (skippedUntrusted) + { + // The sender exceeded the untrusted per-message cap. Charge it (once, + // here) so a flood of untrusted manifests is penalized, while an honest + // message of trusted manifests never is. + from->charge(Resource::kFeeMalformedRequest, "too many untrusted manifests"); + + JLOG(journal.warn()) << "Manifests: message had " << total + << " entries; processed all trusted plus the first " + << kMaxManifestsPerMessage << " untrusted"; + } + if (!relay.list().empty()) { forEach([m2 = std::make_shared(relay, protocol::mtMANIFESTS)]( @@ -1225,15 +1257,60 @@ OverlayImpl::getManifestsMessage() if (auto seq = app_.getValidatorManifests().sequence(); seq != manifestListSeq_) { - protocol::TMManifests tm; - + // Phase 1: snapshot the cache under its own lock. Do not call + // Validators::listed() here — that takes the validator-list lock, and + // forEachManifest holds the manifest-cache lock, so consulting trust + // inside the callback would invert the lock order used elsewhere + // (see onManifests) and risk deadlock. Capture the manifest hash now, + // while we have the Manifest object, for the suppression key. + struct CachedManifest + { + PublicKey masterKey; + std::string serialized; + uint256 hash; + }; + std::vector cached; app_.getValidatorManifests().forEachManifest( - [&tm](std::size_t s) { tm.mutable_list()->Reserve(s); }, - [&tm, &hr = app_.getHashRouter()](Manifest const& manifest) { - tm.add_list()->set_stobject(manifest.serialized.data(), manifest.serialized.size()); - hr.addSuppression(manifest.hash()); + [&cached](std::size_t s) { cached.reserve(s); }, + [&cached](Manifest const& manifest) { + cached.push_back({manifest.masterKey, manifest.serialized, manifest.hash()}); }); + // Phase 2: no cache lock held, so trust checks are safe. Include every + // trusted manifest, then fill any remaining headroom up to + // kMaxManifestsPerMessage with untrusted gossip, so the whole message + // stays within the per-message cap the receiver enforces (trusted + // count is tiny in practice, so this effectively never drops trusted). + std::vector selected; + std::vector untrusted; + for (auto const& e : cached) + { + if (app_.getValidators().listed(e.masterKey)) + { + selected.push_back(&e); + } + else + { + untrusted.push_back(&e); + } + } + + // Cap untrusted only; trusted manifests are all included above. + auto const take = std::min(kMaxManifestsPerMessage, untrusted.size()); + selected.insert(selected.end(), untrusted.begin(), untrusted.begin() + take); + + // Shuffle the order. Cryptographic randomness is not needed here. + std::shuffle(selected.begin(), selected.end(), defaultPrng()); + + protocol::TMManifests tm; + auto& hr = app_.getHashRouter(); + tm.mutable_list()->Reserve(static_cast(selected.size())); + for (auto const* e : selected) + { + tm.add_list()->set_stobject(e->serialized.data(), e->serialized.size()); + hr.addSuppression(e->hash); + } + manifestMessage_.reset(); if (tm.list_size() != 0) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 822ef05304..7de96aa459 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -1131,6 +1132,9 @@ PeerImp::onMessage(std::shared_ptr const& m) if (s > 100) fee_.update(Resource::kFeeModerateBurdenPeer, "oversize"); + // OverlayImpl::onManifests bounds the untrusted work and charges the fee + // if the untrusted count exceeds the per-message cap; trusted manifests + // are always processed and not counted against it. app_.getJobQueue().addJob(JtManifest, "RcvManifests", [this, that = shared_from_this(), m]() { overlay_.onManifests(m, that); }); diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h index b1a30bad10..b181ea6307 100644 --- a/src/xrpld/overlay/detail/ProtocolMessage.h +++ b/src/xrpld/overlay/detail/ProtocolMessage.h @@ -351,6 +351,16 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin return result; } + // Drop an oversized TMManifests without penalty: consume the bytes and + // return no error, so the connection is preserved. + if (header->messageType == protocol::mtMANIFESTS && + (header->payloadWireSize > kMaximumManifestsMessageSize || + header->uncompressedSize > kMaximumManifestsMessageSize)) + { + result.first = header->totalWireSize; + return result; + } + bool success = false; switch (header->messageType) From a88ba66fcea9b635a0e31df3b03ede0bc65c8a07 Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 31 Jul 2026 16:55:12 -0400 Subject: [PATCH 36/68] chore: Bump version to 3.2.1-rc1 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index c488bb20de..323a5e628b 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.0" +char const* const versionString = "3.2.1-rc1" // clang-format on ; From d4c1359921f34a4e96c5c8483119e59f0e30e4df Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Fri, 31 Jul 2026 19:46:17 -0400 Subject: [PATCH 37/68] chore: Bump version to 3.2.1 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 323a5e628b..48bcf2f598 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.2.1-rc1" +char const* const versionString = "3.2.1" // clang-format on ; From 8461ded0d8a1692be899d76266f2c8a6d160aa48 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:00:11 +0100 Subject: [PATCH 38/68] fix: Cap untrusted manifests per message and drop oversized ones --- include/xrpl/basics/base64.h | 8 +- include/xrpl/server/Manifest.h | 31 +++++-- src/libxrpl/server/Manifest.cpp | 8 +- src/libxrpl/server/Wallet.cpp | 2 +- src/test/app/Manifest_test.cpp | 38 ++++---- src/test/app/ValidatorList_test.cpp | 16 ++-- src/xrpld/app/misc/detail/ValidatorList.cpp | 8 +- src/xrpld/overlay/Message.h | 8 ++ src/xrpld/overlay/detail/OverlayImpl.cpp | 98 +++++++++++++++++++-- src/xrpld/overlay/detail/PeerImp.cpp | 4 + src/xrpld/overlay/detail/ProtocolMessage.h | 10 +++ 11 files changed, 175 insertions(+), 56 deletions(-) diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h index 4c743531a6..30fdc1f118 100644 --- a/include/xrpl/basics/base64.h +++ b/include/xrpl/basics/base64.h @@ -56,16 +56,16 @@ encodedSize(std::size_t const nBytes) } /** - * Returns the maximum number of bytes a base64 string of @p nChars characters + * Returns the maximum number of bytes a base64 string of @p numChars characters * decodes to. * - * @param nChars Number of base64 characters. + * @param numChars Number of base64 characters. * @return Upper bound on the number of decoded bytes. */ constexpr std::size_t -decodedSize(std::size_t const nChars) +decodedSize(std::size_t const numChars) { - return ((nChars / 4) * 3) + 2; + return ((numChars / 4) * 3) + 2; } } // namespace base64 diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 452047ecd7..2de5bf4752 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -51,8 +51,8 @@ namespace xrpl { entry (which contains the manifest for this validator) is decoded and added to the manifest cache. Other manifests are added as "gossip" received from xrpld peers, including ones for validators this node does not - list. Manifests for unlisted validators are capped (kMaxUntrustedCount) - so peer gossip cannot grow the cache without bound; listed validators are + trust. Manifests for untrusted validators are capped (kMaxUntrustedCount) + so peer gossip cannot grow the cache without bound; trusted validators are not capped. Entries are never evicted, so a stored revocation is permanent. When an ephemeral key is compromised, a new signing key pair is created, @@ -170,14 +170,14 @@ std::string to_string(Manifest const& m); /** - *Largest a valid manifest can be, in decoded bytes. + * Largest a valid manifest can be, in decoded bytes. * * A manifest has a fixed set of fields. Each is serialized as a field header * (1-2 bytes), an optional length prefix (1 byte for these sizes), and the * field body. Taking every field at its largest gives the maximum below, so * anything larger cannot be a valid manifest. * - * Field header + length + body = bytes + * Field header + length + body = bytes * sfVersion (U16) 2 0 2 4 * sfSequence (U32) 1 0 4 5 * sfPublicKey (33) 1 1 33 35 @@ -200,6 +200,23 @@ constexpr std::size_t kMaxManifestBytes = 358; */ constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes); +/** + * Maximum number of manifests carried in a single TMManifests message. + * + * Outbound, the TMManifests message sent to a peer includes every trusted + * manifest and fills the rest of this budget with untrusted gossip, so it + * never exceeds this size. Inbound, trusted manifests are always processed + * and untrusted ones are processed up to this many, so a peer sending its + * whole cache cannot force unbounded work. + * + * The trusted set is tiny relative to this bound, so trusted manifests are + * not dropped in practice. This is a transitional per-message cap; the cache + * already bounds untrusted manifests (see kMaxUntrustedCount), so it is no + * longer needed once the network has upgraded past nodes that send their + * whole cache in one message. + */ +constexpr std::size_t kMaxManifestsPerMessage = 200; + /** * Constructs Manifest from serialized string * @@ -208,7 +225,7 @@ constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes * @return `std::nullopt` if string is invalid * * @note This does not verify manifest signatures. - * `Manifest::verify` should be called after constructing manifest. + * `Manifest::verify` should be called after constructing manifest. */ /** @{ */ std::optional @@ -303,7 +320,7 @@ to_string(ManifestDisposition m) * must choose. `Capped` is the safe, flood-resistant value; only listed or * configured keys should use `Uncapped`. */ -enum class ManifestRateLimitCap : std::uint8_t { +enum class ManifestRateLimitCapPolicy : std::uint8_t { Capped, ///< Subject to the untrusted cap (unlisted peer gossip) Uncapped ///< Bypasses the cap (listed/trusted or config manifests) }; @@ -469,7 +486,7 @@ public: * May be called concurrently */ ManifestDisposition - applyManifest(Manifest m, ManifestRateLimitCap cap); + applyManifest(Manifest m, ManifestRateLimitCapPolicy cap); /** * Stop counting a master key against the untrusted cap. diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index b34955dc28..3da3f9e9cd 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -382,9 +382,9 @@ ManifestCache::revoked(PublicKey const& pk) const } ManifestDisposition -ManifestCache::applyManifest(Manifest m, ManifestRateLimitCap const cap) +ManifestCache::applyManifest(Manifest m, ManifestRateLimitCapPolicy const cap) { - bool const uncapped = cap == ManifestRateLimitCap::Uncapped; + bool const uncapped = cap == ManifestRateLimitCapPolicy::Uncapped; // The signature is checked only on the first `prewriteCheck` run (under the // read lock). It is expensive, so `checkSignature` is cleared the first @@ -634,7 +634,7 @@ ManifestCache::load( JLOG(j_.warn()) << "Configured manifest revokes public key"; } - if (applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + if (applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped) == ManifestDisposition::Invalid) { JLOG(j_.error()) << "Manifest in config was rejected"; @@ -658,7 +658,7 @@ ManifestCache::load( auto mo = deserializeManifest(base64Decode(revocationStr)); if (!mo || !mo->revoked() || - applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped) == + applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped) == ManifestDisposition::Invalid) { JLOG(j_.error()) << "Invalid validator key revocation in config"; diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp index e6af5b6411..42ac80ef3f 100644 --- a/src/libxrpl/server/Wallet.cpp +++ b/src/libxrpl/server/Wallet.cpp @@ -80,7 +80,7 @@ getManifests( // Only trusted manifests are persisted (see saveManifests), so // anything loaded from the DB bypasses the untrusted cap. - cache.applyManifest(std::move(*mo), ManifestRateLimitCap::Uncapped); + cache.applyManifest(std::move(*mo), ManifestRateLimitCapPolicy::Uncapped); } else { diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp index ae30414b92..ef2043a22c 100644 --- a/src/test/app/Manifest_test.cpp +++ b/src/test/app/Manifest_test.cpp @@ -400,7 +400,7 @@ public: ManifestDisposition::Accepted == cache.applyManifest( makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0), - ManifestRateLimitCap::Capped)); + ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first); BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk); @@ -413,7 +413,7 @@ public: ManifestDisposition::Accepted == cache.applyManifest( makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1), - ManifestRateLimitCap::Capped)); + ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -424,7 +424,7 @@ public: ManifestDisposition::BadEphemeralKey == cache.applyManifest( makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2), - ManifestRateLimitCap::Capped)); + ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first); BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -435,7 +435,7 @@ public: BEAST_EXPECT( ManifestDisposition::Accepted == cache.applyManifest( - makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCap::Capped)); + makeRevocation(sk, KeyType::Ed25519), ManifestRateLimitCapPolicy::Capped)); BEAST_EXPECT(cache.revoked(pk)); BEAST_EXPECT(cache.getSigningKey(pk) == pk); BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first); @@ -907,28 +907,28 @@ public: // higher sequence numbers auto const seq0 = cache.sequence(); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT(cache.sequence() > seq0); auto const seq1 = cache.sequence(); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT(cache.sequence() == seq1); BEAST_EXPECT( - cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT( - cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA2), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA2), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::BadEphemeralKey); // applyManifest should accept manifests with max sequence numbers @@ -936,38 +936,38 @@ public: BEAST_EXPECT(!cache.revoked(pkA)); BEAST_EXPECT(sAMax.revoked()); BEAST_EXPECT( - cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sAMax), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT( - cache.applyManifest(clone(sAMax), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sAMax), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT( - cache.applyManifest(clone(sA0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sA0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT(cache.revoked(pkA)); // applyManifest should reject manifests with invalid signatures BEAST_EXPECT( - cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT( - cache.applyManifest(clone(sB0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Stale); BEAST_EXPECT(!deserializeManifest(fake)); BEAST_EXPECT( - cache.applyManifest(clone(sB1), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Invalid); BEAST_EXPECT( - cache.applyManifest(clone(sB2), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sB2), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); auto const sC0 = makeManifest( kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47); BEAST_EXPECT( - cache.applyManifest(clone(sC0), ManifestRateLimitCap::Capped) == + cache.applyManifest(clone(sC0), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::BadMasterKey); } diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp index 0340b24680..323c77c780 100644 --- a/src/test/app/ValidatorList_test.cpp +++ b/src/test/app/ValidatorList_test.cpp @@ -280,7 +280,7 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) manifests.applyManifest( - *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + *deserializeManifest(cfgManifest), ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT( trustedKeys->load(localSigningPublicOuter, emptyCfgKeys, emptyCfgPublishers)); @@ -373,7 +373,7 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) manifests.applyManifest( - *deserializeManifest(cfgManifest), ManifestRateLimitCap::Capped); + *deserializeManifest(cfgManifest), ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) BEAST_EXPECT(trustedKeys->load(localSigningPublicOuter, cfgKeys, emptyCfgPublishers)); @@ -467,7 +467,7 @@ private: pubRevokedSigning.first, pubRevokedSigning.second, std::numeric_limits::max())), - ManifestRateLimitCap::Capped); + ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) // these two are not revoked (and not in the manifest cache at all.) @@ -509,7 +509,7 @@ private: pubRevokedSigning.first, pubRevokedSigning.second, std::numeric_limits::max())), - ManifestRateLimitCap::Capped); + ManifestRateLimitCapPolicy::Capped); // NOLINTEND(bugprone-unchecked-optional-access) // this one is not revoked (and not in the manifest cache at all.) @@ -1228,7 +1228,7 @@ private: BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCap::Capped) == + manifestsOuter.applyManifest(std::move(*m1), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); @@ -1243,7 +1243,7 @@ private: masterPublic, masterPrivate, signingPublic2, signingKeys2.second, 2)); BEAST_EXPECT( // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCap::Capped) == + manifestsOuter.applyManifest(std::move(*m2), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); BEAST_EXPECT(trustedKeysOuter->listed(masterPublic)); BEAST_EXPECT(trustedKeysOuter->trusted(masterPublic)); @@ -1261,7 +1261,7 @@ private: // NOLINTBEGIN(bugprone-unchecked-optional-access) BEAST_EXPECT(max->revoked()); BEAST_EXPECT( - manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCap::Capped) == + manifestsOuter.applyManifest(std::move(*max), ManifestRateLimitCapPolicy::Capped) == ManifestDisposition::Accepted); // NOLINTEND(bugprone-unchecked-optional-access) @@ -2683,7 +2683,7 @@ private: { valManifests.applyManifest( *deserializeManifest(base64Decode(self->manifest)), - ManifestRateLimitCap::Capped); + ManifestRateLimitCapPolicy::Capped); BEAST_EXPECT( result->load(self->signingPublic, emptyCfgKeys, cfgPublishers, threshold)); } diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index 1b4d2bab80..e355cfacab 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1105,8 +1105,8 @@ ValidatorList::updatePublisherList( continue; } - if (auto const r = - validatorManifests_.applyManifest(std::move(*m), ManifestRateLimitCap::Uncapped); + if (auto const r = validatorManifests_.applyManifest( + std::move(*m), ManifestRateLimitCapPolicy::Uncapped); r == ManifestDisposition::Invalid) { JLOG(j_.warn()) << "List for " << strHex(pubKey) @@ -1362,8 +1362,8 @@ ValidatorList::verify( // Publisher keys are configured/trusted (checked above), so bypass the // untrusted cap. - auto const result = - publisherManifests_.applyManifest(std::move(manifest), ManifestRateLimitCap::Uncapped); + auto const result = publisherManifests_.applyManifest( + std::move(manifest), ManifestRateLimitCapPolicy::Uncapped); if (revoked && result == ManifestDisposition::Accepted) { diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h index e942ed244a..bd4772b451 100644 --- a/src/xrpld/overlay/Message.h +++ b/src/xrpld/overlay/Message.h @@ -4,6 +4,7 @@ #include #include +#include #include @@ -23,6 +24,13 @@ constexpr std::size_t kMaximumMessageSize = megabytes(64); // so we define a separate limit for them. constexpr std::size_t kMaximumPingMessageSize = kilobytes(1); +// Upper bound on the wire size of a TMManifests message: kMaxManifestsPerMessage entries +// of at most kMaxManifestBytes each, plus a small allowance for protobuf +// framing per entry. +constexpr std::size_t kManifestFramingBytes = 8; +constexpr std::size_t kMaximumManifestsMessageSize = + kMaxManifestsPerMessage * (kMaxManifestBytes + kManifestFramingBytes); + // VFALCO NOTE If we forward declare Message and write out shared_ptr // instead of using the in-class type alias, we can remove the // entire ripple.pb.h from the main headers. diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index e59cff85d8..5bac7df720 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -663,12 +664,17 @@ OverlayImpl::onManifests( std::shared_ptr const& m, std::shared_ptr const& from) { - auto const n = m->list_size(); auto const& journal = from->pJournal(); + // Process every trusted manifest, but stop processing untrusted ones once + // kMaxManifestsPerMessage of them have been handled, so the work stays bounded. + auto const total = static_cast(m->list_size()); + std::size_t untrusted = 0; + bool skippedUntrusted = false; + protocol::TMManifests relay; - for (std::size_t i = 0; i < n; ++i) + for (std::size_t i = 0; i < total; ++i) { auto& s = m->list().Get(i).stobject(); @@ -679,6 +685,19 @@ OverlayImpl::onManifests( // lock: listed() takes the validator-list lock, so ordering it // first avoids holding the two locks in opposite orders. bool const isTrusted = app_.getValidators().listed(mo->masterKey); + + // Bound untrusted work: process at most kMaxManifestsPerMessage + // untrusted manifests, but never skip a trusted one. Trusted + // manifests are not counted against the cap. + if (!isTrusted) + { + if (untrusted >= kMaxManifestsPerMessage) + { + skippedUntrusted = true; + continue; + } + ++untrusted; + } // Updates to a known key are relayed even when untrusted. Use // getSequence, not getManifest, to avoid copying the cached payload // on this hot path. @@ -687,7 +706,8 @@ OverlayImpl::onManifests( auto const result = app_.getValidatorManifests().applyManifest( std::move(*mo), - isTrusted ? ManifestRateLimitCap::Uncapped : ManifestRateLimitCap::Capped); + isTrusted ? ManifestRateLimitCapPolicy::Uncapped + : ManifestRateLimitCapPolicy::Capped); if (result == ManifestDisposition::Accepted) { @@ -726,6 +746,18 @@ OverlayImpl::onManifests( } } + if (skippedUntrusted) + { + // The sender exceeded the untrusted per-message cap. Charge it (once, + // here) so a flood of untrusted manifests is penalized, while an honest + // message of trusted manifests never is. + from->charge(Resource::kFeeMalformedRequest, "too many untrusted manifests"); + + JLOG(journal.warn()) << "Manifests: message had " << total + << " entries; processed all trusted plus the first " + << kMaxManifestsPerMessage << " untrusted"; + } + if (!relay.list().empty()) { forEach([m2 = std::make_shared(relay, protocol::mtMANIFESTS)]( @@ -1228,15 +1260,63 @@ OverlayImpl::getManifestsMessage() if (auto seq = app_.getValidatorManifests().sequence(); seq != manifestListSeq_) { - protocol::TMManifests tm; - + // Phase 1: snapshot the cache under its own lock. Do not call + // Validators::listed() here — that takes the validator-list lock, and + // forEachManifest holds the manifest-cache lock, so consulting trust + // inside the callback would invert the lock order used elsewhere + // (see onManifests) and risk deadlock. Capture the manifest hash now, + // while we have the Manifest object, for the suppression key. + struct CachedManifest + { + PublicKey masterKey; + std::string serialized; + uint256 hash; + }; + std::vector cached; app_.getValidatorManifests().forEachManifest( - [&tm](std::size_t s) { tm.mutable_list()->Reserve(s); }, - [&tm, &hr = app_.getHashRouter()](Manifest const& manifest) { - tm.add_list()->set_stobject(manifest.serialized.data(), manifest.serialized.size()); - hr.addSuppression(manifest.hash()); + [&cached](std::size_t s) { cached.reserve(s); }, + [&cached](Manifest const& manifest) { + cached.push_back( + {.masterKey = manifest.masterKey, + .serialized = manifest.serialized, + .hash = manifest.hash()}); }); + // Phase 2: no cache lock held, so trust checks are safe. Include every + // trusted manifest, then fill any remaining headroom up to + // kMaxManifestsPerMessage with untrusted gossip, so the whole message + // stays within the per-message cap the receiver enforces (trusted + // count is tiny in practice, so this effectively never drops trusted). + std::vector selected; + std::vector untrusted; + for (auto const& e : cached) + { + if (app_.getValidators().listed(e.masterKey)) + { + selected.push_back(&e); + } + else + { + untrusted.push_back(&e); + } + } + + // Cap untrusted only; trusted manifests are all included above. + auto const take = std::min(kMaxManifestsPerMessage, untrusted.size()); + selected.insert(selected.end(), untrusted.begin(), untrusted.begin() + take); + + // Shuffle the order. Cryptographic randomness is not needed here. + std::shuffle(selected.begin(), selected.end(), defaultPrng()); + + protocol::TMManifests tm; + auto& hr = app_.getHashRouter(); + tm.mutable_list()->Reserve(static_cast(selected.size())); + for (auto const* e : selected) + { + tm.add_list()->set_stobject(e->serialized.data(), e->serialized.size()); + hr.addSuppression(e->hash); + } + manifestMessage_.reset(); if (tm.list_size() != 0) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 962ab0f408..3644a441b8 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -60,6 +60,7 @@ #include #include #include +#include #include #include #include @@ -1103,6 +1104,9 @@ PeerImp::onMessage(std::shared_ptr const& m) if (s > 100) fee_.update(Resource::kFeeModerateBurdenPeer, "oversize"); + // OverlayImpl::onManifests bounds the untrusted work and charges the fee + // if the untrusted count exceeds the per-message cap; trusted manifests + // are always processed and not counted against it. app_.getJobQueue().addJob(JtManifest, "RcvManifests", [this, that = shared_from_this(), m]() { overlay_.onManifests(m, that); }); diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h index 7c22a8e84c..abd087f3c8 100644 --- a/src/xrpld/overlay/detail/ProtocolMessage.h +++ b/src/xrpld/overlay/detail/ProtocolMessage.h @@ -374,6 +374,16 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin return result; } + // Drop an oversized TMManifests without penalty: consume the bytes and + // return no error, so the connection is preserved. + if (header->messageType == protocol::mtMANIFESTS && + (header->payloadWireSize > kMaximumManifestsMessageSize || + header->uncompressedSize > kMaximumManifestsMessageSize)) + { + result.first = header->totalWireSize; + return result; + } + bool success = false; switch (header->messageType) From 54cfdda00b64b73bf3f9a987ed6467d7fd213e14 Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:08:43 +0100 Subject: [PATCH 39/68] fix: Increase manifest protocol message size cap and fix manifests relay Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- cfg/xrpld-example.cfg | 39 ++++++++++ include/xrpl/config/Constants.h | 2 + include/xrpl/server/Manifest.h | 87 ++++++++++++++++++---- src/libxrpl/server/Manifest.cpp | 2 +- src/test/core/Config_test.cpp | 81 ++++++++++++++++++++ src/xrpld/app/main/Application.cpp | 20 ++++- src/xrpld/core/Config.h | 17 +++++ src/xrpld/core/detail/Config.cpp | 32 ++++++++ src/xrpld/overlay/Message.h | 35 +++++++-- src/xrpld/overlay/detail/OverlayImpl.cpp | 52 ++++++------- src/xrpld/overlay/detail/OverlayImpl.h | 8 ++ src/xrpld/overlay/detail/PeerImp.h | 16 ++++ src/xrpld/overlay/detail/ProtocolMessage.h | 15 ++-- 13 files changed, 347 insertions(+), 59 deletions(-) diff --git a/cfg/xrpld-example.cfg b/cfg/xrpld-example.cfg index 6a44561c68..747bafe077 100644 --- a/cfg/xrpld-example.cfg +++ b/cfg/xrpld-example.cfg @@ -549,6 +549,45 @@ # only be used for local testing and debugging. Do not disable # on mainnet. # +# max_untrusted_count = +# +# The number of manifests the server keeps for validators it does not +# list, and the number it sends and processes in a single peer protocol +# message. Once the server holds this many, a manifest for a new +# unlisted validator is rejected, so peer gossip cannot grow the cache +# without end. +# +# This option can take any value between 50 and 1000, inclusive. If +# the option is not present the server uses its built-in value. +# +# The current default (which is subject to change) is 300. +# +# max_trusted_count = +# +# The number of manifests for listed validators to allow for when +# sizing peer protocol messages. Manifests for listed validators are +# never dropped, whether sending or receiving, because doing so would +# delay a validator key change reaching this server. Set this above the +# number of validators the server lists. +# +# Together the two counts above set the largest manifest message the +# server accepts: bigger messages are discarded without reading them, +# and without penalising the sender. Raising either means the server +# accepts and sends bigger messages than a peer using the defaults, and +# those peers will discard what this server sends. Lowering either below +# what peers send makes this server discard their manifest messages, +# which it does without recording anything. +# +# This option can take any value between 50 and 1000, inclusive. If +# the option is not present the server uses its built-in value. +# +# The current default (which is subject to change) is 300. +# +# NOTE: These two options (max_untrusted_count and max_trusted_count) +# are transitional. They exist to bound manifest-message size and cache +# growth during the network upgrade. They may be removed in a future +# release once the fleet has upgraded, and should not be relied upon as +# stable configuration. # # [transaction_queue] EXPERIMENTAL # diff --git a/include/xrpl/config/Constants.h b/include/xrpl/config/Constants.h index 0fe4efee63..85d9e3f147 100644 --- a/include/xrpl/config/Constants.h +++ b/include/xrpl/config/Constants.h @@ -119,7 +119,9 @@ struct Keys static constexpr auto kLogInterval = "log_interval"; static constexpr auto kMaxDivergedTime = "max_diverged_time"; static constexpr auto kMaxLedgerCountsToStore = "max_ledger_counts_to_store"; + static constexpr auto kMaxTrustedCount = "max_trusted_count"; static constexpr auto kMaxUnknownTime = "max_unknown_time"; + static constexpr auto kMaxUntrustedCount = "max_untrusted_count"; static constexpr auto kMaximumTxnInLedger = "maximum_txn_in_ledger"; static constexpr auto kMaximumTxnPerAccount = "maximum_txn_per_account"; static constexpr auto kMemoryLevel = "memory_level"; diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 2de5bf4752..786967b057 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -201,21 +201,67 @@ constexpr std::size_t kMaxManifestBytes = 358; constexpr std::size_t kMaxManifestBase64 = base64::encodedSize(kMaxManifestBytes); /** - * Maximum number of manifests carried in a single TMManifests message. + * Default number of untrusted manifests to store in cache and allowed + * in one Manifest message. * - * Outbound, the TMManifests message sent to a peer includes every trusted - * manifest and fills the rest of this budget with untrusted gossip, so it - * never exceeds this size. Inbound, trusted manifests are always processed - * and untrusted ones are processed up to this many, so a peer sending its - * whole cache cannot force unbounded work. + * Bounds unlisted validators two ways. In the cache, a manifest for a + * brand-new unlisted key is rejected once this many are held, so peer gossip + * cannot grow the cache without end. In a TMManifests message, this many are + * sent and processed, so a peer sending its whole cache cannot force unbounded + * work. * - * The trusted set is tiny relative to this bound, so trusted manifests are - * not dropped in practice. This is a transitional per-message cap; the cache - * already bounds untrusted manifests (see kMaxUntrustedCount), so it is no - * longer needed once the network has upgraded past nodes that send their - * whole cache in one message. + * Operators can override this with `[overlay] max_untrusted_count`. Both users + * read the configured value and fall back to this default. */ -constexpr std::size_t kMaxManifestsPerMessage = 200; +constexpr std::size_t kMaxUntrustedCount = 300; + +/** + * Default number of trusted manifests allowed in a Manifest message. + * Not used atm while creating the message, but used to calculate the higher limit on + * received message size. Introduced to maintain consistency. Future implementation + * will use this limit. + * + * Trusted manifests are never dropped: every one this node holds is sent, and + * every one received is processed, since dropping one would delay a validator + * key rotation. This count only sizes the largest message accepted, so it must + * stay above any realistic validator list. Cap can be increased in the config + * file if messages get rejected with actual trusted manifest count crossing + * configured(or else default) value. + * Operators can override this with `[overlay] max_trusted_count`. + */ +constexpr std::size_t kMaxTrustedCount = 300; + +/** + * Number of untrusted manifests to store in cache and allowed + * in one Manifest message.. + * + * Returns the operator's override when one is configured, otherwise + * @ref kMaxUntrustedCount. Config stores an override rather than the default + * itself because the core module cannot depend on this module. + * + * @param configured The value from `[overlay] max_untrusted_count`, or + * `std::nullopt` when the operator did not set it. + */ +constexpr std::size_t +untrustedManifestCount(std::optional const& configured) +{ + return configured.value_or(kMaxUntrustedCount); +} + +/** + * Number of trusted manifests allowed in a Manifest message. + * + * Not a cap on how many are sent or processed; see @ref kMaxTrustedCount. + * but used to calculate the higher limit on received message size. + * + * @param configured The value from `[overlay] max_trusted_count`, or + * `std::nullopt` when the operator did not set it. + */ +constexpr std::size_t +trustedManifestCount(std::optional const& configured) +{ + return configured.value_or(kMaxTrustedCount); +} /** * Constructs Manifest from serialized string @@ -361,9 +407,10 @@ private: /** * Maximum number of untrusted master keys kept in the cache. * - * Once reached, a manifest for a brand-new unlisted key is rejected. + * Once reached, a manifest for a brand-new unlisted key is rejected. Set + * from the config, defaulting to @ref kMaxUntrustedCount. */ - static constexpr std::size_t kMaxUntrustedCount = 100; + std::size_t const maxUntrustedCount_; /** * Running count of manifests rejected because the untrusted cap was full. @@ -381,7 +428,17 @@ private: static constexpr std::uint64_t kUntrustedRejectCount = 10000; public: - explicit ManifestCache(beast::Journal j = beast::Journal(beast::Journal::getNullSink())) : j_(j) + /** + * @param j Journal for logging. + * + * @param maxUntrustedCount Untrusted master keys to keep. Pass the + * configured value; defaults to @ref kMaxUntrustedCount. Taken as a + * parameter because this module cannot depend on the config. + */ + explicit ManifestCache( + beast::Journal j = beast::Journal(beast::Journal::getNullSink()), + std::size_t maxUntrustedCount = kMaxUntrustedCount) + : j_(j), maxUntrustedCount_(maxUntrustedCount) { } diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index 3da3f9e9cd..0760196a3b 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -487,7 +487,7 @@ ManifestCache::applyManifest(Manifest m, ManifestRateLimitCapPolicy const cap) lock.owns_lock(), "xrpl::ManifestCache::applyManifest::atUntrustedCap : locked"); (void)lock; // not used. parameter is present to ensure the mutex is // locked when the lambda is called. - if (iter == map_.end() && !uncapped && untrustedKeys_.size() >= kMaxUntrustedCount) + if (iter == map_.end() && !uncapped && untrustedKeys_.size() >= maxUntrustedCount_) { // Log each rejection at debug, but warn only once per interval so a // flood does not fill the log. diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp index e98a0e1e88..ac5471fd3c 100644 --- a/src/test/core/Config_test.cpp +++ b/src/test/core/Config_test.cpp @@ -1575,6 +1575,87 @@ r.ripple.com:51235 // Above upper bound BEAST_EXPECT(!testDiverged("901")); + + testcase("overlay: manifest counts"); + + // Both keys share one range and one parse path, so exercise each + // through the same helper. + auto testCount = [](std::string const& key, + std::string const& value) -> std::optional { + try + { + Config c; + c.loadFromString("[overlay]\n" + key + "=" + value); + return key == "max_trusted_count" ? c.maxTrustedCount : c.maxUntrustedCount; + } + catch (std::runtime_error const&) + { + return {}; + } + }; + + for (auto const* key : {"max_untrusted_count", "max_trusted_count"}) + { + // Failures. A bad value must surface as std::runtime_error, not + // the std::bad_cast that the underlying parse throws. + BEAST_EXPECT(!testCount(key, "none")); + BEAST_EXPECT(!testCount(key, "0.5")); + BEAST_EXPECT(!testCount(key, "400 manifests")); + BEAST_EXPECT(!testCount(key, "-1")); + + // Below lower bound + BEAST_EXPECT(!testCount(key, "0")); + BEAST_EXPECT(!testCount(key, "49")); + + // In bounds + BEAST_EXPECT(testCount(key, "50") == 50); + BEAST_EXPECT(testCount(key, "51") == 51); + BEAST_EXPECT(testCount(key, "300") == 300); + BEAST_EXPECT(testCount(key, "400") == 400); + BEAST_EXPECT(testCount(key, "999") == 999); + BEAST_EXPECT(testCount(key, "1000") == 1000); + + // Above upper bound + BEAST_EXPECT(!testCount(key, "1001")); + } + + // Each key is independent: setting one leaves the other unset. + { + Config c; + c.loadFromString("[overlay]\nmax_untrusted_count=500"); + BEAST_EXPECT(c.maxUntrustedCount == 500); + BEAST_EXPECT(!c.maxTrustedCount); + } + { + Config c; + c.loadFromString("[overlay]\nmax_trusted_count=500"); + BEAST_EXPECT(c.maxTrustedCount == 500); + BEAST_EXPECT(!c.maxUntrustedCount); + } + + // Both can be set together. + { + Config c; + c.loadFromString("[overlay]\nmax_untrusted_count=250\nmax_trusted_count=750"); + BEAST_EXPECT(c.maxUntrustedCount == 250); + BEAST_EXPECT(c.maxTrustedCount == 750); + } + + // Unset leaves no override, so the use sites fall back to the defaults. + { + Config c; + c.loadFromString("[overlay]\nip_limit=64"); + BEAST_EXPECT(!c.maxUntrustedCount); + BEAST_EXPECT(!c.maxTrustedCount); + } + + // No [overlay] section at all leaves both unset too. + { + Config c; + c.loadFromString(""); + BEAST_EXPECT(!c.maxUntrustedCount); + BEAST_EXPECT(!c.maxTrustedCount); + } } void diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index d329475874..b0f0226e49 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -91,6 +91,7 @@ #include #include #include +#include #include #include #include @@ -428,8 +429,14 @@ public: , cluster_(std::make_unique(logs_->journal("Overlay"))) , peerReservations_( std::make_unique(logs_->journal("PeerReservationTable"))) - , validatorManifests_(std::make_unique(logs_->journal("ManifestCache"))) - , publisherManifests_(std::make_unique(logs_->journal("ManifestCache"))) + , validatorManifests_( + std::make_unique( + logs_->journal("ManifestCache"), + untrustedManifestCount(config_->maxUntrustedCount))) + , publisherManifests_( + std::make_unique( + logs_->journal("ManifestCache"), + untrustedManifestCount(config_->maxUntrustedCount))) , validators_( std::make_unique( *validatorManifests_, @@ -1190,6 +1197,15 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) JLOG(journal_.info()) << "Process starting: " << BuildInfo::getFullVersionString() << ", Instance Cookie: " << instanceCookie_; + // Log the resolved manifest counts, whether configured or defaulted, so a + // shared log shows what the server is running without needing its config. + JLOG(journal_.warn()) << "Manifest counts: max_untrusted_count " + << untrustedManifestCount(config_->maxUntrustedCount) + << (config_->maxUntrustedCount ? " (configured)" : " (default)") + << ", max_trusted_count " + << trustedManifestCount(config_->maxTrustedCount) + << (config_->maxTrustedCount ? " (configured)" : " (default)"); + if (numberOfThreads(*config_) < 2) { JLOG(journal_.warn()) << "Limited to a single I/O service thread by " diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index a7cb5d053a..2492614825 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -295,6 +295,23 @@ public: // How long can a peer remain in the "diverged" state std::chrono::seconds maxDivergedTime{300}; + // Optional overrides for how many manifests are kept in the cache and + // carried in one TMManifests message, split by whether this node lists the + // validator. Unset means use the built-in defaults (kMaxUntrustedCount and + // kMaxTrustedCount in Manifest.h). Kept as overrides here, rather than the + // defaults themselves, so the core module need not depend on the server + // module that owns the constants. + std::optional maxUntrustedCount; + std::optional maxTrustedCount; + + // Bounds for both counts above. The lower bound leaves room for a small + // network or a deliberately tight limit; note that setting a count below + // what peers actually send means their manifest messages are dropped for + // being oversized. The upper bound keeps the implied message size well + // under the overall protocol message limit. + static constexpr std::size_t kMinManifestCount = 50; + static constexpr std::size_t kMaxManifestCount = 1000; + // Enable the beta API version bool betaRpcApi = false; diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index 616717c5fd..e93ccec56e 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -923,6 +923,38 @@ Config::loadFromString(std::string const& fileContents) std::string("Invalid value 'max_diverged_time' in ") + Sections::kOverlay + ": the time must be between 60 and 900 seconds, inclusive."); } + + // Both manifest counts parse and validate identically, so read them + // the same way. Returns nullopt when the key is absent, leaving the + // built-in default in effect at the use site. + auto manifestCount = [&sec](char const* key) -> std::optional { + std::optional count; + + try + { + if (auto val = sec.get(key)) + count = beast::lexicalCastThrow(*val); + } + catch (...) + { + Throw( + std::string("Invalid value '") + key + "' in " + Sections::kOverlay + + ": must be of the form '' representing a count of manifests."); + } + + if (count && (*count < kMinManifestCount || *count > kMaxManifestCount)) + { + Throw( + std::string("Invalid value '") + key + "' in " + Sections::kOverlay + + ": the count must be between " + std::to_string(kMinManifestCount) + " and " + + std::to_string(kMaxManifestCount) + ", inclusive."); + } + + return count; + }; + + maxUntrustedCount = manifestCount(Keys::kMaxUntrustedCount); + maxTrustedCount = manifestCount(Keys::kMaxTrustedCount); } if (getSingleSection(secConfig, Sections::kAmendmentMajorityTime, strTemp, j_)) diff --git a/src/xrpld/overlay/Message.h b/src/xrpld/overlay/Message.h index bd4772b451..065da696d8 100644 --- a/src/xrpld/overlay/Message.h +++ b/src/xrpld/overlay/Message.h @@ -24,12 +24,37 @@ constexpr std::size_t kMaximumMessageSize = megabytes(64); // so we define a separate limit for them. constexpr std::size_t kMaximumPingMessageSize = kilobytes(1); -// Upper bound on the wire size of a TMManifests message: kMaxManifestsPerMessage entries -// of at most kMaxManifestBytes each, plus a small allowance for protobuf -// framing per entry. +// Allowance for protobuf framing around each manifest in a TMManifests message. constexpr std::size_t kManifestFramingBytes = 8; -constexpr std::size_t kMaximumManifestsMessageSize = - kMaxManifestsPerMessage * (kMaxManifestBytes + kManifestFramingBytes); + +/** + * Upper bound on the wire size of a TMManifests message. + * + * Allows both counts' worth of entries at @ref kMaxManifestBytes each, plus + * framing per entry. Messages larger than this are dropped before parsing, + * which bounds the work an oversized message can cause. + * + * @param trustedCount Trusted manifests per message. + * + * @param untrustedCount Untrusted manifests per message. + * + * @note A node that raises either count accepts larger messages than a peer + * running the defaults, and the messages it sends may be dropped by such a + * peer. Lowering either count below what peers send drops their manifest + * messages, including any trusted key rotations they carry, and the drop + * is not recorded on either side. + */ +constexpr std::size_t +maximumManifestsMessageSize(std::size_t const trustedCount, std::size_t const untrustedCount) +{ + return (trustedCount + untrustedCount) * (kMaxManifestBytes + kManifestFramingBytes); +} + +// The message size the defaults imply must stay within the overall protocol +// message limit. The same check for the largest configurable counts lives in +// OverlayImpl.h, where the configured bound is visible. +static_assert( + maximumManifestsMessageSize(kMaxTrustedCount, kMaxUntrustedCount) < kMaximumMessageSize); // VFALCO NOTE If we forward declare Message and write out shared_ptr // instead of using the in-class type alias, we can remove the diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 5bac7df720..d1f145a601 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -667,7 +667,10 @@ OverlayImpl::onManifests( auto const& journal = from->pJournal(); // Process every trusted manifest, but stop processing untrusted ones once - // kMaxManifestsPerMessage of them have been handled, so the work stays bounded. + // the configured untrusted count has been handled, so the work stays + // bounded. Trusted manifests are always processed: dropping one would delay + // a validator key rotation reaching this node. + auto const maxUntrusted = untrustedManifestCount(app_.config().maxUntrustedCount); auto const total = static_cast(m->list_size()); std::size_t untrusted = 0; bool skippedUntrusted = false; @@ -686,23 +689,18 @@ OverlayImpl::onManifests( // first avoids holding the two locks in opposite orders. bool const isTrusted = app_.getValidators().listed(mo->masterKey); - // Bound untrusted work: process at most kMaxManifestsPerMessage - // untrusted manifests, but never skip a trusted one. Trusted - // manifests are not counted against the cap. + // Bound untrusted work: process at most maxUntrusted untrusted + // manifests, but never skip a trusted one. Trusted manifests are + // not counted against the cap. if (!isTrusted) { - if (untrusted >= kMaxManifestsPerMessage) + if (untrusted >= maxUntrusted) { skippedUntrusted = true; continue; } ++untrusted; } - // Updates to a known key are relayed even when untrusted. Use - // getSequence, not getManifest, to avoid copying the cached payload - // on this hot path. - bool const isKnown = - app_.getValidatorManifests().getSequence(mo->masterKey).has_value(); auto const result = app_.getValidatorManifests().applyManifest( std::move(*mo), @@ -721,22 +719,17 @@ OverlayImpl::onManifests( "deserialization succeeded"); // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above app_.getOPs().pubManifest(*mo); + // NOLINTEND(bugprone-unchecked-optional-access) + + relay.add_list()->set_stobject(s); - // Relay only trusted manifests or updates to known keys, so - // untrusted gossip for a brand-new key cannot be amplified. // Persist to the wallet DB only for trusted keys, so untrusted // gossip never survives a restart. - if (isTrusted || isKnown) + if (isTrusted) { - relay.add_list()->set_stobject(s); - - if (isTrusted) - { - auto db = app_.getWalletDB().checkoutDb(); - addValidatorManifest(*db, serialized); - } + auto db = app_.getWalletDB().checkoutDb(); + addValidatorManifest(*db, serialized); } - // NOLINTEND(bugprone-unchecked-optional-access) } } else @@ -749,13 +742,12 @@ OverlayImpl::onManifests( if (skippedUntrusted) { // The sender exceeded the untrusted per-message cap. Charge it (once, - // here) so a flood of untrusted manifests is penalized, while an honest - // message of trusted manifests never is. + // here) so a flood of untrusted manifests is penalized. from->charge(Resource::kFeeMalformedRequest, "too many untrusted manifests"); JLOG(journal.warn()) << "Manifests: message had " << total - << " entries; processed all trusted plus the first " - << kMaxManifestsPerMessage << " untrusted"; + << " entries; processed all trusted plus the first " << maxUntrusted + << " untrusted"; } if (!relay.list().empty()) @@ -1283,10 +1275,9 @@ OverlayImpl::getManifestsMessage() }); // Phase 2: no cache lock held, so trust checks are safe. Include every - // trusted manifest, then fill any remaining headroom up to - // kMaxManifestsPerMessage with untrusted gossip, so the whole message - // stays within the per-message cap the receiver enforces (trusted - // count is tiny in practice, so this effectively never drops trusted). + // trusted manifest, then fill any remaining headroom up to the + // configured untrusted count with gossip. Trusted manifests are never + // dropped; the trusted count only sizes the accepted message. std::vector selected; std::vector untrusted; for (auto const& e : cached) @@ -1302,7 +1293,8 @@ OverlayImpl::getManifestsMessage() } // Cap untrusted only; trusted manifests are all included above. - auto const take = std::min(kMaxManifestsPerMessage, untrusted.size()); + auto const take = + std::min(untrustedManifestCount(app_.config().maxUntrustedCount), untrusted.size()); selected.insert(selected.end(), untrusted.begin(), untrusted.begin() + take); // Shuffle the order. Cryptographic randomness is not needed here. diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index 092ac86a6d..4c8fcd2a00 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -54,6 +55,13 @@ namespace xrpl { +// The largest counts an operator can configure must still imply a message size +// within the overall protocol message limit. The same check for the defaults +// lives in Message.h. +static_assert( + maximumManifestsMessageSize(Config::kMaxManifestCount, Config::kMaxManifestCount) < + kMaximumMessageSize); + class PeerImp; class BasicConfig; diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 0085927550..720d9a1195 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include @@ -464,6 +465,21 @@ public: return compressionEnabled_ == Compressed::On; } + /** + * Largest TMManifests message this node accepts, in bytes. + * + * Read by invokeProtocolMessage to drop oversized messages before + * parsing. Not part of the Peer interface: the message handler is a + * template parameter, so only PeerImp needs to provide this. + */ + [[nodiscard]] std::size_t + maxManifestsMessageSize() const + { + return maximumManifestsMessageSize( + trustedManifestCount(app_.config().maxTrustedCount), + untrustedManifestCount(app_.config().maxUntrustedCount)); + } + bool txReduceRelayEnabled() const override { diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h index abd087f3c8..f7d5e26272 100644 --- a/src/xrpld/overlay/detail/ProtocolMessage.h +++ b/src/xrpld/overlay/detail/ProtocolMessage.h @@ -375,13 +375,16 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin } // Drop an oversized TMManifests without penalty: consume the bytes and - // return no error, so the connection is preserved. - if (header->messageType == protocol::mtMANIFESTS && - (header->payloadWireSize > kMaximumManifestsMessageSize || - header->uncompressedSize > kMaximumManifestsMessageSize)) + // return no error, so the connection is preserved. The limit follows this + // node's configured manifests-per-message count. + if (header->messageType == protocol::mtMANIFESTS) { - result.first = header->totalWireSize; - return result; + auto const maxSize = handler.maxManifestsMessageSize(); + if (header->payloadWireSize > maxSize || header->uncompressedSize > maxSize) + { + result.first = header->totalWireSize; + return result; + } } bool success = false; From 39c8c293b32e345ea8f665ba270a7593fc20b900 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 4 Aug 2026 17:11:00 -0400 Subject: [PATCH 40/68] chore: Bump version to 3.3.0-rc7 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 87956a12fd..b3f9128f2e 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc6" +char const* const versionString = "3.3.0-rc7" // clang-format on ; From 00a178fb92ca49521b937ae1a99d863765ea8a90 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 6 Aug 2026 17:34:39 +0100 Subject: [PATCH 41/68] chore: Bump version to 3.3.0 --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index b3f9128f2e..267df204ba 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0-rc7" +char const* const versionString = "3.3.0" // clang-format on ; From a0e78b286fbbfb93fceb0da697a2c0cd64ed76d0 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 7 Aug 2026 17:56:46 +0100 Subject: [PATCH 42/68] chore: Bump version to 3.4.0-b0 (#7976) --- src/libxrpl/protocol/BuildInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 6647437268..ff4e5aa0ee 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -23,7 +23,7 @@ namespace { //------------------------------------------------------------------------------ // clang-format off // NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.3.0" +char const* const versionString = "3.4.0-b0" // clang-format on ; From abf5511d07bd7280a0ac80c80dd5afce0d50c1c4 Mon Sep 17 00:00:00 2001 From: Timur Yalymov <36795566+tyalymov@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:41:23 +0100 Subject: [PATCH 43/68] fix: Correct sign-check wording in lending protocol messages (#7913) --- src/libxrpl/tx/invariants/VaultInvariant.cpp | 6 +++--- src/libxrpl/tx/transactors/lending/LoanPay.cpp | 2 +- src/test/app/Invariants_test.cpp | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index eca50eb809..c577fdf356 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -465,7 +465,7 @@ ValidVault::finalize( if (afterVault.assetsAvailable < kZero) { - JLOG(j.fatal()) << "Invariant failed: assets available must be positive"; + JLOG(j.fatal()) << "Invariant failed: assets available must not be negative"; result = false; } @@ -491,13 +491,13 @@ ValidVault::finalize( if (afterVault.assetsTotal < kZero) { - JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; + JLOG(j.fatal()) << "Invariant failed: assets outstanding must not be negative"; result = false; } if (afterVault.assetsMaximum < kZero) { - JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive"; + JLOG(j.fatal()) << "Invariant failed: assets maximum must not be negative"; result = false; } diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index 74e8efeda2..4619540295 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -813,7 +813,7 @@ LoanPay::doApply() XRPL_ASSERT_PARTS( vaultBalanceAfter >= beast::kZero && brokerBalanceAfter >= beast::kZero, "xrpl::LoanPay::doApply", - "positive vault and broker balances"); + "non-negative vault and broker balances"); XRPL_ASSERT_PARTS( vaultBalanceAfter >= vaultBalanceBefore, "xrpl::LoanPay::doApply", diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 9bc9524f80..6234d81762 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -3260,9 +3260,9 @@ class Invariants_test : public beast::unit_test::Suite "set must not change assets available", "set must not change shares outstanding", "set must not change vault balance", - "assets available must be positive", + "assets available must not be negative", "assets available must not be greater than assets outstanding", - "assets outstanding must be positive"}, + "assets outstanding must not be negative"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const keylet = keylet::vault(a1.id(), ac.view().seq()); auto sleVault = ac.view().peek(keylet); @@ -3424,7 +3424,7 @@ class Invariants_test : public beast::unit_test::Suite TxAccount::A2); doInvariantCheck( - {"assets maximum must be positive"}, + {"assets maximum must not be negative"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const keylet = keylet::vault(a1.id(), ac.view().seq()); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { @@ -3612,7 +3612,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( { - "assets maximum must be positive", + "assets maximum must not be negative", "create operation must not have updated a vault", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { From 798e889ec467661801dc9451a74adcbd3a0de92f Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Fri, 7 Aug 2026 13:24:45 -0700 Subject: [PATCH 44/68] fix: Deduplicate oracle entries in get_aggregate_price RPC (#6586) Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Mayukha Vadari Co-authored-by: Bart --- API-CHANGELOG.md | 1 + src/test/rpc/GetAggregatePrice_test.cpp | 42 +++++++++++++++++++ .../handlers/orderbook/GetAggregatePrice.cpp | 8 ++++ 3 files changed, 51 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index a04f265328..79fb8ff522 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -42,6 +42,7 @@ This section contains changes targeting a future version. ### Bugfixes +- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586) - Peer Crawler: The `port` field in `overlay.active[]` now consistently returns an integer instead of a string for outbound peers. [#6318](https://github.com/XRPLF/rippled/pull/6318) - `ping`: The `ip` field is no longer returned as an empty string for proxied connections without a forwarded-for header. It is now omitted, consistent with the behavior for identified connections. [#6730](https://github.com/XRPLF/rippled/pull/6730) - gRPC `GetLedgerDiff`: Fixed error message that incorrectly said "base ledger not validated" when the desired ledger was not validated. [#6730](https://github.com/XRPLF/rippled/pull/6730) diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp index 3e0bfa1fd3..58c2e8b996 100644 --- a/src/test/rpc/GetAggregatePrice_test.cpp +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -320,6 +320,48 @@ public: BEAST_EXPECT(ret[jss::median] == "74"); BEAST_EXPECT(ret[jss::time] == 946695000); } + + // Duplicate oracle entries should be deduplicated. + // Two separate oracles with different prices give size=2. + // Listing the first oracle twice in the query must not + // inflate the size to 3. + { + Env env(*this); + auto const baseFee = static_cast(env.current()->fees().base.drops()); + + Account const owner1{"owner1"}; + Account const owner2{"owner2"}; + env.fund(XRP(1'000), owner1); + env.fund(XRP(1'000), owner2); + Oracle const oracle1( + env, {.owner = owner1, .series = {{"XRP", "USD", 740, 1}}, .fee = baseFee}); + Oracle const oracle2( + env, {.owner = owner2, .series = {{"XRP", "USD", 840, 1}}, .fee = baseFee}); + + // Query with both oracles listed once + OraclesData const single = { + {owner1, oracle1.documentID()}, {owner2, oracle2.documentID()}}; + auto const retSingle = Oracle::aggregatePrice(env, "XRP", "USD", single); + + // Query with oracle1 listed twice + OraclesData const duplicated = { + {owner1, oracle1.documentID()}, + {owner1, oracle1.documentID()}, + {owner2, oracle2.documentID()}}; + auto const retDup = Oracle::aggregatePrice(env, "XRP", "USD", duplicated); + + // Results should be identical - duplicates must not be + // double-counted + BEAST_EXPECT( + retSingle[jss::entire_set][jss::size] == retDup[jss::entire_set][jss::size]); + BEAST_EXPECT(retDup[jss::entire_set][jss::size].asUInt() == 2); + BEAST_EXPECT( + retSingle[jss::entire_set][jss::mean] == retDup[jss::entire_set][jss::mean]); + BEAST_EXPECT( + retSingle[jss::entire_set][jss::standard_deviation] == + retDup[jss::entire_set][jss::standard_deviation]); + BEAST_EXPECT(retSingle[jss::median] == retDup[jss::median]); + } } void diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp index f493000d0b..33eaa9ce1e 100644 --- a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp +++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp @@ -33,7 +33,9 @@ #include #include #include +#include #include +#include #include namespace xrpl { @@ -251,6 +253,8 @@ doGetAggregatePrice(rpc::JsonContext& context) // Collect the dataset into bimap keyed by lastUpdateTime and // STAmount (Number is int64 and price is uint64) Prices prices; + // Track seen {account, documentID} pairs to skip duplicates + std::set> seen; for (auto const& oracle : params[jss::oracles]) { if (!oracle.isMember(jss::oracle_document_id) || !oracle.isMember(jss::account)) @@ -268,6 +272,10 @@ doGetAggregatePrice(rpc::JsonContext& context) return result; } + // Skip duplicate oracle entries + if (!seen.emplace(*account, *documentID).second) + continue; + auto const sle = ledger->read(keylet::oracle(*account, *documentID)); iteratePriceData(context, sle, [&](STObject const& node) { auto const& series = node.getFieldArray(sfPriceDataSeries); From 0fb92c319421100144fb564a67c3b63e880aea74 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Fri, 7 Aug 2026 17:29:11 -0400 Subject: [PATCH 45/68] refactor: Use SeqProxy instead of uint32 for all sequence-based keylets (#7890) Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com> --- include/xrpl/protocol/Indexes.h | 49 +- include/xrpl/protocol/STTx.h | 6 - include/xrpl/protocol/SeqProxy.h | 19 +- src/libxrpl/ledger/helpers/NFTokenHelpers.cpp | 2 +- src/libxrpl/protocol/Indexes.cpp | 88 ++-- src/libxrpl/protocol/STTx.cpp | 18 +- src/libxrpl/tx/Transactor.cpp | 11 +- src/libxrpl/tx/applySteps.cpp | 2 +- .../tx/transactors/check/CheckCreate.cpp | 5 +- .../tx/transactors/dex/OfferCancel.cpp | 4 +- .../tx/transactors/dex/OfferCreate.cpp | 8 +- .../tx/transactors/escrow/EscrowCancel.cpp | 7 +- .../tx/transactors/escrow/EscrowCreate.cpp | 4 +- .../tx/transactors/escrow/EscrowFinish.cpp | 7 +- .../tx/transactors/lending/LoanBrokerSet.cpp | 4 +- .../tx/transactors/lending/LoanSet.cpp | 4 +- .../payment_channel/PaymentChannelCreate.cpp | 4 +- .../PermissionedDomainSet.cpp | 6 +- .../tx/transactors/system/TicketCreate.cpp | 2 +- .../token/MPTokenIssuanceCreate.cpp | 2 +- .../tx/transactors/vault/VaultCreate.cpp | 6 +- src/test/app/AccountDelete_test.cpp | 37 +- src/test/app/Batch_test.cpp | 10 +- src/test/app/Check_test.cpp | 3 +- src/test/app/EscrowToken_test.cpp | 53 +- src/test/app/Escrow_test.cpp | 29 +- src/test/app/FixNFTokenPageLinks_test.cpp | 7 +- src/test/app/FlowMPT_test.cpp | 3 +- src/test/app/Flow_test.cpp | 3 +- src/test/app/Freeze_test.cpp | 19 +- src/test/app/Invariants_test.cpp | 187 +++---- src/test/app/LPTokenTransfer_test.cpp | 11 +- src/test/app/MPToken_test.cpp | 42 +- src/test/app/NFTokenAuth_test.cpp | 20 +- src/test/app/NFTokenBurn_test.cpp | 19 +- src/test/app/NFTokenDir_test.cpp | 25 +- src/test/app/NFToken_test.cpp | 472 ++++++++++++------ src/test/app/Offer_test.cpp | 9 +- src/test/app/PayChan_test.cpp | 4 +- src/test/app/PermissionedDEX_test.cpp | 33 +- src/test/app/PermissionedDomains_test.cpp | 8 +- src/test/app/Sponsor_test.cpp | 72 +-- src/test/app/TxQ_test.cpp | 4 +- src/test/app/Vault_test.cpp | 55 +- src/test/app/lending/LoanBroker_test.cpp | 64 ++- src/test/app/lending/LoanCashBasis_test.cpp | 14 +- .../app/lending/LoanCoverFreezeAuth_test.cpp | 16 +- src/test/app/lending/LoanInvariants_test.cpp | 7 +- src/test/app/lending/LoanLifecycle_test.cpp | 9 +- src/test/app/lending/LoanMisc_test.cpp | 3 +- src/test/app/lending/LoanPay_test.cpp | 12 +- src/test/app/lending/LoanRounding_test.cpp | 16 +- src/test/app/lending/LoanSecurity_test.cpp | 8 +- src/test/app/lending/LoanTestBase.h | 14 +- src/test/app/lending/LoanValidation_test.cpp | 11 +- src/test/jtx/TestHelpers.h | 5 +- src/test/jtx/impl/TestHelpers.cpp | 4 +- src/test/jtx/impl/batch.cpp | 4 +- src/test/jtx/impl/escrow.cpp | 3 +- src/test/jtx/impl/vault.cpp | 4 +- src/test/rpc/AccountObjects_test.cpp | 3 +- src/test/rpc/AccountTx_test.cpp | 11 +- src/test/rpc/LedgerEntry_test.cpp | 31 +- src/test/rpc/Subscribe_test.cpp | 24 +- src/test/rpc/Transaction_test.cpp | 2 +- src/tests/libxrpl/tx/AccountSet.cpp | 15 +- src/xrpld/app/ledger/detail/LocalTxs.cpp | 3 +- src/xrpld/app/misc/NetworkOPs.cpp | 2 +- src/xrpld/app/misc/detail/TxQ.cpp | 8 +- src/xrpld/rpc/handlers/VaultInfo.cpp | 4 +- .../rpc/handlers/account/AccountInfo.cpp | 3 +- src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp | 22 +- 72 files changed, 1026 insertions(+), 679 deletions(-) diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 07493da0bd..0836cffaf7 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -21,8 +22,6 @@ #include namespace xrpl { - -class SeqProxy; /** * Keylet computation functions. * @@ -123,7 +122,7 @@ trustLine(AccountID const& id, Issue const& issue) noexcept */ /** @{ */ Keylet -offer(AccountID const& id, std::uint32_t seq) noexcept; +offer(AccountID const& id, SeqProxy const& seq) noexcept; inline Keylet offer(uint256 const& key) noexcept @@ -136,7 +135,7 @@ offer(uint256 const& key) noexcept * The initial directory page for a specific quality */ Keylet -quality(Keylet const& k, std::uint64_t q) noexcept; +quality(Keylet const& k, std::uint64_t const q) noexcept; /** * The directory for the next lower quality @@ -149,10 +148,7 @@ next(Keylet const& k); */ /** @{ */ Keylet -ticket(AccountID const& id, std::uint32_t ticketSeq); - -Keylet -ticket(AccountID const& id, SeqProxy ticketSeq); +ticket(AccountID const& id, SeqProxy const& ticketSeq); inline Keylet ticket(uint256 const& key) @@ -178,7 +174,7 @@ sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept; */ /** @{ */ Keylet -check(AccountID const& id, std::uint32_t seq) noexcept; +check(AccountID const& id, SeqProxy const& seq) noexcept; inline Keylet check(uint256 const& key) noexcept @@ -225,10 +221,10 @@ ownerDir(AccountID const& id) noexcept; */ /** @{ */ Keylet -page(uint256 const& root, std::uint64_t index = 0) noexcept; +page(uint256 const& root, std::uint64_t const index = 0) noexcept; inline Keylet -page(Keylet const& root, std::uint64_t index = 0) noexcept +page(Keylet const& root, std::uint64_t const index = 0) noexcept { XRPL_ASSERT(root.type == ltDIR_NODE, "xrpl::keylet::page : valid root type"); return page(root.key, index); @@ -239,13 +235,13 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept * An escrow entry */ Keylet -escrow(AccountID const& src, std::uint32_t seq) noexcept; +escrow(AccountID const& src, SeqProxy const& seq) noexcept; /** * A PaymentChannel */ Keylet -payChannel(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept; +payChannel(AccountID const& src, AccountID const& dst, SeqProxy const& seq) noexcept; /** * NFT page keylets @@ -276,7 +272,7 @@ nftokenPage(Keylet const& k, uint256 const& token); * An offer from an account to buy or sell an NFT */ Keylet -nftokenOffer(AccountID const& owner, std::uint32_t seq); +nftokenOffer(AccountID const& owner, SeqProxy const& seq); inline Keylet nftokenOffer(uint256 const& offer) @@ -316,17 +312,17 @@ bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); // `seq` is stored as `sfXChainClaimID` in the object Keylet -xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq); +xChainClaimID(STXChainBridge const& bridge, std::uint64_t const seq); // `seq` is stored as `sfXChainAccountCreateCount` in the object Keylet -xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq); +xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t const seq); Keylet did(AccountID const& account) noexcept; Keylet -oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; +oracle(AccountID const& account, std::uint32_t const documentID) noexcept; Keylet credential(AccountID const& subject, AccountID const& issuer, Slice const& credType) noexcept; @@ -337,9 +333,6 @@ credential(uint256 const& key) noexcept return {ltCREDENTIAL, key}; } -Keylet -mptokenIssuance(std::uint32_t seq, AccountID const& issuer) noexcept; - Keylet mptokenIssuance(MPTID const& issuanceID) noexcept; @@ -362,7 +355,7 @@ Keylet mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; Keylet -vault(AccountID const& owner, std::uint32_t seq) noexcept; +vault(AccountID const& owner, SeqProxy const& seq) noexcept; inline Keylet vault(uint256 const& vaultKey) @@ -371,7 +364,7 @@ vault(uint256 const& vaultKey) } Keylet -loanBroker(AccountID const& owner, std::uint32_t seq) noexcept; +loanBroker(AccountID const& owner, SeqProxy const& seq) noexcept; inline Keylet loanBroker(uint256 const& key) @@ -380,7 +373,7 @@ loanBroker(uint256 const& key) } Keylet -loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept; +loan(uint256 const& loanBrokerID, SeqProxy const& loanSeq) noexcept; inline Keylet loan(uint256 const& key) @@ -389,7 +382,7 @@ loan(uint256 const& key) } Keylet -permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; +permissionedDomain(AccountID const& account, SeqProxy const& seq) noexcept; Keylet permissionedDomain(uint256 const& domainID) noexcept; @@ -407,12 +400,6 @@ getQualityNext(uint256 const& uBase); std::uint64_t getQuality(uint256 const& uBase); -uint256 -getTicketIndex(AccountID const& account, std::uint32_t uSequence); - -uint256 -getTicketIndex(AccountID const& account, SeqProxy ticketSeq); - template // NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) struct KeyletDesc @@ -426,6 +413,6 @@ struct KeyletDesc extern std::array, 6> const kDirectAccountKeylets; MPTID -makeMptID(std::uint32_t sequence, AccountID const& account); +makeMptID(std::uint32_t const sequence, AccountID const& account); } // namespace xrpl diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index d329d42eee..e213d4e0b7 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -93,12 +93,6 @@ public: [[nodiscard]] SeqProxy getSeqProxy() const; - /** - * Returns the first non-zero value of (Sequence, TicketSequence). - */ - [[nodiscard]] std::uint32_t - getSeqValue() const; - [[nodiscard]] boost::container::flat_set getMentionedAccounts() const; diff --git a/include/xrpl/protocol/SeqProxy.h b/include/xrpl/protocol/SeqProxy.h index e6a97be0e7..fa72914591 100644 --- a/include/xrpl/protocol/SeqProxy.h +++ b/include/xrpl/protocol/SeqProxy.h @@ -53,14 +53,29 @@ public: operator=(SeqProxy const& other) = default; /** - * Factory function to return a sequence-based SeqProxy + * Factory function to return a sequence-based SeqProxy. + * Outside of tests, this function should only be used for "secondary" transaction sequences, + * e.g. `sfOfferSequence`, or sequence fields in an existing ledger object. DO NOT use this for + * the "primary" sequence of a transaction, `sfSequence`. */ static constexpr SeqProxy - sequence(std::uint32_t v) + rawSequence(std::uint32_t v) { return SeqProxy{Type::Seq, v}; } + /** + * Factory function to return a ticket-based SeqProxy. + * Outside of tests, this function should only be used for "secondary" transaction sequences, + * e.g. `sfOfferSequence`, or sequence fields in an existing ledger object. DO NOT use this for + * the "primary" ticket sequence of a transaction, `sfTicketSequence`. + */ + static constexpr SeqProxy + rawTicket(std::uint32_t v) + { + return SeqProxy{Type::Ticket, v}; + } + [[nodiscard]] constexpr std::uint32_t value() const { diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 589e49d335..f3e4597558 100644 --- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -925,7 +925,7 @@ tokenOfferCreateApply( priorBalance < accountReserve(view, acct, j, {.ownerCountDelta = 1})) return tecINSUFFICIENT_RESERVE; - auto const offerID = keylet::nftokenOffer(acctID, seqProxy.value()); + auto const offerID = keylet::nftokenOffer(acctID, seqProxy); // Create the offer: { diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 95416d0f2a..66fdfd453b 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -180,26 +180,13 @@ getQuality(uint256 const& uBase) return boost::endian::load_big_u64(uBase.end() - 8); } -uint256 -getTicketIndex(AccountID const& account, std::uint32_t ticketSeq) -{ - return indexHash(LedgerNameSpace::Ticket, account, ticketSeq); -} - -uint256 -getTicketIndex(AccountID const& account, SeqProxy ticketSeq) -{ - XRPL_ASSERT(ticketSeq.isTicket(), "xrpl::getTicketIndex : valid input"); - return getTicketIndex(account, ticketSeq.value()); -} - MPTID -makeMptID(std::uint32_t sequence, AccountID const& account) +makeMptID(std::uint32_t const sequence, AccountID const& account) { MPTID u; - sequence = boost::endian::native_to_big(sequence); - memcpy(u.data(), &sequence, sizeof(sequence)); - memcpy(u.data() + sizeof(sequence), account.data(), sizeof(account)); + auto const bigEndianSequence = boost::endian::native_to_big(sequence); + memcpy(u.data(), &bigEndianSequence, sizeof(bigEndianSequence)); + memcpy(u.data() + sizeof(bigEndianSequence), account.data(), sizeof(account)); return u; } @@ -286,13 +273,13 @@ trustLine(AccountID const& id0, AccountID const& id1, Currency const& currency) } Keylet -offer(AccountID const& id, std::uint32_t seq) noexcept +offer(AccountID const& id, SeqProxy const& seq) noexcept { - return {ltOFFER, indexHash(LedgerNameSpace::Offer, id, seq)}; + return {ltOFFER, indexHash(LedgerNameSpace::Offer, id, seq.value())}; } Keylet -quality(Keylet const& k, std::uint64_t q) noexcept +quality(Keylet const& k, std::uint64_t const q) noexcept { XRPL_ASSERT(k.type == ltDIR_NODE, "xrpl::keylet::quality : valid input type"); @@ -320,22 +307,17 @@ next(Keylet const& k) } Keylet -ticket(AccountID const& id, std::uint32_t ticketSeq) +ticket(AccountID const& id, SeqProxy const& seq) { - return {ltTICKET, getTicketIndex(id, ticketSeq)}; -} - -Keylet -ticket(AccountID const& id, SeqProxy ticketSeq) -{ - return {ltTICKET, getTicketIndex(id, ticketSeq)}; + XRPL_ASSERT(seq.isTicket(), "xrpl::keylet::ticket : valid input"); + return {ltTICKET, indexHash(LedgerNameSpace::Ticket, id, seq.value())}; } // This function is presently static, since it's never accessed from anywhere // else. If we ever support multiple pages of signer lists, this would be the // keylet used to locate them. static Keylet -signerList(AccountID const& account, std::uint32_t page) noexcept +signerList(AccountID const& account, std::uint32_t const page) noexcept { return {ltSIGNER_LIST, indexHash(LedgerNameSpace::SignerList, account, page)}; } @@ -353,9 +335,9 @@ sponsorship(AccountID const& sponsor, AccountID const& sponsee) noexcept } Keylet -check(AccountID const& id, std::uint32_t seq) noexcept +check(AccountID const& id, SeqProxy const& seq) noexcept { - return {ltCHECK, indexHash(LedgerNameSpace::Check, id, seq)}; + return {ltCHECK, indexHash(LedgerNameSpace::Check, id, seq.value())}; } Keylet @@ -394,7 +376,7 @@ ownerDir(AccountID const& id) noexcept } Keylet -page(uint256 const& key, std::uint64_t index) noexcept +page(uint256 const& key, std::uint64_t const index) noexcept { if (index == 0) return {ltDIR_NODE, key}; @@ -403,15 +385,15 @@ page(uint256 const& key, std::uint64_t index) noexcept } Keylet -escrow(AccountID const& src, std::uint32_t seq) noexcept +escrow(AccountID const& src, SeqProxy const& seq) noexcept { - return {ltESCROW, indexHash(LedgerNameSpace::Escrow, src, seq)}; + return {ltESCROW, indexHash(LedgerNameSpace::Escrow, src, seq.value())}; } Keylet -payChannel(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept +payChannel(AccountID const& src, AccountID const& dst, SeqProxy const& seq) noexcept { - return {ltPAYCHAN, indexHash(LedgerNameSpace::XRPPaymentChannel, src, dst, seq)}; + return {ltPAYCHAN, indexHash(LedgerNameSpace::XRPPaymentChannel, src, dst, seq.value())}; } Keylet @@ -438,9 +420,9 @@ nftokenPage(Keylet const& k, uint256 const& token) } Keylet -nftokenOffer(AccountID const& owner, std::uint32_t seq) +nftokenOffer(AccountID const& owner, SeqProxy const& seq) { - return {ltNFTOKEN_OFFER, indexHash(LedgerNameSpace::NftokenOffer, owner, seq)}; + return {ltNFTOKEN_OFFER, indexHash(LedgerNameSpace::NftokenOffer, owner, seq.value())}; } Keylet @@ -512,7 +494,7 @@ bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType) } Keylet -xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq) +xChainClaimID(STXChainBridge const& bridge, std::uint64_t const seq) { return { ltXCHAIN_OWNED_CLAIM_ID, @@ -526,7 +508,7 @@ xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq) } Keylet -xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq) +xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t const seq) { return { ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID, @@ -546,17 +528,11 @@ did(AccountID const& account) noexcept } Keylet -oracle(AccountID const& account, std::uint32_t const& documentID) noexcept +oracle(AccountID const& account, std::uint32_t const documentID) noexcept { return {ltORACLE, indexHash(LedgerNameSpace::Oracle, account, documentID)}; } -Keylet -mptokenIssuance(std::uint32_t seq, AccountID const& issuer) noexcept -{ - return mptokenIssuance(makeMptID(seq, issuer)); -} - Keylet mptokenIssuance(MPTID const& issuanceID) noexcept { @@ -582,27 +558,29 @@ credential(AccountID const& subject, AccountID const& issuer, Slice const& credT } Keylet -vault(AccountID const& owner, std::uint32_t seq) noexcept +vault(AccountID const& owner, SeqProxy const& seq) noexcept { - return vault(indexHash(LedgerNameSpace::Vault, owner, seq)); + return vault(indexHash(LedgerNameSpace::Vault, owner, seq.value())); } Keylet -loanBroker(AccountID const& owner, std::uint32_t seq) noexcept +loanBroker(AccountID const& owner, SeqProxy const& seq) noexcept { - return loanBroker(indexHash(LedgerNameSpace::LoanBroker, owner, seq)); + return loanBroker(indexHash(LedgerNameSpace::LoanBroker, owner, seq.value())); } Keylet -loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept +loan(uint256 const& loanBrokerID, SeqProxy const& loanSeq) noexcept { - return loan(indexHash(LedgerNameSpace::Loan, loanBrokerID, loanSeq)); + return loan(indexHash(LedgerNameSpace::Loan, loanBrokerID, loanSeq.value())); } Keylet -permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept +permissionedDomain(AccountID const& account, SeqProxy const& seq) noexcept { - return {ltPERMISSIONED_DOMAIN, indexHash(LedgerNameSpace::PermissionedDomain, account, seq)}; + return { + ltPERMISSIONED_DOMAIN, + indexHash(LedgerNameSpace::PermissionedDomain, account, seq.value())}; } Keylet diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 6aadefee27..7f1e19ea12 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -200,22 +200,16 @@ STTx::getSeqProxy() const { std::uint32_t const seq{getFieldU32(sfSequence)}; if (seq != 0) - return SeqProxy::sequence(seq); + return SeqProxy::rawSequence(seq); - std::optional const ticketSeq{operator[](~sfTicketSequence)}; + std::optional const ticketSeq{at(~sfTicketSequence)}; if (!ticketSeq) { // No TicketSequence specified. Return the Sequence, whatever it is. - return SeqProxy::sequence(seq); + return SeqProxy::rawSequence(seq); } - return SeqProxy{SeqProxy::Type::Ticket, *ticketSeq}; -} - -std::uint32_t -STTx::getSeqValue() const -{ - return getSeqProxy().value(); + return SeqProxy::rawTicket(*ticketSeq); } void @@ -459,7 +453,7 @@ STTx::checkBatchSingleSign(STObject const& batchSigner, std::vector con { XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSingleSign : batch transaction"); Serializer msg; - serializeBatch(msg, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds); + serializeBatch(msg, getAccountID(sfAccount), getSeqProxy().value(), getFlags(), txIds); finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg); return singleSignHelper(batchSigner, msg.slice()); } @@ -553,7 +547,7 @@ STTx::checkBatchMultiSign( // with the stuff that stays constant from signature to signature. auto const batchSignerAccount = batchSigner.getAccountID(sfAccount); Serializer dataStart; - serializeBatch(dataStart, getAccountID(sfAccount), getSeqValue(), getFlags(), txIds); + serializeBatch(dataStart, getAccountID(sfAccount), getSeqProxy().value(), getFlags(), txIds); dataStart.addBitString(batchSignerAccount); return multiSignHelper( batchSigner, diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 4b562692d7..5fc6942e20 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -709,7 +709,7 @@ Transactor::checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j } SeqProxy const tSeqProx = tx.getSeqProxy(); - SeqProxy const aSeq = SeqProxy::sequence((*sle)[sfSequence]); + SeqProxy const aSeq = SeqProxy::rawSequence((*sle)[sfSequence]); if (tSeqProx.isSeq()) { @@ -791,16 +791,17 @@ TER Transactor::consumeSeqProxy(SLE::pointer const& sleAccount) { XRPL_ASSERT(sleAccount, "xrpl::Transactor::consumeSeqProxy : non-null account"); - SeqProxy const seqProx = ctx_.tx.getSeqProxy(); - if (seqProx.isSeq()) + SeqProxy const seqProxy = ctx_.tx.getSeqProxy(); + if (seqProxy.isSeq()) { // Note that if this transaction is a TicketCreate, then // the transaction will modify the account root sfSequence // yet again. - sleAccount->setFieldU32(sfSequence, seqProx.value() + 1); + sleAccount->setFieldU32(sfSequence, seqProxy.value() + 1); return tesSUCCESS; } - return ticketDelete(view(), accountID_, getTicketIndex(accountID_, seqProx), j_); + auto const keylet = keylet::ticket(accountID_, seqProxy); + return ticketDelete(view(), accountID_, keylet.key, j_); } // Remove a single Ticket from the ledger. diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 5af4f621a7..2c05c874d3 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -251,7 +251,7 @@ TxConsequences::TxConsequences(NotTEC pfResult) : isBlocker_(false) , fee_(beast::kZero) , potentialSpend_(beast::kZero) - , seqProx_(SeqProxy::sequence(0)) + , seqProx_(SeqProxy::rawSequence(0)) , sequencesConsumed_(0) { XRPL_ASSERT( diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp b/src/libxrpl/tx/transactors/check/CheckCreate.cpp index cb1d81ba4a..129855e48d 100644 --- a/src/libxrpl/tx/transactors/check/CheckCreate.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp @@ -25,7 +25,6 @@ #include #include -#include #include #include @@ -201,14 +200,14 @@ CheckCreate::doApply() return ret; // Note that we use the value from the sequence or ticket as the // Check sequence. For more explanation see comments in SeqProxy.h. - std::uint32_t const seq = ctx_.tx.getSeqValue(); + auto const seq = ctx_.tx.getSeqProxy(); Keylet const checkKeylet = keylet::check(accountID_, seq); auto sleCheck = std::make_shared(checkKeylet); sleCheck->setAccountID(sfAccount, accountID_); AccountID const dstAccountId = ctx_.tx[sfDestination]; sleCheck->setAccountID(sfDestination, dstAccountId); - sleCheck->setFieldU32(sfSequence, seq); + sleCheck->setFieldU32(sfSequence, seq.value()); sleCheck->setFieldAmount(sfSendMax, ctx_.tx[sfSendMax]); if (auto const srcTag = ctx_.tx[~sfSourceTag]) sleCheck->setFieldU32(sfSourceTag, *srcTag); diff --git a/src/libxrpl/tx/transactors/dex/OfferCancel.cpp b/src/libxrpl/tx/transactors/dex/OfferCancel.cpp index 0dea5fa967..fd19037d4f 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCancel.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCancel.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -57,7 +58,8 @@ OfferCancel::doApply() if (!sle) return tefINTERNAL; // LCOV_EXCL_LINE - if (auto sleOffer = view().peek(keylet::offer(accountID_, offerSequence))) + auto const seqProxy = SeqProxy::rawSequence(offerSequence); + if (auto sleOffer = view().peek(keylet::offer(accountID_, seqProxy))) { JLOG(j_.debug()) << "Trying to cancel offer #" << offerSequence; return offerDelete(view(), sleOffer, ctx_.registry.get().getJournal("View")); diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index b95d1001e1..0492f9c062 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -634,7 +635,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) // Note that we use the value from the sequence or ticket as the // offer sequence. For more explanation see comments in SeqProxy.h. - auto const offerSequence = ctx_.tx.getSeqValue(); + auto const offerSequence = ctx_.tx.getSeqProxy(); // This is the original rate of the offer, and is the rate at which // it will be placed, even if crossing offers change the amounts that @@ -648,7 +649,8 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) // Process a cancellation request that's passed along with an offer. if (cancelSequence) { - auto const sleCancel = sb.peek(keylet::offer(accountID_, *cancelSequence)); + auto const seqProxy = SeqProxy::rawSequence(*cancelSequence); + auto const sleCancel = sb.peek(keylet::offer(accountID_, seqProxy)); // It's not an error to not find the offer to cancel: it might have // been consumed or removed. If it is found, however, it's an error @@ -933,7 +935,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel) auto sleOffer = std::make_shared(offerIndex); sleOffer->setAccountID(sfAccount, accountID_); - sleOffer->setFieldU32(sfSequence, offerSequence); + sleOffer->setFieldU32(sfSequence, offerSequence.value()); sleOffer->setFieldH256(sfBookDirectory, dir.key); sleOffer->setFieldAmount(sfTakerPays, saTakerPays); sleOffer->setFieldAmount(sfTakerGets, saTakerGets); diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index feed43d410..21e6bd2c30 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -92,7 +93,8 @@ EscrowCancel::preclaim(PreclaimContext const& ctx) { if (ctx.view.rules().enabled(featureTokenEscrow)) { - auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]); + auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy); auto const slep = ctx.view.read(k); if (!slep) return tecNO_TARGET; @@ -117,7 +119,8 @@ EscrowCancel::preclaim(PreclaimContext const& ctx) TER EscrowCancel::doApply() { - auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const seqProxy = SeqProxy::rawSequence(ctx_.tx[sfOfferSequence]); + auto const k = keylet::escrow(ctx_.tx[sfOwner], seqProxy); auto const slep = ctx_.view().peek(k); if (!slep) { diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 50f2e8b859..212f9da075 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -476,7 +476,7 @@ EscrowCreate::doApply() // Create escrow in ledger. Note that we use the value from the // sequence or ticket. For more explanation see comments in SeqProxy.h. - Keylet const escrowKeylet = keylet::escrow(accountID_, ctx_.tx.getSeqValue()); + Keylet const escrowKeylet = keylet::escrow(accountID_, ctx_.tx.getSeqProxy()); auto const slep = std::make_shared(escrowKeylet); (*slep)[sfAmount] = amount; (*slep)[sfAccount] = accountID_; @@ -489,7 +489,7 @@ EscrowCreate::doApply() if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) { - (*slep)[sfSequence] = ctx_.tx.getSeqValue(); + (*slep)[sfSequence] = ctx_.tx.getSeqProxy().value(); } if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount)) diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 8bc98c7aa8..5fc0aef853 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -203,7 +204,8 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) if (ctx.view.rules().enabled(featureTokenEscrow)) { - auto const k = keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence]); + auto const seqProxy = SeqProxy::rawSequence(ctx.tx[sfOfferSequence]); + auto const k = keylet::escrow(ctx.tx[sfOwner], seqProxy); auto const slep = ctx.view.read(k); if (!slep) return tecNO_TARGET; @@ -228,7 +230,8 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) TER EscrowFinish::doApply() { - auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const seqProxy = SeqProxy::rawSequence(ctx_.tx[sfOfferSequence]); + auto const k = keylet::escrow(ctx_.tx[sfOwner], seqProxy); auto const slep = ctx_.view().peek(k); if (!slep) { diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index b12cfb692f..d6cda9c326 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -218,7 +218,7 @@ LoanBrokerSet::doApply() } auto const vaultPseudoID = sleVault->at(sfAccount); auto const vaultAsset = sleVault->at(sfAsset); - auto const sequence = tx.getSeqValue(); + auto const sequence = tx.getSeqProxy(); auto owner = view.peek(keylet::account(accountID_)); if (!owner) @@ -253,7 +253,7 @@ LoanBrokerSet::doApply() return ter; // Initialize data fields: - broker->at(sfSequence) = sequence; + broker->at(sfSequence) = sequence.value(); broker->at(sfVaultID) = vaultID; broker->at(sfOwner) = accountID_; broker->at(sfAccount) = pseudoId; diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 95a9581dd3..6533a47916 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -594,7 +595,8 @@ LoanSet::doApply() auto loanSequenceProxy = brokerSle->at(sfLoanSequence); // Create the loan - auto loan = std::make_shared(keylet::loan(brokerID, *loanSequenceProxy)); + auto loan = + std::make_shared(keylet::loan(brokerID, SeqProxy::rawSequence(*loanSequenceProxy))); // Prevent copy/paste errors auto setLoanField = [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) { diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp index b17430948a..26d8ff4f04 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp @@ -169,7 +169,7 @@ PaymentChannelCreate::doApply() // // Note that we use the value from the sequence or ticket as the // payChan sequence. For more explanation see comments in SeqProxy.h. - Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqValue()); + Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqProxy()); auto const slep = std::make_shared(payChanKeylet); // Funds held in this channel @@ -185,7 +185,7 @@ PaymentChannelCreate::doApply() (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; if (ctx_.view().rules().enabled(fixIncludeKeyletFields)) { - (*slep)[sfSequence] = ctx_.tx.getSeqValue(); + (*slep)[sfSequence] = ctx_.tx.getSeqProxy().value(); } ctx_.view().insert(slep); diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp index 61ebdcf9c7..36c324eb80 100644 --- a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -114,12 +115,13 @@ PermissionedDomainSet::doApply() return tecINSUFFICIENT_RESERVE; bool const fixEnabled = view().rules().enabled(fixCleanup3_1_3); - auto const seq = fixEnabled ? ctx_.tx.getSeqValue() : ctx_.tx.getFieldU32(sfSequence); + auto const seq = fixEnabled ? ctx_.tx.getSeqProxy() + : SeqProxy::rawSequence(ctx_.tx.getFieldU32(sfSequence)); Keylet const pdKeylet = keylet::permissionedDomain(accountID_, seq); auto slePd = std::make_shared(pdKeylet); slePd->setAccountID(sfOwner, accountID_); - slePd->setFieldU32(sfSequence, seq); + slePd->setFieldU32(sfSequence, seq.value()); slePd->peekFieldArray(sfAcceptedCredentials) = std::move(sortedLE); auto const page = view().dirInsert(keylet::ownerDir(accountID_), pdKeylet, describeOwnerDir(accountID_)); diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp b/src/libxrpl/tx/transactors/system/TicketCreate.cpp index e19dc9fe96..8844d325a8 100644 --- a/src/libxrpl/tx/transactors/system/TicketCreate.cpp +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp @@ -99,7 +99,7 @@ TicketCreate::doApply() for (std::uint32_t i = 0; i < ticketCount; ++i) { std::uint32_t const curTicketSeq = firstTicketSeq + i; - Keylet const ticketKeylet = keylet::ticket(accountID_, curTicketSeq); + Keylet const ticketKeylet = keylet::ticket(accountID_, SeqProxy::rawTicket(curTicketSeq)); SLE::pointer const sleTicket = std::make_shared(ticketKeylet); sleTicket->setAccountID(sfAccount, accountID_); diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index 375110c330..cd0f839030 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -216,7 +216,7 @@ MPTokenIssuanceCreate::doApply() { .priorBalance = preFeeBalance_, .account = accountID_, - .sequence = tx.getSeqValue(), + .sequence = tx.getSeqProxy().value(), .flags = tx.getFlags(), .maxAmount = tx[~sfMaximumAmount], .assetScale = tx[~sfAssetScale], diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index efb0d57c42..f74a27c39b 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -131,7 +131,7 @@ VaultCreate::preclaim(PreclaimContext const& ctx) return tecOBJECT_NOT_FOUND; } - auto const sequence = ctx.tx.getSeqValue(); + auto const sequence = ctx.tx.getSeqProxy(); if (auto const accountId = pseudoAccountAddress(ctx.view, keylet::vault(account, sequence).key); accountId == beast::kZero) return terADDRESS_COLLISION; @@ -148,7 +148,7 @@ VaultCreate::doApply() auto const& tx = ctx_.tx; auto applyViewContext = ctx_.getApplyViewContext(); - auto const sequence = tx.getSeqValue(); + auto const sequence = tx.getSeqProxy(); auto const owner = view().peek(keylet::account(accountID_)); if (owner == nullptr) return tefINTERNAL; // LCOV_EXCL_LINE @@ -218,7 +218,7 @@ VaultCreate::doApply() vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset}); vault->at(sfFlags) = tx.getFlags() & tfVaultPrivate; - vault->at(sfSequence) = sequence; + vault->at(sfSequence) = sequence.value(); vault->at(sfOwner) = accountID_; vault->at(sfAccount) = pseudoId; vault->at(sfAssetsTotal) = Number(0); diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 8fbb786caf..15668d4d71 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -216,8 +217,10 @@ public: BEAST_EXPECT(env.closed()->exists(keylet::account(carol.id()))); BEAST_EXPECT(env.closed()->exists(keylet::ownerDir(carol.id()))); BEAST_EXPECT(env.closed()->exists(keylet::depositPreauth(carol.id(), becky.id()))); - BEAST_EXPECT(env.closed()->exists(keylet::offer(carol.id(), carolOfferSeq))); - BEAST_EXPECT(env.closed()->exists(keylet::ticket(carol.id(), carolTicketSeq))); + BEAST_EXPECT(env.closed()->exists( + keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq)))); + BEAST_EXPECT(env.closed()->exists( + keylet::ticket(carol.id(), SeqProxy::rawTicket(carolTicketSeq)))); BEAST_EXPECT(env.closed()->exists(keylet::signerList(carol.id()))); // Delete carol's account even with stuff in her directory. Show @@ -230,8 +233,10 @@ public: BEAST_EXPECT(!env.closed()->exists(keylet::account(carol.id()))); BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(carol.id()))); BEAST_EXPECT(!env.closed()->exists(keylet::depositPreauth(carol.id(), becky.id()))); - BEAST_EXPECT(!env.closed()->exists(keylet::offer(carol.id(), carolOfferSeq))); - BEAST_EXPECT(!env.closed()->exists(keylet::ticket(carol.id(), carolTicketSeq))); + BEAST_EXPECT(!env.closed()->exists( + keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq)))); + BEAST_EXPECT(!env.closed()->exists( + keylet::ticket(carol.id(), SeqProxy::rawTicket(carolTicketSeq)))); BEAST_EXPECT(!env.closed()->exists(keylet::signerList(carol.id()))); // Verify that Carol's XRP, minus the fee, was transferred to becky. @@ -325,7 +330,7 @@ public: // alice writes a check to becky. Until that check is cashed or // canceled it will prevent alice's and becky's accounts from being // deleted. - uint256 const checkId = keylet::check(alice, env.seq(alice)).key; + uint256 const checkId = keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(check::create(alice, becky, XRP(1))); env.close(); @@ -388,7 +393,8 @@ public: env(escrow::cancel(becky, alice, escrowSeq)); env.close(); - Keylet const alicePayChanKey{keylet::payChannel(alice, becky, env.seq(alice))}; + Keylet const alicePayChanKey{ + keylet::payChannel(alice, becky, SeqProxy::rawSequence(env.seq(alice)))}; env(payChanCreate(alice, becky, XRP(57), 4s, env.now() + 2s, alice.pk())); env.close(); @@ -419,7 +425,8 @@ public: // gw creates a PayChannel with alice as the destination, this should // prevent alice from deleting her account. - Keylet const gwPayChanKey{keylet::payChannel(gw, alice, env.seq(gw))}; + Keylet const gwPayChanKey{ + keylet::payChannel(gw, alice, SeqProxy::rawSequence(env.seq(gw)))}; env(payChanCreate(gw, alice, XRP(68), 4s, env.now() + 2s, alice.pk())); env.close(); @@ -505,7 +512,10 @@ public: // alice's offers. for (std::uint32_t i{0}; i < kOfferCount; ++i) - BEAST_EXPECT(closed->exists(keylet::offer(alice.id(), offerSeq0 + i))); + { + BEAST_EXPECT(closed->exists( + keylet::offer(alice.id(), SeqProxy::rawSequence(offerSeq0 + i)))); + } } // Delete alice's account. Should fail because she has too many @@ -539,7 +549,10 @@ public: // alice's former offers. for (std::uint32_t i{0}; i < kOfferCount; ++i) - BEAST_EXPECT(!closed->exists(keylet::offer(alice.id(), offerSeq0 + i))); + { + BEAST_EXPECT(!closed->exists( + keylet::offer(alice.id(), SeqProxy::rawSequence(offerSeq0 + i)))); + } } } @@ -664,7 +677,8 @@ public: BEAST_EXPECT(closed->exists(keylet::account(bob.id()))); for (std::uint32_t i = 0; i < 250; ++i) { - BEAST_EXPECT(closed->exists(keylet::ticket(bob.id(), ticketSeq + i))); + BEAST_EXPECT( + closed->exists(keylet::ticket(bob.id(), SeqProxy::rawTicket(ticketSeq + i)))); } } @@ -683,7 +697,8 @@ public: BEAST_EXPECT(!closed->exists(keylet::account(bob.id()))); for (std::uint32_t i = 0; i < 250; ++i) { - BEAST_EXPECT(!closed->exists(keylet::ticket(bob.id(), ticketSeq + i))); + BEAST_EXPECT( + !closed->exists(keylet::ticket(bob.id(), SeqProxy::rawTicket(ticketSeq + i)))); } } } diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 5230a1f7dd..c332b26a5b 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -166,7 +167,7 @@ class Batch_test : public beast::unit_test::Suite static uint256 getCheckIndex(AccountID const& account, std::uint32_t uSequence) { - return keylet::check(account, uSequence).key; + return keylet::check(account, SeqProxy::rawSequence(uSequence)).key; } static std::unique_ptr @@ -648,7 +649,7 @@ class Batch_test : public beast::unit_test::Suite serializeBatch( msg, jt.stx->getAccountID(sfAccount), - jt.stx->getSeqValue(), + jt.stx->getSeqProxy().value(), tfAllOrNothing, jt.stx->getBatchTransactionIDs()); finishMultiSigningData(bob.id(), msg); @@ -3176,7 +3177,8 @@ class Batch_test : public beast::unit_test::Suite env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); env.close(); - auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); + auto const brokerKeylet = + keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender))); { using namespace loan_broker; @@ -3198,7 +3200,7 @@ class Batch_test : public beast::unit_test::Suite auto const lenderSeq = env.seq(lender); auto const batchFee = batch::calcBatchFee(env, 0, 2); - auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1)); { auto const [txIDs, batchID] = submitBatch( env, diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index 840c06bd84..364f66c03a 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -53,7 +54,7 @@ class Check_test : public beast::unit_test::Suite static uint256 getCheckIndex(AccountID const& account, std::uint32_t uSequence) { - return keylet::check(account, uSequence).key; + return keylet::check(account, SeqProxy::rawSequence(uSequence)).key; } // Helper function that returns the Checks on an account. diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 4015f5ddc8..d0decaf497 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -943,7 +944,7 @@ struct EscrowToken_test : public beast::unit_test::Suite if (env.current()->rules().enabled(fixCleanup3_2_0)) { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); BEAST_EXPECT(env.current()->exists(trustLineKey)); BEAST_EXPECT(env.balance(alice, usd) == usd(1'000)); } @@ -1072,7 +1073,7 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + auto const aa = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(aa); { xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); @@ -1096,7 +1097,7 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + auto const bb = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))); BEAST_EXPECT(bb); { @@ -1118,7 +1119,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1144,7 +1145,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::cancel(bob, bob, bseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)))); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1188,10 +1189,10 @@ struct EscrowToken_test : public beast::unit_test::Suite (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + auto const ab = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(ab); - auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + auto const bc = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))); BEAST_EXPECT(bc); { @@ -1229,8 +1230,8 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); - BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); @@ -1263,8 +1264,8 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::cancel(bob, bob, bseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); - BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); @@ -1320,7 +1321,7 @@ struct EscrowToken_test : public beast::unit_test::Suite Ter(tecNO_PERMISSION)); env.close(5s); - auto const ag = env.le(keylet::escrow(alice.id(), aseq)); + auto const ag = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(ag); { @@ -1343,7 +1344,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); @@ -2702,7 +2703,8 @@ struct EscrowToken_test : public beast::unit_test::Suite auto const seq1 = env.seq(alice); env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { Sandbox sb(&view, TapNone); - auto sleNew = std::make_shared(keylet::escrow(alice, seq1)); + auto sleNew = + std::make_shared(keylet::escrow(alice, SeqProxy::rawSequence(seq1))); MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; STAmount const amt(mpt, 10); sleNew->setAccountID(sfDestination, bob); @@ -2929,7 +2931,8 @@ struct EscrowToken_test : public beast::unit_test::Suite auto const seq1 = env.seq(alice); env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) { Sandbox sb(&view, TapNone); - auto sleNew = std::make_shared(keylet::escrow(alice, seq1)); + auto sleNew = + std::make_shared(keylet::escrow(alice, SeqProxy::rawSequence(seq1))); MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}}; STAmount const amt(mpt, 10); sleNew->setAccountID(sfDestination, bob); @@ -3280,7 +3283,7 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + auto const aa = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(aa); { xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); @@ -3304,7 +3307,7 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const bb = env.le(keylet::escrow(bob.id(), bseq)); + auto const bb = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))); BEAST_EXPECT(bb); { @@ -3318,7 +3321,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -3338,7 +3341,7 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::cancel(bob, bob, bseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)))); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -3379,10 +3382,10 @@ struct EscrowToken_test : public beast::unit_test::Suite (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + auto const ab = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(ab); - auto const bc = env.le(keylet::escrow(bob.id(), bseq)); + auto const bc = env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq))); BEAST_EXPECT(bc); { @@ -3411,8 +3414,8 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); - BEAST_EXPECT(env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); + BEAST_EXPECT(env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); @@ -3436,8 +3439,8 @@ struct EscrowToken_test : public beast::unit_test::Suite env.close(5s); env(escrow::cancel(bob, bob, bseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); - BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); + BEAST_EXPECT(!env.le(keylet::escrow(bob.id(), SeqProxy::rawSequence(bseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 1); diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index 5623bc4443..8a0d651004 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -230,7 +231,7 @@ struct Escrow_test : public beast::unit_test::Suite Stag(1), Dtag(2)); - auto const sle = env.le(keylet::escrow(alice.id(), seq)); + auto const sle = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))); BEAST_EXPECT(sle); BEAST_EXPECT((*sle)[sfSourceTag] == 1); BEAST_EXPECT((*sle)[sfDestinationTag] == 2); @@ -773,7 +774,8 @@ struct Escrow_test : public beast::unit_test::Suite Fee(150 * baseFee)); // SLE removed on finish - BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); + BEAST_EXPECT( + !env.le(keylet::escrow(Account("alice").id(), SeqProxy::rawSequence(seq)))); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); env.require(Balance("carol", XRP(6000))); env(escrow::cancel("bob", "alice", seq), Ter(tecNO_TARGET)); @@ -795,7 +797,8 @@ struct Escrow_test : public beast::unit_test::Suite env(escrow::cancel("bob", "alice", seq)); env.require(Balance("alice", XRP(5000) - drops(baseFee))); // SLE removed on cancel - BEAST_EXPECT(!env.le(keylet::escrow(Account("alice").id(), seq))); + BEAST_EXPECT( + !env.le(keylet::escrow(Account("alice").id(), SeqProxy::rawSequence(seq)))); } { Env env(*this, features); @@ -1117,7 +1120,7 @@ struct Escrow_test : public beast::unit_test::Suite BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const aa = env.le(keylet::escrow(alice.id(), aseq)); + auto const aa = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(aa); { @@ -1134,7 +1137,7 @@ struct Escrow_test : public beast::unit_test::Suite BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const bb = env.le(keylet::escrow(bruce.id(), bseq)); + auto const bb = env.le(keylet::escrow(bruce.id(), SeqProxy::rawSequence(bseq))); BEAST_EXPECT(bb); { @@ -1148,7 +1151,7 @@ struct Escrow_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1168,7 +1171,7 @@ struct Escrow_test : public beast::unit_test::Suite env.close(5s); env(escrow::cancel(bruce, bruce, bseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), SeqProxy::rawSequence(bseq)))); BEAST_EXPECT( (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); @@ -1198,10 +1201,10 @@ struct Escrow_test : public beast::unit_test::Suite (*env.meta())[sfTransactionResult] == static_cast(tesSUCCESS)); env.close(5s); - auto const ab = env.le(keylet::escrow(alice.id(), aseq)); + auto const ab = env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq))); BEAST_EXPECT(ab); - auto const bc = env.le(keylet::escrow(bruce.id(), bseq)); + auto const bc = env.le(keylet::escrow(bruce.id(), SeqProxy::rawSequence(bseq))); BEAST_EXPECT(bc); { @@ -1230,8 +1233,8 @@ struct Escrow_test : public beast::unit_test::Suite env.close(5s); env(escrow::finish(alice, alice, aseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); - BEAST_EXPECT(env.le(keylet::escrow(bruce.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); + BEAST_EXPECT(env.le(keylet::escrow(bruce.id(), SeqProxy::rawSequence(bseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 0); @@ -1255,8 +1258,8 @@ struct Escrow_test : public beast::unit_test::Suite env.close(5s); env(escrow::cancel(bruce, bruce, bseq)); { - BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), aseq))); - BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), bseq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(aseq)))); + BEAST_EXPECT(!env.le(keylet::escrow(bruce.id(), SeqProxy::rawSequence(bseq)))); xrpl::Dir const aod(*env.current(), keylet::ownerDir(alice.id())); BEAST_EXPECT(std::distance(aod.begin(), aod.end()) == 0); diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index 9be01b2abe..d73ab9b6c7 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -375,7 +376,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite dariaNFTs.reserve(32); for (int i = 0; i < 32; ++i) { - uint256 const offerIndex = keylet::nftokenOffer(carol, env.seq(carol)).key; + uint256 const offerIndex = + keylet::nftokenOffer(carol, SeqProxy::rawSequence(env.seq(carol))).key; env(token::createOffer(carol, carolNFTs.back(), XRP(0)), Txflags(tfSellNFToken)); env.close(); @@ -409,7 +411,8 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite // back from daria. for (uint256 const& nft : dariaNFTs) { - uint256 const offerIndex = keylet::nftokenOffer(carol, env.seq(carol)).key; + uint256 const offerIndex = + keylet::nftokenOffer(carol, SeqProxy::rawSequence(env.seq(carol))).key; env(token::createOffer(carol, nft, drops(1)), token::Owner(daria)); env.close(); diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index 302e55a2cc..a94834eb28 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -408,7 +409,7 @@ struct FlowMPT_test : public beast::unit_test::Suite env(pay(gw, alice, usd(1'000))); env(pay(gw, bob, eur(1'000))); - Keylet const bobUsdOffer = keylet::offer(bob, env.seq(bob)); + Keylet const bobUsdOffer = keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob))); env(offer(bob, usd(10), drops(2)), Txflags(tfPassive)); env(offer(bob, drops(1), eur(1'000)), Txflags(tfPassive)); diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index 8d5162394e..5f12d54aec 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -547,7 +548,7 @@ struct Flow_test : public beast::unit_test::Suite env(pay(gw, alice, usd(1000))); env(pay(gw, bob, eur(1000))); - Keylet const bobUsdOffer = keylet::offer(bob, env.seq(bob)); + Keylet const bobUsdOffer = keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob))); env(offer(bob, usd(1), drops(2)), Txflags(tfPassive)); env(offer(bob, drops(1), eur(1000)), Txflags(tfPassive)); diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 786f5b4680..79087dacc3 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -1788,7 +1789,7 @@ class Freeze_test : public beast::unit_test::Suite env(token::mint(a2, 0), Txflags(tfTransferable)); env.close(); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(10)), token::Owner(a2)); env.close(); @@ -1874,10 +1875,11 @@ class Freeze_test : public beast::unit_test::Suite env(token::mint(a2, 0), Txflags(tfTransferable)); env.close(); - uint256 const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key; + uint256 const sellIdx = + keylet::nftokenOffer(a2, SeqProxy::rawSequence(env.seq(a2))).key; env(token::createOffer(a2, nftID, usd(10)), Txflags(tfSellNFToken)); env.close(); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2)); env.close(); @@ -1900,13 +1902,15 @@ class Freeze_test : public beast::unit_test::Suite env(token::mint(minter, 0), token::XferFee(1u), Txflags(tfTransferable)); env.close(); - uint256 const minterSellIdx = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellIdx = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, drops(1)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(a2, minterSellIdx)); env.close(); - uint256 const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key; + uint256 const sellIdx = + keylet::nftokenOffer(a2, SeqProxy::rawSequence(env.seq(a2))).key; env(token::createOffer(a2, nftID, usd(100)), Txflags(tfSellNFToken)); env.close(); env(trust(g1, minter["USD"](1000), tfSetFreeze | tfSetDeepFreeze)); @@ -1946,7 +1950,7 @@ class Freeze_test : public beast::unit_test::Suite static uint256 getCheckIndex(AccountID const& account, std::uint32_t uSequence) { - return keylet::check(account, uSequence).key; + return keylet::check(account, SeqProxy::rawSequence(uSequence)).key; } static uint256 @@ -1960,7 +1964,8 @@ class Freeze_test : public beast::unit_test::Suite env(token::mint(account, 0), Txflags(tfTransferable)); env.close(); - uint256 const sellOfferIndex = keylet::nftokenOffer(account, env.seq(account)).key; + uint256 const sellOfferIndex = + keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key; env(token::createOffer(account, nftID, currency), Txflags(tfSellNFToken)); env.close(); diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 6234d81762..ffdfe6bc83 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -633,8 +634,8 @@ class Invariants_test : public beast::unit_test::Suite // make a dummy escrow ledger entry, then change the type to an // unsupported value so that the valid type invariant check // will fail. - auto const sleNew = - std::make_shared(keylet::escrow(a1, (*sle)[sfSequence] + 2)); + auto const sleNew = std::make_shared( + keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2))); // We don't use ltNICKNAME directly since it's marked deprecated // to prevent accidental use elsewhere. @@ -921,7 +922,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::offer(a1.id(), (*sle)[sfSequence])); + auto sleNew = std::make_shared( + keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence]))); sleNew->setAccountID(sfAccount, a1.id()); sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]); sleNew->setFieldAmount(sfTakerPays, XRP(-1)); @@ -935,7 +937,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::offer(a1.id(), (*sle)[sfSequence])); + auto sleNew = std::make_shared( + keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence]))); sleNew->setAccountID(sfAccount, a1.id()); sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]); sleNew->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -950,7 +953,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::offer(a1.id(), (*sle)[sfSequence])); + auto sleNew = std::make_shared( + keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence]))); sleNew->setAccountID(sfAccount, a1.id()); sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]); sleNew->setFieldAmount(sfTakerPays, XRP(10)); @@ -974,7 +978,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::escrow(a1, (*sle)[sfSequence] + 2)); + auto sleNew = std::make_shared( + keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2))); sleNew->setFieldAmount(sfAmount, XRP(-1)); ac.view().insert(sleNew); return true; @@ -988,7 +993,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::escrow(a1, (*sle)[sfSequence] + 2)); + auto sleNew = std::make_shared( + keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2))); // Use `drops(1)` to bypass a call to STAmount::canonicalize // with an invalid value sleNew->setFieldAmount(sfAmount, kInitialXrp + drops(1)); @@ -1004,7 +1010,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::escrow(a1, (*sle)[sfSequence] + 2)); + auto sleNew = std::make_shared( + keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2))); Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; STAmount const amt(usd, -1); @@ -1021,7 +1028,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::escrow(a1, (*sle)[sfSequence] + 2)); + auto sleNew = std::make_shared( + keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2))); Issue const bad{badCurrency(), AccountID(0x4985601)}; STAmount const amt(bad, 1); @@ -1038,7 +1046,8 @@ class Invariants_test : public beast::unit_test::Suite auto const sle = ac.view().peek(keylet::account(a1.id())); if (!sle) return false; - auto sleNew = std::make_shared(keylet::escrow(a1, (*sle)[sfSequence] + 2)); + auto sleNew = std::make_shared( + keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2))); MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))}; STAmount const amt(mpt, -1); @@ -1463,7 +1472,7 @@ class Invariants_test : public beast::unit_test::Suite std::uint32_t numCreds = 2, std::uint32_t seq = 10) { - Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), seq); + Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), SeqProxy::rawSequence(seq)); auto sle = std::make_shared(pdKeylet); sle->setAccountID(sfOwner, a1); @@ -2005,7 +2014,7 @@ class Invariants_test : public beast::unit_test::Suite makeEnv(features), {{"domain doesn't exist"}}, [](Account const& a1, Account const&, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a1.id(), 10); + Keylet const offerKey = keylet::offer(a1.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a1); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2032,7 +2041,7 @@ class Invariants_test : public beast::unit_test::Suite makeEnv(features), {{"hybrid offer is malformed"}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a2.id(), 10); + Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a2); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2067,7 +2076,7 @@ class Invariants_test : public beast::unit_test::Suite a2, {{"hybrid offer is malformed"}}, [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a2.id(), 10); + Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a2); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2106,7 +2115,7 @@ class Invariants_test : public beast::unit_test::Suite fixEnabled ? std::vector{{"hybrid offer is malformed"}} : std::vector{}, [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a2.id(), 10); + Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a2); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2143,7 +2152,7 @@ class Invariants_test : public beast::unit_test::Suite a2, {{"hybrid offer is malformed"}}, [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a2.id(), 10); + Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a2); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2176,7 +2185,7 @@ class Invariants_test : public beast::unit_test::Suite a2, {{"transaction consumed wrong domains"}}, [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a2.id(), 10); + Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a2); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2213,7 +2222,7 @@ class Invariants_test : public beast::unit_test::Suite a2, {{"domain transaction affected regular offers"}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - Keylet const offerKey = keylet::offer(a2.id(), 10); + Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10)); auto sleOffer = std::make_shared(offerKey); sleOffer->setAccountID(sfAccount, a2); sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10)); @@ -2414,7 +2423,7 @@ class Invariants_test : public beast::unit_test::Suite // Create Loan Broker using namespace loan_broker; - auto const loanBrokerKeylet = keylet::loanBroker(a.id(), env.seq(a)); + auto const loanBrokerKeylet = keylet::loanBroker(a.id(), SeqProxy::rawSequence(env.seq(a))); // Create a Loan Broker with all default values. env(set(a, vaultID), Fee(kIncrement)); @@ -2918,7 +2927,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"vault deletion succeeded without deleting a vault"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -2939,7 +2948,7 @@ class Invariants_test : public beast::unit_test::Suite {"vault updated by a wrong transaction type", "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -2959,7 +2968,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"vault updated by a wrong transaction type"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -2980,7 +2989,7 @@ class Invariants_test : public beast::unit_test::Suite {"vault updated by a wrong transaction type"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const sequence = ac.view().seq(); - auto const vaultKeylet = keylet::vault(a1.id(), sequence); + auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence)); auto sleVault = std::make_shared(vaultKeylet); auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id())); @@ -2997,7 +3006,7 @@ class Invariants_test : public beast::unit_test::Suite {"vault deleted by a wrong transaction type", "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3019,14 +3028,16 @@ class Invariants_test : public beast::unit_test::Suite "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; ac.view().erase(sleVault); } { - auto const keylet = keylet::vault(a2.id(), ac.view().seq()); + auto const keylet = + keylet::vault(a2.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3055,7 +3066,7 @@ class Invariants_test : public beast::unit_test::Suite [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const sequence = ac.view().seq(); auto const insertVault = [&](Account const a) { - auto const vaultKeylet = keylet::vault(a.id(), sequence); + auto const vaultKeylet = keylet::vault(a.id(), SeqProxy::rawSequence(sequence)); auto sleVault = std::make_shared(vaultKeylet); auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id())); @@ -3075,7 +3086,7 @@ class Invariants_test : public beast::unit_test::Suite {"deleted vault must also delete shares", "deleted Vault without deleting its pseudo-account"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3097,7 +3108,7 @@ class Invariants_test : public beast::unit_test::Suite "deleted vault must have no assets outstanding", "deleted vault must have no assets available"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3122,7 +3133,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"vault operation succeeded without modifying a vault"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3209,7 +3220,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"updated vault must have shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3236,7 +3247,7 @@ class Invariants_test : public beast::unit_test::Suite {"vault operation succeeded without updating shares", "assets available must not be greater than assets outstanding"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3264,7 +3275,7 @@ class Invariants_test : public beast::unit_test::Suite "assets available must not be greater than assets outstanding", "assets outstanding must not be negative"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3296,7 +3307,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"violation of vault immutable data"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3312,7 +3323,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"violation of vault immutable data"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3328,7 +3339,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"violation of vault immutable data"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3345,7 +3356,7 @@ class Invariants_test : public beast::unit_test::Suite {"vault transaction must not change loss unrealized", "set must not change assets outstanding"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { sample.lossUnrealized = 13; sample.assetsTotal = 20; @@ -3362,7 +3373,7 @@ class Invariants_test : public beast::unit_test::Suite "between assets outstanding and available", "vault transaction must not change loss unrealized"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 100, [&](Adjustments& sample) { sample.lossUnrealized = 13; })); @@ -3381,7 +3392,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"loss unrealized must not be negative"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { sample.lossUnrealized = -1; })); @@ -3398,7 +3409,7 @@ class Invariants_test : public beast::unit_test::Suite makeEnv(defaultAmendments() - fixCleanup3_4_0), {}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { sample.lossUnrealized = -1; })); @@ -3412,7 +3423,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"set assets outstanding must not exceed assets maximum"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { sample.assetsMaximum = 1; })); @@ -3426,7 +3437,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"assets maximum must not be negative"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { sample.assetsMaximum = -1; })); @@ -3442,7 +3453,7 @@ class Invariants_test : public beast::unit_test::Suite "updated zero sized vault must have no assets outstanding", "updated zero sized vault must have no assets available"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3463,7 +3474,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"updated shares must not exceed maximum"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3484,7 +3495,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"updated shares must not exceed maximum"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {})); auto sleVault = ac.view().peek(keylet); @@ -3511,7 +3522,7 @@ class Invariants_test : public beast::unit_test::Suite "create operation must not have updated a vault", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3537,7 +3548,7 @@ class Invariants_test : public beast::unit_test::Suite "create operation must not have updated a vault", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3564,7 +3575,7 @@ class Invariants_test : public beast::unit_test::Suite "create operation must not have updated a vault", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3588,7 +3599,7 @@ class Invariants_test : public beast::unit_test::Suite "create operation must not have updated a vault", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3616,7 +3627,7 @@ class Invariants_test : public beast::unit_test::Suite "create operation must not have updated a vault", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3640,7 +3651,7 @@ class Invariants_test : public beast::unit_test::Suite "shares issuer must be a pseudo-account", "shares issuer pseudo-account must point back to the vault"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); auto sleVault = ac.view().peek(keylet); if (!sleVault) return false; @@ -3669,7 +3680,7 @@ class Invariants_test : public beast::unit_test::Suite // the invariants holding. Except one: it is created by the // wrong transaction type. auto const sequence = ac.view().seq(); - auto const vaultKeylet = keylet::vault(a1.id(), sequence); + auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence)); auto sleVault = std::make_shared(vaultKeylet); auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id())); @@ -3725,7 +3736,7 @@ class Invariants_test : public beast::unit_test::Suite "shares issuer pseudo-account must point back to the vault"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const sequence = ac.view().seq(); - auto const vaultKeylet = keylet::vault(a1.id(), sequence); + auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence)); auto sleVault = std::make_shared(vaultKeylet); auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id())); @@ -3784,7 +3795,7 @@ class Invariants_test : public beast::unit_test::Suite {"shares issuer and vault pseudo-account must be the same", "shares issuer must exist"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { auto const sequence = ac.view().seq(); - auto const vaultKeylet = keylet::vault(a1.id(), sequence); + auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence)); auto sleVault = std::make_shared(vaultKeylet); auto const vaultPage = ac.view().dirInsert( keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id())); @@ -3825,7 +3836,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"deposit must change vault balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) { sample.vaultAssets.reset(); })); @@ -3838,7 +3849,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"deposit assets outstanding must not exceed assets maximum"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 200, [&](Adjustments& sample) { sample.assetsMaximum = 1; })); @@ -3857,7 +3868,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"deposit must increase vault balance", "deposit must change depositor balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); // Move 10 drops to A4 to enforce total XRP balance auto sleA4 = ac.view().peek(keylet::account(a4.id())); @@ -3887,7 +3898,7 @@ class Invariants_test : public beast::unit_test::Suite "deposit and assets outstanding must add up", "deposit and assets available must add up"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); // Move 10 drops from A2 to A3 to enforce total XRP balance auto sleA3 = ac.view().peek(keylet::account(a3.id())); @@ -3910,7 +3921,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"deposit must change depositor balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); // Move 10 drops from A3 to vault to enforce total XRP balance auto sleA3 = ac.view().peek(keylet::account(a3.id())); @@ -3932,7 +3943,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"deposit must change depositor shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { sample.accountShares.reset(); })); @@ -3946,7 +3957,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"deposit must change vault shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments& sample) { sample.sharesTotal = 0; @@ -3964,7 +3975,7 @@ class Invariants_test : public beast::unit_test::Suite "deposit must not change vault balance by more than deposited " "amount"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { sample.accountShares->amount = -5; sample.sharesTotal = -10; @@ -3983,7 +3994,7 @@ class Invariants_test : public beast::unit_test::Suite (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000; ac.view().update(sleA3); - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { sample.assetsTotal = 11; })); @@ -4004,7 +4015,7 @@ class Invariants_test : public beast::unit_test::Suite {"deposit and assets outstanding must add up", "deposit and assets available must add up"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) { sample.assetsTotal = 7; sample.assetsAvailable = 7; @@ -4020,7 +4031,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"withdrawal must change vault balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) { sample.vaultAssets.reset(); })); @@ -4037,7 +4048,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"withdrawal must change one destination balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); // Move 10 drops to A4 to enforce total XRP balance auto sleA4 = ac.view().peek(keylet::account(a4.id())); @@ -4071,7 +4082,7 @@ class Invariants_test : public beast::unit_test::Suite "withdrawal and assets available must add up", }, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); // Move 10 drops from A2 to A3 to enforce total XRP balance auto sleA3 = ac.view().peek(keylet::account(a3.id())); @@ -4094,7 +4105,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"withdrawal must change one destination balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); if (!kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) { *sample.vaultAssets -= 5; }))) @@ -4115,7 +4126,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"withdrawal must change depositor shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) { sample.accountShares.reset(); })); @@ -4129,7 +4140,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"withdrawal must change vault shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [](Adjustments& sample) { sample.sharesTotal = 0; })); @@ -4145,7 +4156,7 @@ class Invariants_test : public beast::unit_test::Suite "withdrawal must change depositor and vault shares by equal " "amount"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) { sample.accountShares->amount = 5; sample.sharesTotal = 10; @@ -4161,7 +4172,7 @@ class Invariants_test : public beast::unit_test::Suite {"withdrawal and assets outstanding must add up", "withdrawal and assets available must add up"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) { sample.assetsTotal = -15; sample.assetsAvailable = -15; @@ -4180,7 +4191,7 @@ class Invariants_test : public beast::unit_test::Suite (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000; ac.view().update(sleA3); - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) { sample.assetsTotal = -7; })); @@ -4248,7 +4259,8 @@ class Invariants_test : public beast::unit_test::Suite "withdrawal must change depositor and vault shares by equal " "amount"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2)); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) { sample.accountShares->amount = 5; })); @@ -4263,7 +4275,8 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"clawback must change vault balance"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2)); return kAdjust(ac.view(), keylet, kArgs(a2.id(), -1, [&](Adjustments& sample) { sample.vaultAssets.reset(); })); @@ -4277,7 +4290,7 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"clawback may only be performed by the asset issuer"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq()); + auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq())); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {})); }, XRPAmount{}, @@ -4289,7 +4302,8 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"clawback may only be performed by the asset issuer"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2)); return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {})); }, XRPAmount{}, @@ -4302,7 +4316,8 @@ class Invariants_test : public beast::unit_test::Suite "clawback must decrease holder shares", "clawback must change vault shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2)); return kAdjust(ac.view(), keylet, kArgs(a4.id(), 10, [&](Adjustments& sample) { sample.sharesTotal = 0; })); @@ -4320,7 +4335,8 @@ class Invariants_test : public beast::unit_test::Suite doInvariantCheck( {"clawback must change holder shares"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2)); return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) { sample.accountShares.reset(); })); @@ -4340,7 +4356,8 @@ class Invariants_test : public beast::unit_test::Suite "clawback and assets outstanding must add up", "clawback and assets available must add up"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { - auto const keylet = keylet::vault(a1.id(), ac.view().seq() - 2); + auto const keylet = + keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2)); return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) { sample.accountShares->amount = -8; sample.assetsTotal = -7; @@ -4398,7 +4415,8 @@ class Invariants_test : public beast::unit_test::Suite if (!sle) return false; - auto sleNew = std::make_shared(keylet::check(a1.id(), (*sle)[sfSequence])); + auto sleNew = std::make_shared( + keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence]))); sleNew->setAccountID(sfAccount, a1.id()); sleNew->setAccountID(sfDestination, a2.id()); sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax)); @@ -4413,7 +4431,8 @@ class Invariants_test : public beast::unit_test::Suite if (!sle) return false; - auto sleNew = std::make_shared(keylet::check(a1.id(), (*sle)[sfSequence])); + auto sleNew = std::make_shared( + keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence]))); sleNew->setAccountID(sfAccount, a1.id()); sleNew->setAccountID(sfDestination, a2.id()); sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax)); @@ -5797,7 +5816,7 @@ class Invariants_test : public beast::unit_test::Suite STTx{ttACCOUNT_SET, [](STObject&) {}}, {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, [&checkID](Account const& a1, Account const& a2, Env& env) { - checkID = keylet::check(a1.id(), env.seq(a1)).key; + checkID = keylet::check(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key; env(check::create(a1, a2, XRP(1))); return true; }); @@ -5923,7 +5942,7 @@ class Invariants_test : public beast::unit_test::Suite OpenView ov{*env.current()}; - auto const vaultKeylet = keylet::vault(a1.id(), ov.seq()); + auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ov.seq())); auto sleVault = std::make_shared(vaultKeylet); sleVault->makeFieldAbsent(sfAccount); ov.rawInsert(sleVault); diff --git a/src/test/app/LPTokenTransfer_test.cpp b/src/test/app/LPTokenTransfer_test.cpp index 2947b3a3ce..e30e37ed98 100644 --- a/src/test/app/LPTokenTransfer_test.cpp +++ b/src/test/app/LPTokenTransfer_test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -310,7 +311,7 @@ class LPTokenTransfer_test : public jtx::AMMTest // carol_ can always create a check with lptoken that has frozen // token - uint256 const carolChkId{keylet::check(carol_, env.seq(carol_)).key}; + uint256 const carolChkId{keylet::check(carol_, SeqProxy::rawSequence(env.seq(carol_))).key}; env(check::create(carol_, bob_, STAmount{lpIssue, 10})); env.close(); @@ -327,7 +328,7 @@ class LPTokenTransfer_test : public jtx::AMMTest env.close(); // bob_ creates a check - uint256 const bobChkId{keylet::check(bob_, env.seq(bob_)).key}; + uint256 const bobChkId{keylet::check(bob_, SeqProxy::rawSequence(env.seq(bob_))).key}; env(check::create(bob_, carol_, STAmount{lpIssue, 10})); env.close(); @@ -359,7 +360,8 @@ class LPTokenTransfer_test : public jtx::AMMTest env.close(); // bob_ creates a sell offer for lptoken - uint256 const sellOfferIndex = keylet::nftokenOffer(bob_, env.seq(bob_)).key; + uint256 const sellOfferIndex = + keylet::nftokenOffer(bob_, SeqProxy::rawSequence(env.seq(bob_))).key; env(token::createOffer(bob_, nftID, STAmount{lpIssue, 10}), Txflags(tfSellNFToken)); env.close(); @@ -420,7 +422,8 @@ class LPTokenTransfer_test : public jtx::AMMTest env.close(); // bob_ creates a buy offer with lptoken despite bob_'s USD is frozen - uint256 const buyOfferIndex = keylet::nftokenOffer(bob_, env.seq(bob_)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(bob_, SeqProxy::rawSequence(env.seq(bob_))).key; env(token::createOffer(bob_, nftID, STAmount{lpIssue, 10}), token::Owner(carol_)); env.close(); diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp index c9adce0305..b392dca758 100644 --- a/src/test/app/MPToken_test.cpp +++ b/src/test/app/MPToken_test.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -2385,7 +2386,7 @@ class MPToken_test : public beast::unit_test::Suite env.submit(tx); env.close(); - auto const checkKeylet = keylet::check(alice.id(), checkSeq); + auto const checkKeylet = keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq)); auto const sleCheck = env.le(checkKeylet); BEAST_EXPECT((sleCheck != nullptr) == !bad.negative); if (sleCheck && !bad.negative) @@ -2413,7 +2414,7 @@ class MPToken_test : public beast::unit_test::Suite env.submit(tx); env.close(); - auto const checkKeylet = keylet::check(alice.id(), checkSeq); + auto const checkKeylet = keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq)); BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative); if (!bad.negative) { @@ -2441,7 +2442,7 @@ class MPToken_test : public beast::unit_test::Suite env.submit(tx); env.close(); - auto const checkKeylet = keylet::check(alice.id(), checkSeq); + auto const checkKeylet = keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq)); BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative); if (!bad.negative) { @@ -2472,7 +2473,7 @@ class MPToken_test : public beast::unit_test::Suite env.submit(tx); env.close(); - auto const checkKeylet = keylet::check(alice.id(), checkSeq); + auto const checkKeylet = keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq)); BEAST_EXPECT((env.le(checkKeylet) != nullptr) == !bad.negative); if (!bad.negative) { @@ -2501,7 +2502,7 @@ class MPToken_test : public beast::unit_test::Suite env.jt( check::cash( bob, - keylet::check(alice.id(), checkSeq).key, + keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq)).key, STAmount{issue, std::uint64_t{1}})), sfAmount, badCashAmount, @@ -2510,7 +2511,8 @@ class MPToken_test : public beast::unit_test::Suite tx.ter = bad.holderSourcePreFixTer; env.submit(tx); env.close(); - BEAST_EXPECT(env.le(keylet::check(alice.id(), checkSeq)) != nullptr); + BEAST_EXPECT( + env.le(keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq))) != nullptr); BEAST_EXPECT( (env.balance(alice, issue).value() == STAmount{MPTAmount{10'000}, issue})); BEAST_EXPECT( @@ -2534,7 +2536,7 @@ class MPToken_test : public beast::unit_test::Suite env.jt( check::cash( bob, - keylet::check(alice.id(), checkSeq).key, + keylet::check(alice.id(), SeqProxy::rawSequence(checkSeq)).key, STAmount{issue, std::uint64_t{1}})), sfAmount, badCashAmount, @@ -2562,7 +2564,9 @@ class MPToken_test : public beast::unit_test::Suite tx.ter = bad.negative ? TER{temBAD_AMOUNT} : TER{tecINSUFFICIENT_FUNDS}; env.submit(tx); env.close(); - BEAST_EXPECT(env.le(keylet::escrow(alice.id(), escrowSeq)) == nullptr); + BEAST_EXPECT( + env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(escrowSeq))) == + nullptr); } { Env env{*this, withFix}; @@ -2963,7 +2967,7 @@ class MPToken_test : public beast::unit_test::Suite auto const issue = makeIssue(env); auto const badAmount = badMPTAmount(issue, bad); - uint256 const fakeVaultId = keylet::vault(gw.id(), 1).key; + uint256 const fakeVaultId = keylet::vault(gw.id(), SeqProxy::rawSequence(1)).key; auto tx = withNonCanonicalMPTAmount( env.jt( Vault::clawback( @@ -6561,7 +6565,7 @@ class MPToken_test : public beast::unit_test::Suite auto const mpt = mptTester["MPT"]; mptTester.authorize({.account = alice}); - uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + uint256 const checkId{keylet::check(gw, SeqProxy::rawSequence(env.seq(gw))).key}; env(check::create(gw, alice, mpt(100)), Ter(temDISABLED)); env.close(); @@ -6582,7 +6586,7 @@ class MPToken_test : public beast::unit_test::Suite mptTester.authorize({.account = alice}); mptTester.pay(gw, alice, 50); - uint256 const checkId{keylet::check(alice, env.seq(alice)).key}; + uint256 const checkId{keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key}; // can create env(check::create(alice, carol, mpt(100))); @@ -6612,7 +6616,7 @@ class MPToken_test : public beast::unit_test::Suite .flags = tfMPTCanTransfer | tfMPTCanTrade}); auto const mpt = mptTester["MPT"]; - uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + uint256 const checkId{keylet::check(gw, SeqProxy::rawSequence(env.seq(gw))).key}; // can create env(check::create(gw, alice, mpt(200))); @@ -6766,7 +6770,7 @@ class MPToken_test : public beast::unit_test::Suite {.env = env, .issuer = gw, .holders = {alice, carol}, .flags = tfMPTCanTrade}); // src is issuer - uint256 checkId{keylet::check(gw, env.seq(gw)).key}; + uint256 checkId{keylet::check(gw, SeqProxy::rawSequence(env.seq(gw))).key}; // can create env(check::create(gw, alice, mpt(100))); @@ -6780,7 +6784,7 @@ class MPToken_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(gw, mpt) == mpt(-100)); // dst is issuer - checkId = keylet::check(alice, env.seq(alice)).key; + checkId = keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key; // can create env(check::create(alice, gw, mpt(100))); @@ -6794,13 +6798,13 @@ class MPToken_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(gw, mpt) == mpt(0)); // neither src nor dst is issuer, can't create - checkId = keylet::check(alice, env.seq(alice)).key; + checkId = keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(check::create(alice, carol, mpt(100)), Ter(tecNO_AUTH)); env.close(); // can create now mpt.set({.account = gw, .flags = tfMPTSetCanTransfer}); - checkId = keylet::check(alice, env.seq(alice)).key; + checkId = keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(check::create(alice, carol, mpt(100))); env.close(); env(pay(gw, alice, mpt(10))); @@ -6824,7 +6828,7 @@ class MPToken_test : public beast::unit_test::Suite .pay = 10, .flags = tfMPTCanTransfer}); - uint256 const checkId{keylet::check(alice, env.seq(alice)).key}; + uint256 const checkId{keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key}; // can create env(check::create(alice, carol, mpt(100))); @@ -6898,7 +6902,7 @@ class MPToken_test : public beast::unit_test::Suite env.fund(XRP(1'000), alice, carol); // src is issuer - uint256 const checkId{keylet::check(alice, env.seq(alice)).key}; + uint256 const checkId{keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key}; // can create env(check::create(alice, carol, mpt(100))); @@ -6926,7 +6930,7 @@ class MPToken_test : public beast::unit_test::Suite auto const mpt = mptTester["MPT"]; mptTester.authorize({.account = alice}); - uint256 const checkId{keylet::check(gw, env.seq(gw)).key}; + uint256 const checkId{keylet::check(gw, SeqProxy::rawSequence(env.seq(gw))).key}; env(check::create(gw, alice, mpt(100))); env.close(); diff --git a/src/test/app/NFTokenAuth_test.cpp b/src/test/app/NFTokenAuth_test.cpp index 66716a13b7..e82a47a5d7 100644 --- a/src/test/app/NFTokenAuth_test.cpp +++ b/src/test/app/NFTokenAuth_test.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -43,7 +44,8 @@ class NFTokenAuth_test : public beast::unit_test::Suite env(token::mint(account, 0), token::XferFee(xfee), Txflags(tfTransferable)); env.close(); - auto const sellIdx = keylet::nftokenOffer(account, env.seq(account)).key; + auto const sellIdx = + keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key; env(token::createOffer(account, nftID, currency), Txflags(tfSellNFToken)); env.close(); @@ -74,7 +76,7 @@ public: env(pay(g1, a1, usd(1000))); auto const [nftID, _] = mintAndOfferNFT(env, a2, drops(1)); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; // It should be possible to create a buy offer even if NFT owner is not // authorized @@ -179,7 +181,7 @@ public: env(pay(g1, a2, usd(10))); env.close(); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(10)), token::Owner(a2)); env.close(); @@ -244,7 +246,7 @@ public: // Authorizing trustline to make an offer creation possible env(trust(g1, usd(0), a2, tfSetfAuth)); env.close(); - auto const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key; + auto const sellIdx = keylet::nftokenOffer(a2, SeqProxy::rawSequence(env.seq(a2))).key; env(token::createOffer(a2, nftID, usd(10)), Txflags(tfSellNFToken)); env.close(); // @@ -268,7 +270,7 @@ public: } else { - auto const sellIdx = keylet::nftokenOffer(a2, env.seq(a2)).key; + auto const sellIdx = keylet::nftokenOffer(a2, SeqProxy::rawSequence(env.seq(a2))).key; // Old behavior: sell offer can be created without authorization env(token::createOffer(a2, nftID, usd(10)), Txflags(tfSellNFToken)); @@ -353,7 +355,7 @@ public: env.close(); auto const [nftID, sellIdx] = mintAndOfferNFT(env, a2, usd(10)); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2)); env.close(); @@ -422,7 +424,7 @@ public: env.close(); auto const [nftID, sellIdx] = mintAndOfferNFT(env, a2, usd(10)); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2)); env.close(); @@ -483,7 +485,7 @@ public: env.close(); auto const [nftID, sellIdx] = mintAndOfferNFT(env, a2, usd(10)); - auto const buyIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + auto const buyIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(11)), token::Owner(a2)); env.close(); @@ -559,7 +561,7 @@ public: auto const [nftID, minterSellIdx] = mintAndOfferNFT(env, minter, drops(1), 1); env(token::acceptSellOffer(a1, minterSellIdx)); - uint256 const sellIdx = keylet::nftokenOffer(a1, env.seq(a1)).key; + uint256 const sellIdx = keylet::nftokenOffer(a1, SeqProxy::rawSequence(env.seq(a1))).key; env(token::createOffer(a1, nftID, usd(100)), Txflags(tfSellNFToken)); if (features[fixEnforceNFTokenTrustlineV2]) diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index 140fe2de15..52565432a9 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -76,7 +77,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite for (uint32_t i = 0; i < tokenCancelCount; ++i) { // Create sell offer - offerIndexes.push_back(keylet::nftokenOffer(owner, env.seq(owner)).key); + offerIndexes.push_back( + keylet::nftokenOffer(owner, SeqProxy::rawSequence(env.seq(owner))).key); env(token::createOffer(owner, nftokenID, drops(1)), Txflags(tfSellNFToken)); env.close(); } @@ -237,7 +239,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite // We do the same work on alice and minter, so make a lambda. auto xferNFT = [&env, &becky](AcctStat& acct, auto& iter) { uint256 const offerIndex = - keylet::nftokenOffer(acct.acct, env.seq(acct.acct)).key; + keylet::nftokenOffer(acct.acct, SeqProxy::rawSequence(env.seq(acct.acct))) + .key; env(token::createOffer(acct, *iter, XRP(0)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(becky, offerIndex)); @@ -871,7 +874,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite } // Becky creates a buy offer - uint256 const beckyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftokenID, drops(1)), token::Owner(alice)); env.close(); @@ -1046,7 +1050,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite env.close(); // Minter creates an offer for the NFToken. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nfts.back(), XRP(0)), Txflags(tfSellNFToken)); env.close(); @@ -1117,7 +1122,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite nfts.pop_back(); // alice creates an offer for the NFToken. - uint256 const aliceOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, last32NFTs.back(), XRP(0)), Txflags(tfSellNFToken)); env.close(); @@ -1151,7 +1157,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite for (uint256 const nftID : last32NFTs) { // minter creates an offer for the NFToken. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 7dd0b14fe5..7770741a36 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -142,7 +143,8 @@ class NFTokenDir_test : public beast::unit_test::Suite std::vector offers; for (uint256 const& nftID : nftIDs) { - offers.emplace_back(keylet::nftokenOffer(issuer, env.seq(issuer)).key); + offers.emplace_back( + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key); env(token::createOffer(issuer, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); } @@ -214,7 +216,8 @@ class NFTokenDir_test : public beast::unit_test::Suite env.close(); // Create an offer to give the NFT to buyer for free. - offers.emplace_back(keylet::nftokenOffer(account, env.seq(account)).key); + offers.emplace_back( + keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), Txflags(tfSellNFToken)); @@ -237,7 +240,8 @@ class NFTokenDir_test : public beast::unit_test::Suite // generates a non-tesSUCCESS error code. for (uint256 const& nftID : nftIDs) { - uint256 const offerID = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerID = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken)); env.close(); @@ -418,7 +422,8 @@ class NFTokenDir_test : public beast::unit_test::Suite env.close(); // Create an offer to give the NFT to buyer for free. - offers.emplace_back(keylet::nftokenOffer(account, env.seq(account)).key); + offers.emplace_back( + keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), Txflags(tfSellNFToken)); @@ -445,7 +450,8 @@ class NFTokenDir_test : public beast::unit_test::Suite // generates a non-tesSUCCESS error code. for (uint256 const& nftID : nftIDs) { - uint256 const offerID = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerID = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken)); env.close(); @@ -648,7 +654,8 @@ class NFTokenDir_test : public beast::unit_test::Suite env.close(); // Create an offer to give the NFT to buyer for free. - offers.emplace_back(keylet::nftokenOffer(account, env.seq(account)).key); + offers.emplace_back( + keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), Txflags(tfSellNFToken)); @@ -684,7 +691,8 @@ class NFTokenDir_test : public beast::unit_test::Suite // a non-tesSUCCESS error code. for (uint256 const& nftID : nftIDs) { - uint256 const offerID = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerID = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(100)), Txflags(tfSellNFToken)); env.close(); @@ -820,7 +828,8 @@ class NFTokenDir_test : public beast::unit_test::Suite env.close(); // Create an offer to give the NFT to buyer for free. - offers[i].emplace_back(keylet::nftokenOffer(account, env.seq(account)).key); + offers[i].emplace_back( + keylet::nftokenOffer(account, SeqProxy::rawSequence(env.seq(account))).key); env(token::createOffer(account, nftID, XRP(0)), token::Destination(buyer), Txflags(tfSellNFToken)); diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index acd54ae26a..a7437eea7f 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -143,7 +144,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite Account const alice{"alice"}; env.fund(XRP(10000), alice); env.close(); - uint256 const aliceOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId1, XRP(1000)), token::Owner(master)); env.close(); @@ -861,7 +863,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, alice) == 1); // This is the offer we'll try to cancel. - uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftAlice0ID, XRP(1)), token::Owner(alice), Ter(tesSUCCESS)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == 1); @@ -904,7 +907,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // List of offer IDs containing zero is invalid. // craftedIndex is not a valid offer index but it is not zero. - auto const craftedIndex = keylet::nftokenOffer(gw, env.seq(gw)).key; + auto const craftedIndex = + keylet::nftokenOffer(gw, SeqProxy::rawSequence(env.seq(gw))).key; env(token::cancelOffer(buyer, {buyerOfferIndex, uint256{}, craftedIndex}), Ter(temMALFORMED)); env.close(); @@ -944,7 +948,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { // gw attempts to cancel a Check as through it is an NFTokenOffer. - auto const gwCheckId = keylet::check(gw, env.seq(gw)).key; + auto const gwCheckId = keylet::check(gw, SeqProxy::rawSequence(env.seq(gw))).key; env(check::create(gw, env.master, XRP(300))); env.close(); @@ -1006,32 +1010,37 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, alice) == aliceCount); // alice creates sell offers for her nfts. - uint256 const plainOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const plainOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftAlice0ID, XRP(10)), Txflags(tfSellNFToken)); env.close(); aliceCount++; BEAST_EXPECT(ownerCount(env, alice) == aliceCount); - uint256 const audOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const audOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftAlice0ID, gwAUD(30)), Txflags(tfSellNFToken)); env.close(); aliceCount++; BEAST_EXPECT(ownerCount(env, alice) == aliceCount); - uint256 const xrpOnlyOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const xrpOnlyOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftXrpOnlyID, XRP(20)), Txflags(tfSellNFToken)); env.close(); aliceCount++; BEAST_EXPECT(ownerCount(env, alice) == aliceCount); - uint256 const noXferOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const noXferOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftNoXferID, XRP(30)), Txflags(tfSellNFToken)); env.close(); aliceCount++; BEAST_EXPECT(ownerCount(env, alice) == aliceCount); // alice creates a sell offer that will expire soon. - uint256 const aliceExpOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceExpOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftNoXferID, XRP(40)), Txflags(tfSellNFToken), token::Expiration(lastClose(env) + 5)); @@ -1040,7 +1049,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, alice) == aliceCount); // buyer creates a Buy offer that will expire soon. - uint256 const buyerExpOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerExpOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftAlice0ID, XRP(40)), token::Owner(alice), token::Expiration(lastClose(env) + 5)); @@ -1108,7 +1118,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, buyer) == buyerCount); // The buy offer must be present in the ledger. - uint256 const missingOfferIndex = keylet::nftokenOffer(alice, 1).key; + uint256 const missingOfferIndex = keylet::nftokenOffer(alice, SeqProxy::rawSequence(1)).key; env(token::acceptBuyOffer(buyer, missingOfferIndex), Ter(tecOBJECT_NOT_FOUND)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == buyerCount); @@ -1171,7 +1181,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // corresponding buy and sell offers. { // buyer creates a buy offer for one of alice's nfts. - uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftAlice0ID, gwAUD(29)), token::Owner(alice)); env.close(); buyerCount++; @@ -1204,7 +1215,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite } { // buyer creates a buy offer for one of alice's nfts. - uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftAlice0ID, gwAUD(31)), token::Owner(alice)); env.close(); buyerCount++; @@ -1243,7 +1255,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // preclaim buy { // buyer creates a buy offer for one of alice's nfts. - uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftAlice0ID, gwAUD(30)), token::Owner(alice)); env.close(); buyerCount++; @@ -1270,7 +1283,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // alice gives her NFT to gw, so alice no longer owns nftAlice0. { - uint256 const offerIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const offerIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftAlice0ID, XRP(0)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(gw, offerIndex)); @@ -1295,7 +1309,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // preclaim sell { // buyer creates a buy offer for one of alice's nfts. - uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftXrpOnlyID, XRP(30)), token::Owner(alice)); env.close(); buyerCount++; @@ -1323,7 +1338,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // buyer attempting to accept one of alice's offers with // insufficient funds. { - uint256 const offerIndex = keylet::nftokenOffer(gw, env.seq(gw)).key; + uint256 const offerIndex = + keylet::nftokenOffer(gw, SeqProxy::rawSequence(env.seq(gw))).key; env(token::createOffer(gw, nftAlice0ID, XRP(0)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(alice, offerIndex)); @@ -1376,7 +1392,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::mint(minter1, 0u), token::Issuer(alice), Txflags(flags)); env.close(); - uint256 const offerIndex = keylet::nftokenOffer(minter1, env.seq(minter1)).key; + uint256 const offerIndex = + keylet::nftokenOffer(minter1, SeqProxy::rawSequence(env.seq(minter1))).key; env(token::createOffer(minter1, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); @@ -1479,13 +1496,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(ownerCount(env, alice) == 2); - uint256 const aliceOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftIOUsOkayID, gwAUD(50)), Txflags(tfSellNFToken)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 3); BEAST_EXPECT(ownerCount(env, buyer) == 1); - uint256 const buyerOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyerOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftIOUsOkayID, gwAUD(50)), token::Owner(alice)); env.close(); BEAST_EXPECT(ownerCount(env, buyer) == 2); @@ -1588,7 +1607,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // becky buys the nft for 1 drop. - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), token::Owner(alice)); env.close(); env(token::acceptBuyOffer(alice, beckyBuyOfferIndex)); @@ -1596,14 +1616,16 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // becky attempts to sell the nft for AUD. TER const createOfferTER = (xferFee != 0u) ? TER(tecNO_LINE) : TER(tesSUCCESS); - uint256 const beckyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken), Ter(createOfferTER)); env.close(); // cheri offers to buy the nft for CAD. - uint256 const cheriOfferIndex = keylet::nftokenOffer(cheri, env.seq(cheri)).key; + uint256 const cheriOfferIndex = + keylet::nftokenOffer(cheri, SeqProxy::rawSequence(env.seq(cheri))).key; env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)), token::Owner(becky), Ter(createOfferTER)); @@ -1641,14 +1663,16 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite break; } // becky buys the nft for 1 drop. - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, drops(1)), token::Owner(alice)); env.close(); env(token::acceptBuyOffer(alice, beckyBuyOfferIndex)); env.close(); // becky sells the nft for AUD. - uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckySellOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(cheri, beckySellOfferIndex)); @@ -1659,7 +1683,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // becky buys the nft back for CAD. uint256 const beckyBuyBackOfferIndex = - keylet::nftokenOffer(becky, env.seq(becky)).key; + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, gwCAD(50)), token::Owner(cheri)); env.close(); env(token::acceptBuyOffer(cheri, beckyBuyBackOfferIndex)); @@ -1679,7 +1703,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // alice sells the nft using AUD. - uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceSellOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftNoAutoTrustID, gwAUD(200)), Txflags(tfSellNFToken)); env.close(); @@ -1696,7 +1721,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite Txflags(tfSellNFToken), Ter(tecNO_LINE)); env.close(); - uint256 const cheriSellOfferIndex = keylet::nftokenOffer(cheri, env.seq(cheri)).key; + uint256 const cheriSellOfferIndex = + keylet::nftokenOffer(cheri, SeqProxy::rawSequence(env.seq(cheri))).key; env(token::createOffer(cheri, nftNoAutoTrustID, gwCAD(100)), Txflags(tfSellNFToken)); env.close(); @@ -1743,7 +1769,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite Ter(tefNFTOKEN_IS_NOT_TRANSFERABLE)); // alice offers to sell the nft and becky accepts the offer. - uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceSellOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftAliceNoTransferID, XRP(20)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(becky, aliceSellOfferIndex)); @@ -1771,7 +1798,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // alice offers to buy the nft back from becky. becky accepts // the offer. - uint256 const aliceBuyOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceBuyOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftAliceNoTransferID, XRP(22)), token::Owner(becky)); env.close(); env(token::acceptBuyOffer(becky, aliceBuyOfferIndex)); @@ -1827,7 +1855,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter successfully offers their nft for sale. BEAST_EXPECT(ownerCount(env, minter) == 1); - uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftMinterNoTransferID, XRP(22)), Txflags(tfSellNFToken)); env.close(); BEAST_EXPECT(ownerCount(env, minter) == 2); @@ -1862,7 +1891,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // alice can create an offer to buy the nft. BEAST_EXPECT(ownerCount(env, alice) == 0); - uint256 const aliceBuyOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceBuyOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftMinterNoTransferID, XRP(25)), token::Owner(becky)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 1); @@ -1877,7 +1907,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Now minter can create an offer to buy the nft. BEAST_EXPECT(ownerCount(env, minter) == 0); - uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterBuyOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftMinterNoTransferID, XRP(26)), token::Owner(becky)); env.close(); BEAST_EXPECT(ownerCount(env, minter) == 1); @@ -1916,12 +1947,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, alice) == 1); // Both alice and becky can make offers for alice's nft. - uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceSellOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftAliceID, XRP(20)), Txflags(tfSellNFToken)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 2); - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAliceID, XRP(21)), token::Owner(alice)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 2); @@ -1933,7 +1966,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, becky) == 2); // becky offers to sell the nft. - uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckySellOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAliceID, XRP(22)), Txflags(tfSellNFToken)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 0); @@ -1948,7 +1982,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, minter) == 1); // minter offers to sell the nft. - uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftAliceID, XRP(23)), Txflags(tfSellNFToken)); env.close(); BEAST_EXPECT(ownerCount(env, alice) == 0); @@ -2030,7 +2065,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // Becky buys the nft for XAU(10). Check balances. - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, gwXAU(10)), token::Owner(alice)); env.close(); BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000)); @@ -2042,7 +2078,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990)); // becky sells nft to carol. alice's balance should not change. - uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckySellOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, gwXAU(10)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(carol, beckySellOfferIndex)); @@ -2052,7 +2089,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990)); // minter buys nft from carol. alice's balance should not change. - uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterBuyOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(10)), token::Owner(carol)); env.close(); env(token::acceptBuyOffer(carol, minterBuyOfferIndex)); @@ -2064,7 +2102,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter sells the nft to alice. gwXAU balances should finish // where they started. - uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(10)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(alice, minterSellOfferIndex)); @@ -2091,7 +2130,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // Becky buys the nft for XAU(10). Check balances. - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, gwXAU(10)), token::Owner(alice)); env.close(); BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000)); @@ -2103,7 +2143,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990)); // becky sells nft to carol. alice's balance goes up. - uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckySellOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, gwXAU(10)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(carol, beckySellOfferIndex)); @@ -2114,7 +2155,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(carol, gwXAU) == gwXAU(990)); // minter buys nft from carol. alice's balance goes up. - uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterBuyOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(10)), token::Owner(carol)); env.close(); env(token::acceptBuyOffer(carol, minterBuyOfferIndex)); @@ -2127,7 +2169,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter sells the nft to alice. Because alice is part of the // transaction no transfer fee is removed. - uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(10)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(alice, minterSellOfferIndex)); @@ -2172,7 +2215,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // Becky buys the nft for XAU(10). Check balances. - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, gwXAU(10)), token::Owner(alice)); env.close(); BEAST_EXPECT(env.balance(alice, gwXAU) == gwXAU(1000)); @@ -2184,7 +2228,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(becky, gwXAU) == gwXAU(990)); // becky sells nft to minter. alice's balance goes up. - uint256 const beckySellOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckySellOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, gwXAU(100)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(minter, beckySellOfferIndex)); @@ -2195,7 +2240,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); // carol buys nft from minter. alice's balance goes up. - uint256 const carolBuyOfferIndex = keylet::nftokenOffer(carol, env.seq(carol)).key; + uint256 const carolBuyOfferIndex = + keylet::nftokenOffer(carol, SeqProxy::rawSequence(env.seq(carol))).key; env(token::createOffer(carol, nftID, gwXAU(10)), token::Owner(minter)); env.close(); env(token::acceptBuyOffer(minter, carolBuyOfferIndex)); @@ -2208,7 +2254,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // carol sells the nft to alice. Because alice is part of the // transaction no transfer fee is removed. - uint256 const carolSellOfferIndex = keylet::nftokenOffer(carol, env.seq(carol)).key; + uint256 const carolSellOfferIndex = + keylet::nftokenOffer(carol, SeqProxy::rawSequence(env.seq(carol))).key; env(token::createOffer(carol, nftID, gwXAU(10)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(alice, carolSellOfferIndex)); @@ -2249,7 +2296,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // alice there should be no transfer fee. STAmount aliceBalance = env.balance(alice); STAmount minterBalance = env.balance(minter); - uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterBuyOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, XRP(1)), token::Owner(alice)); env.close(); env(token::acceptBuyOffer(alice, minterBuyOfferIndex)); @@ -2263,7 +2311,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // alice does not get any transfer fee. auto pmt = drops(50000); STAmount carolBalance = env.balance(carol); - uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, pmt), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(carol, minterSellOfferIndex)); @@ -2277,7 +2326,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // carol sells to becky. This is the smallest amount to pay for a // transfer that enables a transfer fee of 1 basis point. STAmount beckyBalance = env.balance(becky); - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; pmt = drops(50001); env(token::createOffer(becky, nftID, pmt), token::Owner(carol)); env.close(); @@ -2322,7 +2372,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // alice there should be no transfer fee. STAmount aliceBalance = env.balance(alice, gwXAU); STAmount minterBalance = env.balance(minter, gwXAU); - uint256 const minterBuyOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterBuyOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, tinyXAU), token::Owner(alice)); env.close(); env(token::acceptBuyOffer(alice, minterBuyOfferIndex)); @@ -2334,7 +2385,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // minter sells to carol. STAmount carolBalance = env.balance(carol, gwXAU); - uint256 const minterSellOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterSellOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, tinyXAU), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(carol, minterSellOfferIndex)); @@ -2352,7 +2404,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite STAmount const cheapNFT(gwXAU, STAmount::kMinValue, STAmount::kMinOffset + 5); STAmount beckyBalance = env.balance(becky, gwXAU); - uint256 const beckyBuyOfferIndex = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftID, cheapNFT), token::Owner(carol)); env.close(); env(token::acceptBuyOffer(carol, beckyBuyOfferIndex)); @@ -2582,22 +2635,26 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Test how adding a Destination field to an offer affects permissions // for canceling offers. { - uint256 const offerMinterToIssuer = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToIssuer = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(issuer), Txflags(tfSellNFToken)); - uint256 const offerMinterToBuyer = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToBuyer = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(buyer), Txflags(tfSellNFToken)); - uint256 const offerIssuerToMinter = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const offerIssuerToMinter = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftokenID, drops(1)), token::Owner(minter), token::Destination(minter)); - uint256 const offerIssuerToBuyer = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const offerIssuerToBuyer = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftokenID, drops(1)), token::Owner(minter), token::Destination(buyer)); @@ -2639,7 +2696,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // accepting that offer. { uint256 const offerMinterSellsToBuyer = - keylet::nftokenOffer(minter, env.seq(minter)).key; + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(buyer), Txflags(tfSellNFToken)); @@ -2668,7 +2725,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // accepting that offer. { uint256 const offerMinterBuysFromBuyer = - keylet::nftokenOffer(minter, env.seq(minter)).key; + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Owner(buyer), token::Destination(buyer)); @@ -2696,7 +2753,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // destination must act as a broker. The NFToken owner may not // simply accept the offer. uint256 const offerBuyerBuysFromMinter = - keylet::nftokenOffer(buyer, env.seq(buyer)).key; + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID, drops(1)), token::Owner(minter), token::Destination(broker)); @@ -2719,12 +2776,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Show that a sell offer's Destination can broker that sell offer // to another account. { - uint256 const offerMinterToBroker = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToBroker = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(broker), Txflags(tfSellNFToken)); - uint256 const offerBuyerToMinter = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerBuyerToMinter = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID, drops(1)), token::Owner(minter)); env.close(); @@ -2756,15 +2815,18 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Destination doesn't match, but can complete if the Destination // does match. { - uint256 const offerBuyerToMinter = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerBuyerToMinter = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID, drops(1)), token::Destination(minter), Txflags(tfSellNFToken)); - uint256 const offerMinterToBuyer = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToBuyer = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Owner(buyer)); - uint256 const offerIssuerToBuyer = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const offerIssuerToBuyer = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftokenID, drops(1)), token::Owner(buyer)); env.close(); @@ -2812,12 +2874,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Show that if a buy and a sell offer both have the same destination, // then that destination can broker the offers. { - uint256 const offerMinterToBroker = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToBroker = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(broker), Txflags(tfSellNFToken)); - uint256 const offerBuyerToBroker = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerBuyerToBroker = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID, drops(1)), token::Owner(minter), token::Destination(broker)); @@ -2887,7 +2951,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // create offer (allowed now) then cancel { - uint256 const offerIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(buyer), @@ -2900,7 +2965,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // create offer, enable flag, then cancel { - uint256 const offerIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(buyer), @@ -2919,7 +2985,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // create offer then transfer { - uint256 const offerIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID, drops(1)), token::Destination(buyer), @@ -3006,23 +3073,27 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { std::uint32_t const expiration = lastClose(env) + 25; - uint256 const offerMinterToIssuer = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToIssuer = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID0, drops(1)), token::Destination(issuer), token::Expiration(expiration), Txflags(tfSellNFToken)); - uint256 const offerMinterToAnyone = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offerMinterToAnyone = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID0, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); - uint256 const offerIssuerToMinter = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const offerIssuerToMinter = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftokenID0, drops(1)), token::Owner(minter), token::Expiration(expiration)); - uint256 const offerBuyerToMinter = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerBuyerToMinter = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, drops(1)), token::Owner(minter), token::Expiration(expiration)); @@ -3082,13 +3153,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { std::uint32_t const expiration = lastClose(env) + 25; - uint256 const offer0 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offer0 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID0, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); minterCount++; - uint256 const offer1 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const offer1 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID1, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); @@ -3153,7 +3226,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Transfer nftokenID0 back to minter so we start the next test in // a simple place. - uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerSellBack = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, XRP(0)), Txflags(tfSellNFToken), token::Destination(minter)); @@ -3172,13 +3246,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { std::uint32_t const expiration = lastClose(env) + 25; - uint256 const offer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offer0 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, drops(1)), token::Owner(minter), token::Expiration(expiration)); buyerCount++; - uint256 const offer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offer1 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID1, drops(1)), token::Owner(minter), token::Expiration(expiration)); @@ -3241,7 +3317,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Transfer nftokenID0 back to minter so we start the next test in // a simple place. - uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerSellBack = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, XRP(0)), Txflags(tfSellNFToken), token::Destination(minter)); @@ -3260,23 +3337,27 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { std::uint32_t const expiration = lastClose(env) + 25; - uint256 const sellOffer0 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const sellOffer0 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID0, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); minterCount++; - uint256 const sellOffer1 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const sellOffer1 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID1, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); minterCount++; - uint256 const buyOffer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOffer0 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, drops(1)), token::Owner(minter)); buyerCount++; - uint256 const buyOffer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOffer1 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID1, drops(1)), token::Owner(minter)); buyerCount++; @@ -3335,7 +3416,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Transfer nftokenID0 back to minter so we start the next test in // a simple place. - uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerSellBack = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, XRP(0)), Txflags(tfSellNFToken), token::Destination(minter)); @@ -3354,18 +3436,22 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { std::uint32_t const expiration = lastClose(env) + 25; - uint256 const sellOffer0 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const sellOffer0 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID0, drops(1)), Txflags(tfSellNFToken)); - uint256 const sellOffer1 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const sellOffer1 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID1, drops(1)), Txflags(tfSellNFToken)); - uint256 const buyOffer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOffer0 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, drops(1)), token::Expiration(expiration), token::Owner(minter)); - uint256 const buyOffer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOffer1 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID1, drops(1)), token::Expiration(expiration), token::Owner(minter)); @@ -3416,7 +3502,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Transfer nftokenID0 back to minter so we start the next test in // a simple place. - uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerSellBack = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, XRP(0)), Txflags(tfSellNFToken), token::Destination(minter)); @@ -3435,22 +3522,26 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { std::uint32_t const expiration = lastClose(env) + 25; - uint256 const sellOffer0 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const sellOffer0 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID0, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); - uint256 const sellOffer1 = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const sellOffer1 = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftokenID1, drops(1)), token::Expiration(expiration), Txflags(tfSellNFToken)); - uint256 const buyOffer0 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOffer0 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, drops(1)), token::Expiration(expiration), token::Owner(minter)); - uint256 const buyOffer1 = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOffer1 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID1, drops(1)), token::Expiration(expiration), token::Owner(minter)); @@ -3492,7 +3583,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Transfer nftokenID0 back to minter so we start the next test in // a simple place. - uint256 const offerSellBack = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerSellBack = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftokenID0, XRP(0)), Txflags(tfSellNFToken), token::Destination(minter)); @@ -3530,7 +3622,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // Anyone can cancel an expired offer. - uint256 const expiredOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const expiredOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftokenID, XRP(1000)), Txflags(tfSellNFToken), @@ -3552,7 +3645,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Create a couple of offers with a destination. Those offers // should be cancellable by the creator and the destination. - uint256 const dest1OfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const dest1OfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftokenID, XRP(1000)), token::Destination(becky), @@ -3570,7 +3664,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, alice) == 1); // alice can cancel her own offer, even if becky is the destination. - uint256 const dest2OfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const dest2OfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftokenID, XRP(1000)), token::Destination(becky), @@ -3589,7 +3684,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::mint(minter, 0), token::Issuer(alice), Txflags(tfTransferable)); env.close(); - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, mintersNFTokenID, XRP(1000)), Txflags(tfSellNFToken)); env.close(); @@ -3647,7 +3743,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::mint(nftAcct, 0), token::Uri(uri), Txflags(tfTransferable)); env.close(); - offerIndexes.push_back(keylet::nftokenOffer(offerAcct, env.seq(offerAcct)).key); + offerIndexes.push_back( + keylet::nftokenOffer(offerAcct, SeqProxy::rawSequence(env.seq(offerAcct))).key); env(token::createOffer(offerAcct, nftokenID, drops(1)), token::Owner(nftAcct), token::Expiration(lastClose(env) + 5)); @@ -3682,7 +3779,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::mint(alice, 0), token::Uri(uri), Txflags(tfTransferable)); env.close(); - offerIndexes.push_back(keylet::nftokenOffer(alice, env.seq(alice)).key); + offerIndexes.push_back( + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key); env(token::createOffer(alice, nftokenID, drops(1)), Txflags(tfSellNFToken)); env.close(); @@ -3793,13 +3891,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(); // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); // buyer creates their offer. Note: a buy offer can never // offer zero. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter)); env.close(); @@ -3835,13 +3935,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(); // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); // buyer creates their offer. Note: a buy offer can never // offer zero. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter)); env.close(); @@ -3884,13 +3986,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(kMaxTransferFee); // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); // buyer creates their offer. Note: a buy offer can never // offer zero. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter)); env.close(); @@ -3926,13 +4030,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(kMaxTransferFee); // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, XRP(0)), Txflags(tfSellNFToken)); env.close(); // buyer creates their offer. Note: a buy offer can never // offer zero. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, XRP(1)), token::Owner(minter)); env.close(); @@ -3999,14 +4105,16 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(); // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(1000)), Txflags(tfSellNFToken)); env.close(); { // buyer creates an offer for more XAU than they currently // own. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(1001)), token::Owner(minter)); env.close(); @@ -4023,7 +4131,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { // buyer creates an offer for less that what minter is // asking. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(999)), token::Owner(minter)); env.close(); @@ -4039,7 +4148,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite } // buyer creates a large enough offer. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter)); env.close(); @@ -4076,13 +4186,15 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(kMaxTransferFee); // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(900)), Txflags(tfSellNFToken)); env.close(); { // buyer creates an offer for more XAU than they currently // own. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(1001)), token::Owner(minter)); env.close(); @@ -4099,7 +4211,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite { // buyer creates an offer for less that what minter is // asking. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(899)), token::Owner(minter)); env.close(); @@ -4114,7 +4227,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); } // buyer creates a large enough offer. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter)); env.close(); @@ -4154,12 +4268,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(kMaxTransferFee / 2); // 25% // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(900)), Txflags(tfSellNFToken)); env.close(); // buyer creates a large enough offer. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter)); env.close(); @@ -4191,12 +4307,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const nftID = mintNFT(kMaxTransferFee / 2); // 25% // minter creates their offer. - uint256 const minterOfferIndex = keylet::nftokenOffer(minter, env.seq(minter)).key; + uint256 const minterOfferIndex = + keylet::nftokenOffer(minter, SeqProxy::rawSequence(env.seq(minter))).key; env(token::createOffer(minter, nftID, gwXAU(900)), Txflags(tfSellNFToken)); env.close(); // buyer creates a large enough offer. - uint256 const buyOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID, gwXAU(1000)), token::Owner(minter)); env.close(); @@ -4246,9 +4364,11 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(nftCount(env, buyer2) == 0); // Both buyer1 and buyer2 create buy offers for nftId. - uint256 const buyer1OfferIndex = keylet::nftokenOffer(buyer1, env.seq(buyer1)).key; + uint256 const buyer1OfferIndex = + keylet::nftokenOffer(buyer1, SeqProxy::rawSequence(env.seq(buyer1))).key; env(token::createOffer(buyer1, nftId, XRP(100)), token::Owner(issuer)); - uint256 const buyer2OfferIndex = keylet::nftokenOffer(buyer2, env.seq(buyer2)).key; + uint256 const buyer2OfferIndex = + keylet::nftokenOffer(buyer2, SeqProxy::rawSequence(env.seq(buyer2))).key; env(token::createOffer(buyer2, nftId, XRP(100)), token::Owner(issuer)); env.close(); @@ -4336,7 +4456,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // NFTokenCreateOffer BEAST_EXPECT(ownerCount(env, buyer) == 10); - uint256 const offerIndex0 = keylet::nftokenOffer(buyer, buyerTicketSeq).key; + uint256 const offerIndex0 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(buyerTicketSeq)).key; env(token::createOffer(buyer, nftId, XRP(1)), token::Owner(issuer), ticket::Use(buyerTicketSeq++)); @@ -4351,7 +4472,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ticketCount(env, buyer) == 8); // NFTokenCreateOffer. buyer tries again. - uint256 const offerIndex1 = keylet::nftokenOffer(buyer, buyerTicketSeq).key; + uint256 const offerIndex1 = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(buyerTicketSeq)).key; env(token::createOffer(buyer, nftId, XRP(2)), token::Owner(issuer), ticket::Use(buyerTicketSeq++)); @@ -4428,7 +4550,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env(token::createOffer(becky, nftId, XRP(2)), token::Owner(minter)); env.close(); - uint256 const carlaOfferIndex = keylet::nftokenOffer(carla, env.seq(carla)).key; + uint256 const carlaOfferIndex = + keylet::nftokenOffer(carla, SeqProxy::rawSequence(env.seq(carla))).key; env(token::createOffer(carla, nftId, XRP(3)), token::Owner(minter)); env.close(); @@ -4706,25 +4829,29 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite TER const offerCreateTER = temBAD_AMOUNT; // Make offers with negative amounts for the NFTs - uint256 const sellNegXrpOfferIndex = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const sellNegXrpOfferIndex = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftID0, XRP(-2)), Txflags(tfSellNFToken), Ter(offerCreateTER)); env.close(); - uint256 const sellNegIouOfferIndex = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const sellNegIouOfferIndex = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftID1, gwXAU(-2)), Txflags(tfSellNFToken), Ter(offerCreateTER)); env.close(); - uint256 const buyNegXrpOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyNegXrpOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID0, XRP(-1)), token::Owner(issuer), Ter(offerCreateTER)); env.close(); - uint256 const buyNegIouOfferIndex = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const buyNegIouOfferIndex = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::createOffer(buyer, nftID1, gwXAU(-1)), token::Owner(issuer), Ter(offerCreateTER)); @@ -4887,7 +5014,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const& nftID, STAmount const& amount, std::optional const terCode = {}) { - uint256 const offerID = keylet::nftokenOffer(offerer, env.seq(offerer)).key; + uint256 const offerID = + keylet::nftokenOffer(offerer, SeqProxy::rawSequence(env.seq(offerer))).key; env(token::createOffer(offerer, nftID, amount), token::Owner(owner), terCode ? Ter(*terCode) : Ter(static_cast(tesSUCCESS))); @@ -4900,7 +5028,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite uint256 const& nftID, STAmount const& amount, std::optional const terCode = {}) { - uint256 const offerID = keylet::nftokenOffer(offerer, env.seq(offerer)).key; + uint256 const offerID = + keylet::nftokenOffer(offerer, SeqProxy::rawSequence(env.seq(offerer))).key; env(token::createOffer(offerer, nftID, amount), Txflags(tfSellNFToken), terCode ? Ter(*terCode) : Ter(static_cast(tesSUCCESS))); @@ -5413,10 +5542,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Bob creates a buy offer for 5 XRP. Alice creates a sell offer // for 0 XRP. - uint256 const bobBuyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key; + uint256 const bobBuyOfferIndex = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId, XRP(5)), token::Owner(alice)); - uint256 const aliceSellOfferIndex = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceSellOfferIndex = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, XRP(0)), token::Destination(bob), Txflags(tfSellNFToken)); @@ -5430,7 +5561,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(env.le(keylet::nftokenOffer(bobBuyOfferIndex))); // Bob creates a sell offer for the gift NFT from alice. - uint256 const bobSellOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key; + uint256 const bobSellOfferIndex = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId, XRP(4)), Txflags(tfSellNFToken)); env.close(); @@ -6047,7 +6179,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite token::Amount(XRP(10)), token::Destination(buyer), token::Expiration(lastClose(env) + 25)); - uint256 const offerAliceSellsToBuyer = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const offerAliceSellsToBuyer = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::cancelOffer(alice, {offerAliceSellsToBuyer})); env.close(); @@ -6056,7 +6189,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite token::Amount(XRP(10)), token::Destination(alice), token::Expiration(lastClose(env) + 25)); - uint256 const offerBuyerSellsToAlice = keylet::nftokenOffer(buyer, env.seq(buyer)).key; + uint256 const offerBuyerSellsToAlice = + keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key; env(token::cancelOffer(alice, {offerBuyerSellsToAlice})); env.close(); @@ -6207,12 +6341,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Alice creates one sell offer for each NFT // Verify the offer indexes are correct in the NFTokenCreateOffer tx // meta - uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex1 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId1, drops(1)), Txflags(tfSellNFToken)); env.close(); verifyNFTokenOfferID(aliceOfferIndex1); - uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex2 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId2, drops(1)), Txflags(tfSellNFToken)); env.close(); verifyNFTokenOfferID(aliceOfferIndex2); @@ -6226,7 +6362,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // Bobs creates a buy offer for nftId1 // Verify the offer id is correct in the NFTokenCreateOffer tx meta - auto const bobBuyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key; + auto const bobBuyOfferIndex = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId1, drops(1)), token::Owner(alice)); env.close(); verifyNFTokenOfferID(bobBuyOfferIndex); @@ -6247,7 +6384,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite verifyNFTokenID(nftId); // Alice creates sell offer and set broker as destination - uint256 const offerAliceToBroker = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const offerAliceToBroker = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, drops(1)), token::Destination(broker), Txflags(tfSellNFToken)); @@ -6255,7 +6393,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite verifyNFTokenOfferID(offerAliceToBroker); // Bob creates buy offer - uint256 const offerBobToBroker = keylet::nftokenOffer(bob, env.seq(bob)).key; + uint256 const offerBobToBroker = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId, drops(1)), token::Owner(alice)); env.close(); verifyNFTokenOfferID(offerBobToBroker); @@ -6276,12 +6415,14 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite verifyNFTokenID(nftId); // Alice creates 2 sell offers for the same NFT - uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex1 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken)); env.close(); verifyNFTokenOfferID(aliceOfferIndex1); - uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex2 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken)); env.close(); verifyNFTokenOfferID(aliceOfferIndex2); @@ -6296,7 +6437,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite if (features[featureNFTokenMintOffer]) { uint256 const aliceMintWithOfferIndex1 = - keylet::nftokenOffer(alice, env.seq(alice)).key; + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::mint(alice), token::Amount(XRP(0))); env.close(); verifyNFTokenOfferID(aliceMintWithOfferIndex1); @@ -6319,7 +6460,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // acct makes an sell offer - uint256 const sellOfferIndex = keylet::nftokenOffer(acct, env.seq(acct)).key; + uint256 const sellOfferIndex = + keylet::nftokenOffer(acct, SeqProxy::rawSequence(env.seq(acct))).key; env(token::createOffer(acct, nftId, amt), Txflags(tfSellNFToken)); env.close(); @@ -6488,7 +6630,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // Bob makes a buy offer for 1 XRP - auto const buyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key; + auto const buyOfferIndex = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId, XRP(1)), token::Owner(alice)); env.close(); @@ -6532,14 +6675,16 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite env.close(); // Alice creates sell offer and set broker as destination - uint256 const offerAliceToBroker = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const offerAliceToBroker = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, XRP(1)), token::Destination(broker), Txflags(tfSellNFToken)); env.close(); // Bob creates buy offer - uint256 const offerBobToBroker = keylet::nftokenOffer(bob, env.seq(bob)).key; + uint256 const offerBobToBroker = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId, XRP(1)), token::Owner(alice)); env.close(); @@ -6633,10 +6778,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // becky buys the nfts for 1 drop each. { - uint256 const beckyBuyOfferIndex1 = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex1 = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, drops(1)), token::Owner(issuer)); - uint256 const beckyBuyOfferIndex2 = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex2 = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), token::Owner(issuer)); env.close(); @@ -6647,7 +6794,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // becky creates offers to sell the nfts for AUD. uint256 const beckyAutoTrustOfferIndex = - keylet::nftokenOffer(becky, env.seq(becky)).key; + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken)); env.close(); @@ -6666,7 +6813,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, issuer) == 1); uint256 const beckyNoAutoTrustOfferIndex = - keylet::nftokenOffer(becky, env.seq(becky)).key; + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftNoAutoTrustID, gwAUD(100)), Txflags(tfSellNFToken)); env.close(); @@ -6790,10 +6937,12 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // becky buys the nfts for 1 drop each. { - uint256 const beckyBuyOfferIndex1 = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex1 = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, drops(1)), token::Owner(issuer)); - uint256 const beckyBuyOfferIndex2 = keylet::nftokenOffer(becky, env.seq(becky)).key; + uint256 const beckyBuyOfferIndex2 = + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftNoAutoTrustID, drops(1)), token::Owner(issuer)); env.close(); @@ -6821,7 +6970,7 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // However if the NFToken has the tfTrustLine flag set, // then becky can create the offer. uint256 const beckyAutoTrustOfferIndex = - keylet::nftokenOffer(becky, env.seq(becky)).key; + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, isISU(100)), Txflags(tfSellNFToken)); env.close(); @@ -6839,11 +6988,11 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite // With featureNFTokenMintOffer things go better. // becky creates offers to sell the nfts for ISU. uint256 const beckyNoAutoTrustOfferIndex = - keylet::nftokenOffer(becky, env.seq(becky)).key; + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftNoAutoTrustID, isISU(100)), Txflags(tfSellNFToken)); env.close(); uint256 const beckyAutoTrustOfferIndex = - keylet::nftokenOffer(becky, env.seq(becky)).key; + keylet::nftokenOffer(becky, SeqProxy::rawSequence(env.seq(becky))).key; env(token::createOffer(becky, nftAutoTrustID, isISU(100)), Txflags(tfSellNFToken)); env.close(); @@ -7077,7 +7226,8 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite checkURI(issuer, "uri", __LINE__); // Account != Owner - uint256 const offerID = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const offerID = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftId, XRP(0)), Txflags(tfSellNFToken)); env.close(); env(token::acceptSellOffer(alice, offerID)); diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 33721e91d8..500372bca3 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -797,13 +798,15 @@ public: // The offer expires (it's not removed yet). env.close(); env.require(Owners(bob, 1), offers(bob, 1)); - auto const expiredBobOffer = keylet::offer(bob, env.seq(bob) - 1); + auto const expiredBobOffer = + keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob) - 1)); // bob creates the offer that will be crossed. env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS)); env.close(); env.require(Owners(bob, 2), offers(bob, 2)); - auto const crossedBobOffer = keylet::offer(bob, env.seq(bob) - 1); + auto const crossedBobOffer = + keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob) - 1)); env(trust(alice, usd(1000)), Ter(tesSUCCESS)); env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS)); @@ -850,7 +853,7 @@ public: env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS)); env.close(); - auto const bobOffer = keylet::offer(bob, env.seq(bob) - 1); + auto const bobOffer = keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob) - 1)); env(trust(alice, usd(1000)), Ter(tesSUCCESS)); env(pay(gw, alice, usd(1000)), Ter(tesSUCCESS)); diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index 592b0ef326..96fe094c62 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -71,7 +72,8 @@ struct PayChan_test : public beast::unit_test::Suite auto const sle = view.read(keylet::account(account)); if (!sle) return {}; - auto const k = keylet::payChannel(account, dst, (*sle)[sfSequence] - 1); + auto const k = + keylet::payChannel(account, dst, SeqProxy::rawSequence((*sle)[sfSequence] - 1)); return {k.key, view.read(k)}; } diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 68b2fa99a7..a7e4cd7615 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +60,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite [[nodiscard]] static bool offerExists(Env const& env, Account const& account, std::uint32_t offerSeq) { - return static_cast(env.le(keylet::offer(account.id(), offerSeq))); + return static_cast( + env.le(keylet::offer(account.id(), SeqProxy::rawSequence(offerSeq)))); } [[nodiscard]] static bool @@ -84,11 +86,11 @@ class PermissionedDEX_test : public beast::unit_test::Suite auto const& indexes = page->getFieldV256(sfIndexes); return std::ranges::any_of(indexes, [&](auto const& index) { - return index == keylet::offer(account, offerSeq).key; + return index == keylet::offer(account, SeqProxy::rawSequence(offerSeq)).key; }); }; - auto const sle = env.le(keylet::offer(account.id(), offerSeq)); + auto const sle = env.le(keylet::offer(account.id(), SeqProxy::rawSequence(offerSeq))); if (!sle) return false; if (sle->getFieldAmount(sfTakerGets) != takerGets) @@ -147,7 +149,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite static std::optional getDefaultOfferDirKey(Env const& env, Account const& account, std::uint32_t offerSeq) { - if (auto const sle = env.le(keylet::offer(account.id(), offerSeq))) + if (auto const sle = env.le(keylet::offer(account.id(), SeqProxy::rawSequence(offerSeq)))) return Keylet(ltDIR_NODE, (*sle)[sfBookDirectory]).key; return {}; @@ -1244,7 +1246,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + auto const sleHybridOffer = + env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(hybridOfferSeq))); if (!BEAST_EXPECT(sleHybridOffer)) return; auto const openDir = @@ -1277,7 +1280,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(offerExists(env, bob, regularOfferSeq)); BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); - auto const sleHybridOffer = env.le(keylet::offer(bob.id(), hybridOfferSeq)); + auto const sleHybridOffer = + env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(hybridOfferSeq))); if (!BEAST_EXPECT(sleHybridOffer)) return; auto const openDir = @@ -1570,7 +1574,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID)); env.close(); - auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq)); + auto const sleOffer = + env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq))); BEAST_EXPECT(sleOffer); BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir); BEAST_EXPECT(sleOffer->getFieldArray(sfAdditionalBooks).size() == 1); @@ -1666,7 +1671,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite // Directly manipulate the offer SLE in the open ledger so that // sfAdditionalBooks is present but empty (size 0). This is the // malformed state that fixCleanup3_1_3 is designed to catch. - auto const offerKey = keylet::offer(bob.id(), bobOfferSeq); + auto const offerKey = keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq)); env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) { auto const sle = view.read(offerKey); if (!sle) @@ -1735,7 +1740,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // After crossing, Alice's remaining offer should be placed. - auto const sle = env.le(keylet::offer(alice_.id(), aliceOfferSeq)); + auto const sle = env.le(keylet::offer(alice_.id(), SeqProxy::rawSequence(aliceOfferSeq))); BEAST_EXPECT(sle); BEAST_EXPECT(sle->isFieldPresent(sfAdditionalBooks)); BEAST_EXPECT(sle->getFieldArray(sfAdditionalBooks).size() == 1); @@ -1873,7 +1878,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env(offer(setup.bob, XRP(100), setup.usd(40))); env.close(); - auto const sle = env.le(keylet::offer(setup.bob.id(), bobOfferSeq)); + auto const sle = + env.le(keylet::offer(setup.bob.id(), SeqProxy::rawSequence(bobOfferSeq))); BEAST_EXPECT(sle); auto const dirKey = sle->getFieldH256(sfBookDirectory); @@ -1907,7 +1913,8 @@ class PermissionedDEX_test : public beast::unit_test::Suite env(offer(alice_, USD(100), XRP(300)), Txflags(tfHybrid), Domain(domainID)); env.close(); - auto const sle = env.le(keylet::offer(alice_.id(), aliceOfferSeq)); + auto const sle = + env.le(keylet::offer(alice_.id(), SeqProxy::rawSequence(aliceOfferSeq))); BEAST_EXPECT(sle); auto const openDirKey = @@ -2023,7 +2030,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true)); - auto const oldOffer = env.le(keylet::offer(alice.id(), oldSeq)); + auto const oldOffer = env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(oldSeq))); if (!BEAST_EXPECT(oldOffer)) return; BEAST_EXPECT(oldOffer->getFieldH256(sfDomainID) == domainA); @@ -2038,7 +2045,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(!offerExists(env, alice, oldSeq)); BEAST_EXPECT(checkOffer(env, alice, newSeq, USD(100), XRP(2), 0, true)); - auto const newOffer = env.le(keylet::offer(alice.id(), newSeq)); + auto const newOffer = env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(newSeq))); if (!BEAST_EXPECT(newOffer)) return; BEAST_EXPECT(newOffer->getFieldH256(sfDomainID) == domainB); diff --git a/src/test/app/PermissionedDomains_test.cpp b/src/test/app/PermissionedDomains_test.cpp index 784c2b4f56..1a2472b397 100644 --- a/src/test/app/PermissionedDomains_test.cpp +++ b/src/test/app/PermissionedDomains_test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -552,11 +553,14 @@ class PermissionedDomains_test : public beast::unit_test::Suite auto domain = pdomain::getNewDomain(env.meta()); if (features[fixCleanup3_1_3]) { - BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), seq).key); + BEAST_EXPECT( + domain == + keylet::permissionedDomain(alice.id(), SeqProxy::rawSequence(seq)).key); } else { - BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), 0).key); + BEAST_EXPECT( + domain == keylet::permissionedDomain(alice.id(), SeqProxy::rawSequence(0)).key); } } diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index b1c762d733..bcd31bc6a0 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -988,7 +989,8 @@ public: BEAST_EXPECT(sle->at(sfRemainingOwnerCount) == 99); BEAST_EXPECT(sle->at(sfFeeAmount) == XRP(99)); - env(check::cancel(alice, keylet::check(alice, checkSeq).key), Ter(tesSUCCESS)); + env(check::cancel(alice, keylet::check(alice, SeqProxy::rawSequence(checkSeq)).key), + Ter(tesSUCCESS)); env.close(); sle = env.le(keylet::sponsorship(sponsor, alice)); @@ -1377,7 +1379,7 @@ public: env(check::create(alice, bob, XRP(1))); env.close(); - auto const checkId = keylet::check(alice, seq).key; + auto const checkId = keylet::check(alice, SeqProxy::rawSequence(seq)).key; BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), @@ -1390,7 +1392,8 @@ public: env.close(); // Invalid ObjectID (not found) - env(sponsor::transfer(alice, tfSponsorshipCreate, keylet::check(alice, 0).key), + env(sponsor::transfer( + alice, tfSponsorshipCreate, keylet::check(alice, SeqProxy::rawSequence(0)).key), sponsor::As(sponsor1, spfSponsorReserve), Sig(sfSponsorSignature, sponsor1), Ter(tecNO_ENTRY)); @@ -1497,7 +1500,7 @@ public: auto const ticketSeq = env.seq(alice); env(ticket::create(alice, 1)); env.close(); - auto ticketId = keylet::ticket(alice, ticketSeq + 1).key; + auto ticketId = keylet::ticket(alice, SeqProxy::rawTicket(ticketSeq + 1)).key; BEAST_EXPECT(env.le(keylet::unchecked(ticketId))); env(sponsor::transfer(alice, tfSponsorshipEnd, ticketId), Ter(tecNO_PERMISSION)); env.close(); @@ -1518,7 +1521,7 @@ public: env(check::create(alice, bob, XRP(1))); env.close(); - auto const checkId = keylet::check(alice, seq).key; + auto const checkId = keylet::check(alice, SeqProxy::rawSequence(seq)).key; BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), @@ -1552,7 +1555,7 @@ public: env(check::create(alice, bob, XRP(1))); env.close(); - auto const checkId = keylet::check(alice, seq).key; + auto const checkId = keylet::check(alice, SeqProxy::rawSequence(seq)).key; BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); // insufficient reserve count @@ -1654,7 +1657,7 @@ public: env(check::create(alice, bob, XRP(1))); env.close(); - auto const checkId = keylet::check(alice, seq).key; + auto const checkId = keylet::check(alice, SeqProxy::rawSequence(seq)).key; BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), @@ -1694,7 +1697,7 @@ public: env(check::create(alice, bob, XRP(1))); env.close(); - auto const checkId = keylet::check(alice, seq).key; + auto const checkId = keylet::check(alice, SeqProxy::rawSequence(seq)).key; BEAST_EXPECT(env.le(keylet::unchecked(checkId)) != nullptr); env(sponsor::transfer(alice, tfSponsorshipCreate, checkId), @@ -1839,7 +1842,7 @@ public: auto const ticketSeq = env.seq(alice); env(ticket::create(alice, 1)); env.close(); - auto const ticketID = keylet::ticket(alice, ticketSeq + 1).key; + auto const ticketID = keylet::ticket(alice, SeqProxy::rawTicket(ticketSeq + 1)).key; BEAST_EXPECT(env.le(keylet::unchecked(ticketID))); checkBlocked(alice, ticketID); @@ -1869,7 +1872,8 @@ public: {.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(loan_broker::set(alice, vaultKeylet.key), loan_broker::kDebtMaximum(xrpAsset(1000).value()), loan_broker::kManagementFeeRate(TenthBips16{0}), @@ -1877,7 +1881,7 @@ public: loan_broker::kCoverRateLiquidation(TenthBips32{0})); env.close(); - auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1)); env(loan::set(borrower, brokerKeylet.key, xrpAsset(100).value()), Sig(sfCounterpartySignature, alice), Fee(env.current()->fees().base * 2)); @@ -2803,7 +2807,7 @@ public: BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); - auto const keylet = keylet::check(alice, seq); + auto const keylet = keylet::check(alice, SeqProxy::rawSequence(seq)); BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); if (cosigning) @@ -2867,7 +2871,7 @@ public: BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); // CheckCash - auto const checkId2 = keylet::check(alice, seq2).key; + auto const checkId2 = keylet::check(alice, SeqProxy::rawSequence(seq2)).key; env(check::cash(bob, checkId2, XRP(1))); env.close(); @@ -2907,7 +2911,7 @@ public: BEAST_EXPECT(ownerCount(env, bob) == 0); BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); - auto const keylet = keylet::check(alice, seq2); + auto const keylet = keylet::check(alice, SeqProxy::rawSequence(seq2)); BEAST_EXPECT(env.le(keylet)->getAccountID(sfSponsor) == sponsor.id()); // CheckCash @@ -2973,7 +2977,7 @@ public: submit(check::create(alice, bob, mpt(1))); }); - auto const checkKeylet = keylet::check(alice, seq2); + auto const checkKeylet = keylet::check(alice, SeqProxy::rawSequence(seq2)); BEAST_EXPECT(env.le(checkKeylet)->getAccountID(sfSponsor) == sponsor.id()); BEAST_EXPECT(ownerCount(env, bob) == 0); @@ -3382,12 +3386,16 @@ public: escrow::kCancelTime(env.now() + 100s)); }); BEAST_EXPECT( - env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))) + ->getAccountID(sfSponsor) == sponsor.id()); // transfer sponsor if (cosigning) { - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + env(sponsor::transfer( + alice, + tfSponsorshipReassign, + keylet::escrow(alice, SeqProxy::rawSequence(seq)).key), sponsor::As(sponsor2, spfSponsorReserve), Sig(sfSponsorSignature, sponsor2)); env.close(); @@ -3397,7 +3405,10 @@ public: env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); env.close(); - env(sponsor::transfer(alice, tfSponsorshipReassign, keylet::escrow(alice, seq).key), + env(sponsor::transfer( + alice, + tfSponsorshipReassign, + keylet::escrow(alice, SeqProxy::rawSequence(seq)).key), sponsor::As(sponsor2, spfSponsorReserve)); env.close(); } @@ -3408,7 +3419,8 @@ public: BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); BEAST_EXPECT( - env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor2.id()); + env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))) + ->getAccountID(sfSponsor) == sponsor2.id()); // EscrowFinish env(escrow::finish(bob, alice, seq), @@ -3462,7 +3474,8 @@ public: }); BEAST_EXPECT( - env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))) + ->getAccountID(sfSponsor) == sponsor.id()); // EscrowFinish testEachSponsorship( @@ -3524,7 +3537,8 @@ public: }); BEAST_EXPECT( - env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))) + ->getAccountID(sfSponsor) == sponsor.id()); if (cosigning) { @@ -3625,7 +3639,7 @@ public: tecNO_LINE_INSUF_RESERVE, [&](Env& env, auto const& submit) { submit(escrow::cancel(alice, alice, seq)); }, [&]() { - BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)))); auto const trustSle = env.le(keylet::trustLine(alice, gw, usd.currency)); BEAST_EXPECT(trustSle); if (trustSle) @@ -3728,7 +3742,8 @@ public: }); BEAST_EXPECT( - env.le(keylet::escrow(alice, seq))->getAccountID(sfSponsor) == sponsor.id()); + env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq))) + ->getAccountID(sfSponsor) == sponsor.id()); if (cosigning) { @@ -3810,7 +3825,7 @@ public: } env.close(); - BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)))); BEAST_EXPECT(ownerCount(env, alice) == 0); BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); @@ -4754,7 +4769,7 @@ public: env(check::create(alice, bob, XRP(1))); env.close(); - auto const keylet = keylet::check(alice, seq); + auto const keylet = keylet::check(alice, SeqProxy::rawSequence(seq)); env(sponsor::transfer(alice, tfSponsorshipCreate, keylet.key), sponsor::As(bob, spfSponsorReserve), @@ -5491,14 +5506,14 @@ public: if (expected == tesSUCCESS) { - BEAST_EXPECT(!env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(!env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)))); BEAST_EXPECT(env.le(keylet::trustLine(alice, gw, usd.currency))); BEAST_EXPECT(env.balance(alice, usd) == usd(100)); BEAST_EXPECT(ownerCount(env, alice) == 1); // the new line } else { - BEAST_EXPECT(env.le(keylet::escrow(alice, seq))); + BEAST_EXPECT(env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)))); BEAST_EXPECT(!env.le(keylet::trustLine(alice, gw, usd.currency))); BEAST_EXPECT(ownerCount(env, alice) == 1); // still the escrow } @@ -5598,7 +5613,8 @@ public: BEAST_EXPECT(sponsorCountBefore == 1); // check costs 1 owner count // Cancel (delete) the check. - env(check::cancel(checkOwner, keylet::check(checkOwner, checkSeq).key)); + env(check::cancel( + checkOwner, keylet::check(checkOwner, SeqProxy::rawSequence(checkSeq)).key)); env.close(); auto sponsorCountAfter = sponsoringOwnerCount(env, sponsor); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 1fe48add27..5b257449e0 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -2889,7 +2889,7 @@ public: checkMetrics(*this, env, 5, std::nullopt, 7, 6); { auto aliceStat = txQ.getAccountTxs(alice.id()); - SeqProxy seq = SeqProxy::sequence(aliceSeq); + SeqProxy seq = SeqProxy::rawSequence(aliceSeq); BEAST_EXPECT(aliceStat.size() == 5); for (auto const& tx : aliceStat) { @@ -3754,7 +3754,7 @@ public: checkMetrics(*this, env, 2, 24, 16, 12); auto const aliceQueue = env.app().getTxQ().getAccountTxs(alice.id()); BEAST_EXPECT(aliceQueue.size() == 2); - SeqProxy seq = SeqProxy::sequence(aliceSeq); + SeqProxy seq = SeqProxy::rawSequence(aliceSeq); for (auto const& tx : aliceQueue) { BEAST_EXPECT(tx.seqProxy == seq); diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 773ce28963..70527f570d 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -51,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -3125,7 +3126,7 @@ class Vault_test : public beast::unit_test::Suite Vault const vault{env}; env.fund(XRP(1000), owner); - auto const keylet = keylet::vault(owner.id(), env.seq(owner)); + auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); for (int i = 0; i < 256; ++i) { AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key); @@ -3947,7 +3948,8 @@ class Vault_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000)); // Create a loan broker backed by this vault - auto const brokerKeylet = keylet::loanBroker(d.owner.id(), env.seq(d.owner)); + auto const brokerKeylet = + keylet::loanBroker(d.owner.id(), SeqProxy::rawSequence(env.seq(d.owner))); env(set(d.owner, d.keylet.key)); env.close(); @@ -4461,12 +4463,13 @@ class Vault_test : public beast::unit_test::Suite env.close(); auto const& sharesAvailable = vaultShareBalance(vaultKeylet); - auto const& brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const& brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); - auto const& loanKeylet = keylet::loan(brokerKeylet.key, 1); + auto const& loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1)); // Create a simple Loan for the full amount of Vault assets env(set(depositor, brokerKeylet.key, asset(100).value()), @@ -4854,7 +4857,8 @@ class Vault_test : public beast::unit_test::Suite PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); // Create a loan broker backed by this vault - auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); @@ -4912,7 +4916,8 @@ class Vault_test : public beast::unit_test::Suite PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); // Create a loan broker backed by this vault - auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); @@ -4967,7 +4972,8 @@ class Vault_test : public beast::unit_test::Suite PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); // Create a loan broker backed by this vault - auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); @@ -5021,7 +5027,8 @@ class Vault_test : public beast::unit_test::Suite return; PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); - auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); @@ -5069,7 +5076,8 @@ class Vault_test : public beast::unit_test::Suite return; PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); - auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); @@ -5176,7 +5184,8 @@ class Vault_test : public beast::unit_test::Suite PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); // Create a loan broker backed by this vault - auto const brokerKeylet = keylet::loanBroker(owner.id(), env.seq(owner)); + auto const brokerKeylet = + keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(set(owner, vaultKeylet.key)); env.close(); @@ -5310,7 +5319,7 @@ class Vault_test : public beast::unit_test::Suite "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); } - auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + auto const newKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); try { auto const insertAt = maxInt64Plus2.size() - 3; @@ -5378,7 +5387,7 @@ class Vault_test : public beast::unit_test::Suite "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."); } - auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + auto const newKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); try { auto const insertAt = maxInt64Plus2.size() - 1; @@ -5450,7 +5459,8 @@ class Vault_test : public beast::unit_test::Suite // These values will be rounded to 15 significant digits { - auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + auto const newKeylet = + keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); try { auto const insertAt = maxInt64Plus2.size() - 1; @@ -5474,7 +5484,8 @@ class Vault_test : public beast::unit_test::Suite } { tx[sfAssetsMaximum] = "9223372036854775807e40"; // max int64 * 10^40 - auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + auto const newKeylet = + keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(tx); env.close(); @@ -5488,7 +5499,8 @@ class Vault_test : public beast::unit_test::Suite } { tx[sfAssetsMaximum] = "9223372036854775807e-40"; // max int64 * 10^-40 - auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + auto const newKeylet = + keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(tx); env.close(); @@ -5502,7 +5514,8 @@ class Vault_test : public beast::unit_test::Suite } { tx[sfAssetsMaximum] = "9223372036854775807e-100"; // max int64 * 10^-100 - auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); + auto const newKeylet = + keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); env(tx); env.close(); @@ -6088,7 +6101,8 @@ class Vault_test : public beast::unit_test::Suite env.close(); // Loan broker: no cover, no management fee, debt cap 10x principal. - f.brokerID = keylet::loanBroker(f.lender.id(), env.seq(f.lender)).key; + f.brokerID = + keylet::loanBroker(f.lender.id(), SeqProxy::rawSequence(env.seq(f.lender))).key; { using namespace loan_broker; env(set(f.lender, vaultKeylet.key), @@ -6100,7 +6114,8 @@ class Vault_test : public beast::unit_test::Suite auto const sleBroker = env.le(keylet::loanBroker(f.brokerID)); if (!BEAST_EXPECT(sleBroker)) return f; - f.loanKeylet = keylet::loan(f.brokerID, sleBroker->at(sfLoanSequence)); + f.loanKeylet = + keylet::loan(f.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence))); { using namespace loan; @@ -7587,7 +7602,7 @@ class Vault_test : public beast::unit_test::Suite Vault const vault{env}; - auto const keylet = keylet::vault(owner.id(), 1); + auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(1)); auto delTx = vault.del({.owner = owner, .id = keylet.key}); // Test VaultDelete with featureLendingProtocolV1_1 disabled @@ -7619,7 +7634,7 @@ class Vault_test : public beast::unit_test::Suite { testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault"); - auto const keylet = keylet::vault(owner.id(), env.seq(owner)); + auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner))); // Recreate the transaction as the vault keylet changed auto delTx = vault.del({.owner = owner, .id = keylet.key}); diff --git a/src/test/app/lending/LoanBroker_test.cpp b/src/test/app/lending/LoanBroker_test.cpp index a610dbe931..c5efb59194 100644 --- a/src/test/app/lending/LoanBroker_test.cpp +++ b/src/test/app/lending/LoanBroker_test.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -96,7 +97,8 @@ class LoanBroker_test : public beast::unit_test::Suite using namespace loan_broker; // Can't create a loan broker regardless of whether the vault exists env(set(alice, keylet.key), Ter(temDISABLED)); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); // Other LoanBroker transactions are disabled, too. // 1. LoanBrokerCoverDeposit env(coverDeposit(alice, brokerKeylet.key, asset(1000)), Ter(temDISABLED)); @@ -182,7 +184,8 @@ class LoanBroker_test : public beast::unit_test::Suite static PrettyAsset const kGhostIouAsset = kNonExistent["GST"]; PrettyAsset const vaultPseudoIouAsset = vault.pseudoAccount["PSD"]; - auto const badKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const badKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, badVault.vaultID)); env.close(); auto const badBrokerPseudo = [&]() { @@ -195,7 +198,7 @@ class LoanBroker_test : public beast::unit_test::Suite }(); PrettyAsset const badBrokerPseudoIouAsset = badBrokerPseudo["WAT"]; - auto const keylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const keylet = keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); { // Start with default values auto jtx = env.jt(set(alice, vault.vaultID)); @@ -650,7 +653,7 @@ class LoanBroker_test : public beast::unit_test::Suite TenthBips32 const tenthBipsZero{0}; - auto badKeylet = keylet::vault(alice.id(), env.seq(alice)); + auto badKeylet = keylet::vault(alice.id(), SeqProxy::rawSequence(env.seq(alice))); // Try some failure cases // not the vault owner env(set(evan, vault.vaultID), Ter(tecNO_PERMISSION)); @@ -741,7 +744,8 @@ class LoanBroker_test : public beast::unit_test::Suite // Modifications // Update the fields - auto const nextKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const nextKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); // fields that can't be changed // LoanBrokerID @@ -897,7 +901,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultInfo.vaultID)); env.close(); @@ -1040,7 +1045,7 @@ class LoanBroker_test : public beast::unit_test::Suite env(del(alice, brokerKeylet.key), Ter(tecHAS_OBLIGATIONS)); // Repay and delete the loan - auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); + auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1)); env(loan::pay(borrower, loanKeylet.key, asset(50).value())); env(loan::del(alice, loanKeylet.key)); @@ -1217,7 +1222,8 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); // Predict LoanBroker key using alice's current sequence BEFORE submit - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); // Create LoanBroker pointing to the vault env(loan_broker::set(alice, vaultKeylet.key)); @@ -1323,7 +1329,8 @@ class LoanBroker_test : public beast::unit_test::Suite err); }); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); // Can create LoanBroker if the vault owner is not authorized forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); }); @@ -1401,7 +1408,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultInfo.vaultID)); env.close(); @@ -1548,7 +1556,8 @@ class LoanBroker_test : public beast::unit_test::Suite Ter(err)); env.close(); - auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); + auto const brokerKeylet = + keylet::loanBroker(broker, SeqProxy::rawSequence(env.seq(broker))); env(loan_broker::set(broker, keylet.key)); env.close(); @@ -1662,7 +1671,8 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); // Create loan broker - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -1779,7 +1789,8 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); // Create loan broker - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -1856,7 +1867,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -1926,7 +1938,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -2006,7 +2019,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -2069,7 +2083,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -2184,7 +2199,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(50)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -2359,7 +2375,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)})); // Test LoanBroker withdraw - auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); + auto const brokerKeylet = + keylet::loanBroker(broker, SeqProxy::rawSequence(env.seq(broker))); env(loan_broker::set(broker, keylet.key)); env.close(); @@ -2487,7 +2504,8 @@ class LoanBroker_test : public beast::unit_test::Suite } // Test LoanBroker withdraw - auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); + auto const brokerKeylet = + keylet::loanBroker(broker, SeqProxy::rawSequence(env.seq(broker))); env(loan_broker::set(broker, keylet.key)); env.close(); @@ -2551,7 +2569,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(createTx); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); @@ -2698,7 +2717,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(createTx); env.close(); - auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); + auto const brokerKeylet = + keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice))); env(set(alice, vaultKeylet.key)); env.close(); diff --git a/src/test/app/lending/LoanCashBasis_test.cpp b/src/test/app/lending/LoanCashBasis_test.cpp index c1334180c1..11053b6fd0 100644 --- a/src/test/app/lending/LoanCashBasis_test.cpp +++ b/src/test/app/lending/LoanCashBasis_test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -86,7 +87,8 @@ private: Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); auto const loanSequence = brokerBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = + keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), kCounterparty(lender), @@ -309,7 +311,8 @@ private: auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerBeforeLoan); auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = + keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); env(loanParams(env, broker)); env.close(); @@ -530,7 +533,8 @@ private: auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerBeforeLoan); auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = + keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); env(loanParams(env, broker)); env.close(); @@ -735,7 +739,7 @@ private: auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerBeforeLoan); auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); // ---- LoanSet origination: whole-life formulas expected ---- auto const vaultBeforeSet = env.le(broker.vaultKeylet()); @@ -943,7 +947,7 @@ private: auto const brokerBeforeLoan = env.le(brokerKeylet); BEAST_EXPECT(brokerBeforeLoan); auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); LoanParameters const loanParams{ .account = borrower, diff --git a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp index ec0238c173..a9b3542c4e 100644 --- a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp +++ b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -90,7 +91,7 @@ private: env(loanATx); env.close(); - auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1); + auto const loanAKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1)); // Create Loan B auto loanBTx = env.jt( @@ -104,7 +105,7 @@ private: env(loanBTx); env.close(); - auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2); + auto const loanBKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(2)); auto loanASle = env.le(loanAKeylet); if (!BEAST_EXPECT(loanASle)) @@ -224,7 +225,8 @@ private: if (!BEAST_EXPECT(sleBroker)) return; - auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence)); + auto const loanKeylet = + keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence))); env(set(borrower, broker.brokerID, kPrincipalAmount), Sig(sfCounterpartySignature, lender), @@ -324,7 +326,7 @@ private: BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1)); env(set(borrower, brokerInfo.brokerID, 10'000), Sig(sfCounterpartySignature, broker), @@ -388,7 +390,7 @@ private: // Create vault and broker auto const brokerInfo = createVaultAndBroker(env, iou, broker); // Create a loan first (this creates debt) - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1)); env(set(borrower, brokerInfo.brokerID, 10'000), Sig(sfCounterpartySignature, broker), kLoanServiceFee(iou(100).value()), @@ -468,7 +470,7 @@ private: // Create vault and broker auto const brokerInfo = createVaultAndBroker(env, mpt, broker); // Create a loan first (this creates debt) - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1)); env(set(borrower, brokerInfo.brokerID, 10'000), Sig(sfCounterpartySignature, broker), kLoanServiceFee(mpt(100).value()), @@ -567,7 +569,7 @@ private: // Create vault and broker auto const brokerInfo = createVaultAndBroker(env, mpt, broker); // Create a loan first (this creates debt) - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + auto const keylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1)); env(set(borrower, brokerInfo.brokerID, 10'000), Sig(sfCounterpartySignature, broker), kLoanServiceFee(mpt(100).value()), diff --git a/src/test/app/lending/LoanInvariants_test.cpp b/src/test/app/lending/LoanInvariants_test.cpp index 381a3f8b48..264dbdcd24 100644 --- a/src/test/app/lending/LoanInvariants_test.cpp +++ b/src/test/app/lending/LoanInvariants_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -542,7 +543,8 @@ private: if (!BEAST_EXPECT(brokerSle1)) return std::nullopt; auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence); - auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq); + auto const tinyLoanKeylet = + keylet::loan(c.broker.brokerID, SeqProxy::rawSequence(tinyLoanSeq)); env(set(c.borrower, c.broker.brokerID, Number{1, -2}), Sig(sfCounterpartySignature, c.lender), @@ -559,7 +561,8 @@ private: if (!BEAST_EXPECT(brokerSle2)) return std::nullopt; auto const bigLoanSeq = brokerSle2->at(sfLoanSequence); - auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq); + auto const bigLoanKeylet = + keylet::loan(c.broker.brokerID, SeqProxy::rawSequence(bigLoanSeq)); env(set(c.borrower, c.broker.brokerID, Number{500}), Sig(sfCounterpartySignature, c.lender), diff --git a/src/test/app/lending/LoanLifecycle_test.cpp b/src/test/app/lending/LoanLifecycle_test.cpp index e9868769b7..6cced5c97a 100644 --- a/src/test/app/lending/LoanLifecycle_test.cpp +++ b/src/test/app/lending/LoanLifecycle_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -306,7 +307,7 @@ private: // Issuer "borrowed" 200, OutstandingAmount decreased by 200 BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance + 200)); // Pay Loan - auto const loanKeylet = keylet::loan(broker.brokerID, 1); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(1)); env(pay(borrower, loanKeylet.key, asset(200))); env.close(); // Issuer "re-payed" 200, OutstandingAmount increased by 200 @@ -355,7 +356,8 @@ private: txFee); env.close(); - auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker)); + auto const brokerKeylet = + keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker))); env(loan_broker::set(broker, vaultKeylet.key), txFee); env.close(); @@ -371,7 +373,8 @@ private: env.close(); std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); + auto const loanKeylet = + keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(loanSequence)); auto const brokerBalanceBefore = env.balance(broker, asset); diff --git a/src/test/app/lending/LoanMisc_test.cpp b/src/test/app/lending/LoanMisc_test.cpp index 44c093b3c2..2cb4f38ecf 100644 --- a/src/test/app/lending/LoanMisc_test.cpp +++ b/src/test/app/lending/LoanMisc_test.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -386,7 +387,7 @@ private: Sig(sfCounterpartySignature, lender), loanSetFee); env.close(); - auto const loanKeylet = keylet::loan(broker.brokerID, 1); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(1)); BEAST_EXPECT(env.le(loanKeylet)); // Repayment still works. diff --git a/src/test/app/lending/LoanPay_test.cpp b/src/test/app/lending/LoanPay_test.cpp index e06d728c87..9d840fe1bf 100644 --- a/src/test/app/lending/LoanPay_test.cpp +++ b/src/test/app/lending/LoanPay_test.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -68,7 +69,7 @@ private: return; auto const loanSequence = brokerPreLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); Number const principal = asset(1'000).value(); Number const serviceFee = asset(2).value(); @@ -220,8 +221,8 @@ private: auto const brokerSle = env.le(result.brokerKeylet()); if (!BEAST_EXPECT(brokerSle)) return; - auto const loanKeylet = - keylet::loan(result.brokerKeylet().key, brokerSle->at(sfLoanSequence)); + auto const loanKeylet = keylet::loan( + result.brokerKeylet().key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence))); env(loan::set( borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment), Sig(sfCounterpartySignature, lender), @@ -437,7 +438,8 @@ private: auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle); auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = + keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); env(set(borrower, broker.brokerID, principalRequested), Sig(sfCounterpartySignature, lender), @@ -690,7 +692,7 @@ private: return; // Intentionally shadow the outer values auto const loanSequence = brokerState->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); + auto const keylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); auto const interval = maxLoanTime / total; auto createJson = env.json( diff --git a/src/test/app/lending/LoanRounding_test.cpp b/src/test/app/lending/LoanRounding_test.cpp index b914961e78..5e69c9f79e 100644 --- a/src/test/app/lending/LoanRounding_test.cpp +++ b/src/test/app/lending/LoanRounding_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -90,7 +91,7 @@ private: // The loan keylet is based on the LoanSequence of the // _LOAN_BROKER_ object. auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); + return keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); }(); if (!loanKeyletOpt) return; @@ -325,7 +326,8 @@ private: auto borrowerBalance = [&]() { return env.balance(borrower, iou); }; auto const borrowerScale = static_cast(borrowerBalance()).exponent(); - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq); + auto const loanKeylet = + keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(currentSeq)); auto const maybePeriodicPayment = [&]() -> std::optional { auto const loanSle = env.le(loanKeylet); if (!BEAST_EXPECT(loanSle)) @@ -452,7 +454,8 @@ private: env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)})); env.close(); - auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); + auto const brokerKeylet = + keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender))); env(loan_broker::set(lender, vaultKeylet.key), loan_broker::kDebtMaximum(Number{100}), Fee(env.current()->fees().base * 2)); @@ -462,7 +465,7 @@ private: if (!BEAST_EXPECT(brokerStateBefore)) return; auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); + auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(loanSequence)); env(loan::set(borrower, brokerKeylet.key, Number{1}), Sig(sfCounterpartySignature, lender), @@ -674,7 +677,8 @@ private: auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle); auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = + keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment), Sig(sfCounterpartySignature, lender), @@ -862,7 +866,7 @@ private: if (!BEAST_EXPECT(sleBroker)) return; auto const loanSequence = sleBroker->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); using namespace loan; env(set(borrower, broker.brokerID, Number{1000}, tfLoanOverpayment), diff --git a/src/test/app/lending/LoanSecurity_test.cpp b/src/test/app/lending/LoanSecurity_test.cpp index b08d80b51c..21772d0617 100644 --- a/src/test/app/lending/LoanSecurity_test.cpp +++ b/src/test/app/lending/LoanSecurity_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -103,7 +104,7 @@ private: auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); BEAST_EXPECT(brokerSle); auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0; - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); env(createJtx); env.close(); @@ -424,7 +425,8 @@ private: txFee); env.close(); - auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender)); + auto const brokerKeyLet = + keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender))); env(loan_broker::set(lender, vaultKeyLet.key), txFee); env.close(); @@ -441,7 +443,7 @@ private: env.close(); std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); + auto const loanKeylet = keylet::loan(brokerKeyLet.key, SeqProxy::rawSequence(loanSequence)); if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) { diff --git a/src/test/app/lending/LoanTestBase.h b/src/test/app/lending/LoanTestBase.h index 35aa1e26cb..dabdfc9bed 100644 --- a/src/test/app/lending/LoanTestBase.h +++ b/src/test/app/lending/LoanTestBase.h @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -474,7 +475,7 @@ protected: BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); } - auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender)); + auto const keylet = keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender))); using namespace loan_broker; env(set(lender, vaultKeylet.key, params.flags), @@ -665,9 +666,9 @@ protected: { auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); if (!BEAST_EXPECT(brokerStateBefore)) - return keylet::loan(broker.brokerID, 0); + return keylet::loan(broker.brokerID, SeqProxy::rawSequence(0)); auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); + return keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); } // Funds issuer/lender/borrower with XRP, creates an IOU asset issued by @@ -852,7 +853,7 @@ protected: // The loan keylet is based on the LoanSequence of the // _LOAN_BROKER_ object. auto const loanSequence = brokerSle->at(sfLoanSequence); - return keylet::loan(broker.brokerID, loanSequence); + return keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)); }(); if (!loanKeyletOpt) return std::nullopt; @@ -1329,7 +1330,8 @@ protected: // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_ // object. auto const loanSequence = brokerSle->at(sfLoanSequence); - return std::make_pair(keylet::loan(broker.brokerID, loanSequence), loanSequence); + return std::make_pair( + keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)), loanSequence); }(); VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); @@ -1666,7 +1668,7 @@ protected: auto const baseFee = env.current()->fees().base; - auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); + auto badKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(env.seq(lender))); // Try some failure cases // flags are checked first env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), diff --git a/src/test/app/lending/LoanValidation_test.cpp b/src/test/app/lending/LoanValidation_test.cpp index d2985c4c30..884384db55 100644 --- a/src/test/app/lending/LoanValidation_test.cpp +++ b/src/test/app/lending/LoanValidation_test.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -57,7 +58,7 @@ private: Account const bob{"bob"}; env.fund(XRP(10000), alice, bob); - auto const keylet = keylet::loanBroker(alice, env.seq(alice)); + auto const keylet = keylet::loanBroker(alice, SeqProxy::rawSequence(env.seq(alice))); using namespace std::chrono_literals; using namespace loan; @@ -73,7 +74,7 @@ private: env(setTx); // Actual sequence will be based off the loan broker, but we // obviously don't have one of those if the amendment is disabled - auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice)); + auto const loanKeylet = keylet::loan(keylet.key, SeqProxy::rawSequence(env.seq(alice))); // Other Loan transactions are disabled, too. // 2. LoanDelete env(del(alice, loanKeylet.key), Ter(temDISABLED)); @@ -300,7 +301,8 @@ private: env.close(); std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); + auto const loanKeylet = + keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(loanSequence)); env(fset(issuer, asfGlobalFreeze)); env.close(); @@ -398,7 +400,8 @@ private: }); static constexpr std::uint32_t kLoanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, kLoanSequence); + auto const loanKeylet = + keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(kLoanSequence)); // Can't loan pay if the borrower is not authorized forUnauthAuth([&](bool authorized) { diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index d5cd8e66b8..5c8486e6c5 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -26,6 +26,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -779,9 +780,9 @@ inline constexpr FeeLevel64 kBaseFeeLevel{TxQ::kBaseLevel}; inline constexpr FeeLevel64 kMinEscalationFeeLevel = kBaseFeeLevel * 500; inline uint256 -getCheckIndex(AccountID const& account, std::uint32_t uSequence) +getCheckIndex(AccountID const& account, std::uint32_t const sequence) { - return keylet::check(account, uSequence).key; + return keylet::check(account, SeqProxy::rawSequence(sequence)).key; } template diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index d73eb8adf4..2fa2aebcda 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -571,7 +572,8 @@ claim( uint256 channel(AccountID const& account, AccountID const& dst, std::uint32_t seqProxyValue) { - auto const k = keylet::payChannel(account, dst, seqProxyValue); + auto const seqProxy = SeqProxy::rawSequence(seqProxyValue); + auto const k = keylet::payChannel(account, dst, seqProxy); return k.key; } diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp index b1061f65a3..d8d067d95a 100644 --- a/src/test/jtx/impl/batch.cpp +++ b/src/test/jtx/impl/batch.cpp @@ -102,7 +102,7 @@ Sig::operator()(Env& env, JTx& jt) const serializeBatch( msg, stx.getAccountID(sfAccount), - stx.getSeqValue(), + stx.getSeqProxy().value(), stx.getFlags(), stx.getBatchTransactionIDs()); finishMultiSigningData(e.acct.id(), msg); @@ -146,7 +146,7 @@ Msig::operator()(Env& env, JTx& jt) const serializeBatch( msg, stx.getAccountID(sfAccount), - stx.getSeqValue(), + stx.getSeqProxy().value(), stx.getFlags(), stx.getBatchTransactionIDs()); msg.addBitString(master.id()); diff --git a/src/test/jtx/impl/escrow.cpp b/src/test/jtx/impl/escrow.cpp index 61c260a5d0..c2f3f94fa3 100644 --- a/src/test/jtx/impl/escrow.cpp +++ b/src/test/jtx/impl/escrow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -58,7 +59,7 @@ cancel(AccountID const& account, Account const& from, std::uint32_t seq) Rate rate(Env& env, Account const& account, std::uint32_t const& seq) { - auto const sle = env.le(keylet::escrow(account.id(), seq)); + auto const sle = env.le(keylet::escrow(account.id(), SeqProxy::rawSequence(seq))); if (sle->isFieldPresent(sfTransferRate)) return xrpl::Rate((*sle)[sfTransferRate]); return Rate{0}; diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp index 7084347763..baff576243 100644 --- a/src/test/jtx/impl/vault.cpp +++ b/src/test/jtx/impl/vault.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -19,7 +20,8 @@ namespace xrpl::test::jtx { std::tuple Vault::create(CreateArgs const& args) const { - auto keylet = keylet::vault(args.owner.id(), env.seq(args.owner)); + auto const seqProxy = SeqProxy::rawSequence(env.seq(args.owner)); + auto keylet = keylet::vault(args.owner.id(), seqProxy); json::Value jv; jv[jss::TransactionType] = jss::VaultCreate; jv[jss::Account] = args.owner.human(); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index c656c97a4c..1450709f59 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -1546,7 +1547,7 @@ public: env(check::create(owner, dest, XRP(1))); env.close(); - auto const checkId = keylet::check(owner, checkSeq); + auto const checkId = keylet::check(owner, SeqProxy::rawSequence(checkSeq)); if (!BEAST_EXPECT(env.le(checkId))) return; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index b144a6dc84..735c318b21 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -592,7 +593,8 @@ class AccountTx_test : public beast::unit_test::Suite env(payChanCreate, Sig(alie)); env.close(); - std::string const payChanIndex{strHex(keylet::payChannel(alice, gw, payChanSeq).key)}; + std::string const payChanIndex{ + strHex(keylet::payChannel(alice, gw, SeqProxy::rawSequence(payChanSeq)).key)}; { json::Value payChanFund; @@ -617,10 +619,11 @@ class AccountTx_test : public beast::unit_test::Suite // Check { - auto const aliceCheckId = keylet::check(alice, env.seq(alice)).key; + auto const aliceCheckId = + keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(check::create(alice, gw, XRP(300)), Sig(alie)); - auto const gwCheckId = keylet::check(gw, env.seq(gw)).key; + auto const gwCheckId = keylet::check(gw, SeqProxy::rawSequence(env.seq(gw))).key; env(check::create(gw, alice, XRP(200))); env.close(); @@ -1355,7 +1358,7 @@ class AccountTx_test : public beast::unit_test::Suite checkTx(sponsor, jss::SponsorshipSet); // create an object with sponsor - auto const checkId = keylet::check(alice, env.seq(alice)).key; + auto const checkId = keylet::check(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(check::create(alice, sponsor, XRP(1)), sponsor::As(sponsor, spfSponsorReserve)); env.close(); checkTx(alice, jss::CheckCreate); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index b8301d5656..24dde05ce1 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include @@ -807,7 +808,7 @@ class LedgerEntry_test : public beast::unit_test::Suite env.fund(XRP(10000), alice); env.close(); - auto const checkId = keylet::check(env.master, env.seq(env.master)); + auto const checkId = keylet::check(env.master, SeqProxy::rawSequence(env.seq(env.master))); env(check::create(env.master, alice, XRP(100))); env.close(); @@ -1527,7 +1528,8 @@ class LedgerEntry_test : public beast::unit_test::Suite uint256 const nftokenID0 = token::getNextID(env, issuer, 0, tfTransferable); env(token::mint(issuer, 0), Txflags(tfTransferable)); env.close(); - uint256 const offerID = keylet::nftokenOffer(issuer, env.seq(issuer)).key; + uint256 const offerID = + keylet::nftokenOffer(issuer, SeqProxy::rawSequence(env.seq(issuer))).key; env(token::createOffer(issuer, nftokenID0, drops(1)), token::Destination(buyer), Txflags(tfSellNFToken)); @@ -1711,7 +1713,8 @@ class LedgerEntry_test : public beast::unit_test::Suite std::string const ledgerHash{to_string(env.closed()->header().hash)}; - uint256 const payChanIndex{keylet::payChannel(alice, env.master, env.seq(alice) - 1).key}; + uint256 const payChanIndex{ + keylet::payChannel(alice, env.master, SeqProxy::rawSequence(env.seq(alice) - 1)).key}; { // Request the payment channel using its index. json::Value jvParams; @@ -1949,7 +1952,7 @@ class LedgerEntry_test : public beast::unit_test::Suite env.close(); // Create two tickets. - std::uint32_t const tkt1{env.seq(env.master) + 1}; + SeqProxy tkt1 = SeqProxy::rawTicket(env.seq(env.master)); env(ticket::create(env.master, 2)); env.close(); @@ -1960,7 +1963,7 @@ class LedgerEntry_test : public beast::unit_test::Suite { // Not a valid ticket requested by index. json::Value jvParams; - jvParams[jss::ticket] = to_string(getTicketIndex(env.master, tkt1 - 1)); + jvParams[jss::ticket] = to_string(keylet::ticket(env.master, tkt1).key); jvParams[jss::ledger_hash] = ledgerHash; json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; @@ -1969,31 +1972,34 @@ class LedgerEntry_test : public beast::unit_test::Suite { // First real ticket requested by index. json::Value jvParams; - jvParams[jss::ticket] = to_string(getTicketIndex(env.master, tkt1)); + tkt1.advanceBy(1); + jvParams[jss::ticket] = to_string(keylet::ticket(env.master, tkt1).key); jvParams[jss::ledger_hash] = ledgerHash; json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Ticket); - BEAST_EXPECT(jrr[jss::node][sfTicketSequence.jsonName] == tkt1); + BEAST_EXPECT(jrr[jss::node][sfTicketSequence.jsonName] == tkt1.value()); } { // Second real ticket requested by account and sequence. + tkt1.advanceBy(1); json::Value jvParams; jvParams[jss::ticket] = json::ValueType::Object; jvParams[jss::ticket][jss::account] = env.master.human(); - jvParams[jss::ticket][jss::ticket_seq] = tkt1 + 1; + jvParams[jss::ticket][jss::ticket_seq] = tkt1.value(); jvParams[jss::ledger_hash] = ledgerHash; json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; BEAST_EXPECT( - jrr[jss::node][jss::index] == to_string(getTicketIndex(env.master, tkt1 + 1))); + jrr[jss::node][jss::index] == to_string(keylet::ticket(env.master, tkt1).key)); } { // Not a valid ticket requested by account and sequence. + tkt1.advanceBy(1); json::Value jvParams; jvParams[jss::ticket] = json::ValueType::Object; jvParams[jss::ticket][jss::account] = env.master.human(); - jvParams[jss::ticket][jss::ticket_seq] = tkt1 + 2; + jvParams[jss::ticket][jss::ticket_seq] = tkt1.value(); jvParams[jss::ledger_hash] = ledgerHash; json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; @@ -2253,7 +2259,8 @@ class LedgerEntry_test : public beast::unit_test::Suite jv[jss::result][jss::node][sfLedgerEntryType.jsonName] == jss::PermissionedDomain); std::string const pdIdx = jv[jss::result][jss::index].asString(); - BEAST_EXPECT(strHex(keylet::permissionedDomain(alice, seq).key) == pdIdx); + BEAST_EXPECT( + strHex(keylet::permissionedDomain(alice, SeqProxy::rawSequence(seq)).key) == pdIdx); params.clear(); params[jss::ledger_index] = jss::validated; @@ -2703,7 +2710,7 @@ class LedgerEntry_test : public beast::unit_test::Suite env.fund(XRP(10000), alice); env.close(); - auto const checkId = keylet::check(env.master, env.seq(env.master)); + auto const checkId = keylet::check(env.master, SeqProxy::rawSequence(env.seq(env.master))); env(check::create(env.master, alice, XRP(100))); env.close(); diff --git a/src/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 567f31437a..47c2245fa5 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -1453,12 +1454,14 @@ public: // Alice creates one sell offer for each NFT // Verify the offer indexes are correct in the NFTokenCreateOffer tx // meta - uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex1 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId1, drops(1)), Txflags(tfSellNFToken)); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex1); - uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex2 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId2, drops(1)), Txflags(tfSellNFToken)); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex2); @@ -1472,7 +1475,8 @@ public: // Bobs creates a buy offer for nftId1 // Verify the offer id is correct in the NFTokenCreateOffer tx meta - auto const bobBuyOfferIndex = keylet::nftokenOffer(bob, env.seq(bob)).key; + auto const bobBuyOfferIndex = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId1, drops(1)), token::Owner(alice)); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(bobBuyOfferIndex); @@ -1493,7 +1497,8 @@ public: verifyNFTokenID(nftId); // Alice creates sell offer and set broker as destination - uint256 const offerAliceToBroker = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const offerAliceToBroker = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, drops(1)), token::Destination(broker), Txflags(tfSellNFToken)); @@ -1501,7 +1506,8 @@ public: verifyNFTokenOfferID(offerAliceToBroker); // Bob creates buy offer - uint256 const offerBobToBroker = keylet::nftokenOffer(bob, env.seq(bob)).key; + uint256 const offerBobToBroker = + keylet::nftokenOffer(bob, SeqProxy::rawSequence(env.seq(bob))).key; env(token::createOffer(bob, nftId, drops(1)), token::Owner(alice)); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(offerBobToBroker); @@ -1522,12 +1528,14 @@ public: verifyNFTokenID(nftId); // Alice creates 2 sell offers for the same NFT - uint256 const aliceOfferIndex1 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex1 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken)); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex1); - uint256 const aliceOfferIndex2 = keylet::nftokenOffer(alice, env.seq(alice)).key; + uint256 const aliceOfferIndex2 = + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::createOffer(alice, nftId, drops(1)), Txflags(tfSellNFToken)); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceOfferIndex2); @@ -1542,7 +1550,7 @@ public: if (features[featureNFTokenMintOffer]) { uint256 const aliceMintWithOfferIndex1 = - keylet::nftokenOffer(alice, env.seq(alice)).key; + keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key; env(token::mint(alice), token::Amount(XRP(0))); BEAST_EXPECT(env.syncClose()); verifyNFTokenOfferID(aliceMintWithOfferIndex1); diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 2921c63c17..a65dba1d9c 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -345,7 +345,7 @@ class Transaction_test : public beast::unit_test::Suite auto const tx = env.jt(noop(alice), Seq(env.seq(alice))).stx; // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - auto const ctid = *rpc::encodeCTID(endLegSeq, tx->getSeqValue(), netID); + auto const ctid = *rpc::encodeCTID(endLegSeq, tx->getSeqProxy().value(), netID); for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq) { auto const result = env.rpc( diff --git a/src/tests/libxrpl/tx/AccountSet.cpp b/src/tests/libxrpl/tx/AccountSet.cpp index 87d00c58bf..ae291791d4 100644 --- a/src/tests/libxrpl/tx/AccountSet.cpp +++ b/src/tests/libxrpl/tx/AccountSet.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -610,7 +611,7 @@ TEST(AccountSet, Ticket) // Get alice's current sequence - the ticket will be created at seq + 1 std::uint32_t const aliceSeqBefore = env.getAccountRoot(alice.id()).getSequence(); - std::uint32_t const ticketSeq = aliceSeqBefore + 1; + auto const ticketSeq = SeqProxy::rawTicket(aliceSeqBefore + 1); // Create a ticket EXPECT_EQ(env.submit(transactions::TicketCreateBuilder{alice, 1}, alice).ter, tesSUCCESS); @@ -623,7 +624,9 @@ TEST(AccountSet, Ticket) // Try using a ticket that alice doesn't have EXPECT_EQ( - env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq + 1), alice) + env.submit( + transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value() + 1), + alice) .ter, terPRE_TICKET); env.close(); @@ -636,7 +639,9 @@ TEST(AccountSet, Ticket) // Actually use alice's ticket (noop AccountSet) EXPECT_EQ( - env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter, + env.submit( + transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value()), alice) + .ter, tesSUCCESS); env.close(); @@ -649,7 +654,9 @@ TEST(AccountSet, Ticket) // Try re-using a ticket that alice already used EXPECT_EQ( - env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter, + env.submit( + transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value()), alice) + .ter, tefNO_TICKET); } diff --git a/src/xrpld/app/ledger/detail/LocalTxs.cpp b/src/xrpld/app/ledger/detail/LocalTxs.cpp index 5bfe8684f0..d540134f8d 100644 --- a/src/xrpld/app/ledger/detail/LocalTxs.cpp +++ b/src/xrpld/app/ledger/detail/LocalTxs.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -147,7 +148,7 @@ public: if (!sleAcct) return false; - SeqProxy const acctSeq = SeqProxy::sequence(sleAcct->getFieldU32(sfSequence)); + SeqProxy const acctSeq = SeqProxy::rawSequence(sleAcct->getFieldU32(sfSequence)); SeqProxy const seqProx = txn.getSeqProxy(); if (seqProx.isSeq()) diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 1af3b64161..771330367c 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -4121,7 +4121,7 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) if (accountId == kGenesisAccountId) { auto stx = tx->getSTransaction(); - if (stx->getAccountID(sfAccount) == accountId && stx->getSeqValue() == 1) + if (stx->getAccountID(sfAccount) == accountId && stx->getSeqProxy().value() == 1) return true; } diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index 041d2ade1e..b9cfc1d65c 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -773,7 +773,7 @@ TxQ::apply( return {terNO_ACCOUNT, false}; // If the transaction needs a Ticket is that Ticket in the ledger? - SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); + SeqProxy const acctSeqProx = SeqProxy::rawSequence((*sleAccount)[sfSequence]); SeqProxy const txSeqProx = tx->getSeqProxy(); if (txSeqProx.isTicket() && !view.exists(keylet::ticket(account, txSeqProx))) { @@ -1605,9 +1605,9 @@ TxQ::nextQueuableSeqImpl(SLE::const_ref sleAccount, std::scoped_lock // If the account is not in the ledger or a non-account was passed // then return zero. We have no idea. if (!sleAccount || sleAccount->getType() != ltACCOUNT_ROOT) - return SeqProxy::sequence(0); + return SeqProxy::rawSequence(0); - SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); + SeqProxy const acctSeqProx = SeqProxy::rawSequence((*sleAccount)[sfSequence]); // If the account is not in the queue then acctSeqProx is good enough. auto const accountIter = byAccount_.find((*sleAccount)[sfAccount]); @@ -1669,7 +1669,7 @@ TxQ::tryDirectApply( if (!sleAccount) return {}; - SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); + SeqProxy const acctSeqProx = SeqProxy::rawSequence((*sleAccount)[sfSequence]); SeqProxy const txSeqProx = tx->getSeqProxy(); // Can only directly apply if the transaction sequence matches the account diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp index 034cd8383f..c216192ab3 100644 --- a/src/xrpld/rpc/handlers/VaultInfo.cpp +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -48,7 +49,8 @@ parseVault(json::Value const& params, json::Value& jvResult) return std::nullopt; } - uNodeIndex = keylet::vault(*id, params[jss::seq].asUInt()).key; + auto const seq = SeqProxy::rawSequence(params[jss::seq].asUInt()); + uNodeIndex = keylet::vault(*id, seq).key; } else { diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index c618ad3b3a..d4232cf451 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -260,7 +261,7 @@ doAccountInfo(rpc::JsonContext& context) // We expect txs to be returned sorted by SeqProxy. Verify // that with a couple of asserts. - SeqProxy prevSeqProxy = SeqProxy::sequence(0); + SeqProxy prevSeqProxy = SeqProxy::rawSequence(0); for (auto const& tx : txs) { json::Value jvTx = json::ValueType::Object; diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 0dd52b6776..5271720b34 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -433,7 +434,8 @@ parseEscrow( if (!seq) return std::unexpected(seq.error()); - return keylet::escrow(*id, *seq).key; + auto const seqProxy = SeqProxy::rawSequence(*seq); + return keylet::escrow(*id, seqProxy).key; } auto const parseFeeSettings = fixed(keylet::feeSettings()); @@ -495,7 +497,8 @@ parseLoanBroker( if (!seq) return std::unexpected(seq.error()); - return keylet::loanBroker(*id, *seq).key; + auto const seqProxy = SeqProxy::rawSequence(*seq); + return keylet::loanBroker(*id, seqProxy).key; } static std::expected @@ -517,7 +520,8 @@ parseLoan( if (!seq) return std::unexpected(seq.error()); - return keylet::loan(*id, *seq).key; + auto const seqProxy = SeqProxy::rawSequence(*seq); + return keylet::loan(*id, seqProxy).key; } static std::expected @@ -600,7 +604,8 @@ parseOffer( if (!seq) return std::unexpected(seq.error()); - return keylet::offer(*id, *seq).key; + auto const seqProxy = SeqProxy::rawSequence(*seq); + return keylet::offer(*id, seqProxy).key; } static std::expected @@ -662,7 +667,8 @@ parsePermissionedDomain( if (!seq) return std::unexpected(seq.error()); - return keylet::permissionedDomain(*account, pd[jss::seq].asUInt()).key; + auto const seqProxy = SeqProxy::rawSequence(pd[jss::seq].asUInt()); + return keylet::permissionedDomain(*account, seqProxy).key; } static std::expected @@ -766,7 +772,8 @@ parseTicket( if (!seq) return std::unexpected(seq.error()); - return getTicketIndex(*id, *seq); + auto const seqProxy = SeqProxy::rawTicket(*seq); + return keylet::ticket(*id, seqProxy).key; } static std::expected @@ -788,7 +795,8 @@ parseVault( if (!seq) return std::unexpected(seq.error()); - return keylet::vault(*id, *seq).key; + auto const seqProxy = SeqProxy::rawSequence(*seq); + return keylet::vault(*id, seqProxy).key; } static std::expected From 94bccb3a5a781e342ed761e34236d4215152ab40 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Fri, 7 Aug 2026 17:53:54 -0400 Subject: [PATCH 46/68] fix: Fix MPT/DEX Audit/Attackathon reports (Phase 2) (#7537) Signed-off-by: dependabot[bot] Co-authored-by: Sergey Kuznetsov Co-authored-by: Ayaz Salikhov Co-authored-by: Andrzej Budzanowski Co-authored-by: Marek Foss Co-authored-by: Alex Kremer Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Co-authored-by: Bart Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> Co-authored-by: Mayukha Vadari Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- include/xrpl/ledger/helpers/EscrowHelpers.h | 25 ++- include/xrpl/protocol/detail/features.macro | 2 +- include/xrpl/tx/paths/AMMLiquidity.h | 10 +- src/libxrpl/tx/paths/AMMLiquidity.cpp | 19 +- src/libxrpl/tx/paths/AMMOffer.cpp | 4 + src/libxrpl/tx/paths/BookStep.cpp | 9 +- .../tx/transactors/check/CheckCash.cpp | 26 ++- src/libxrpl/tx/transactors/dex/AMMDeposit.cpp | 32 ++- .../tx/transactors/dex/AMMWithdraw.cpp | 33 ++- src/test/app/AMMMPT_test.cpp | 156 ++++++++++++++ src/test/app/AMM_test.cpp | 192 ++++++++++++++++-- src/test/app/CheckMPT_test.cpp | 53 +++++ src/test/app/EscrowToken_test.cpp | 68 +++++++ src/test/rpc/Feature_test.cpp | 6 +- 14 files changed, 567 insertions(+), 68 deletions(-) diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 9f54e53769..062443cd92 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -241,10 +243,25 @@ escrowUnlockApplyHelper( auto finalAmt = amount; if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate) { - // compute transfer fee, if any - auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true); - // compute balance to transfer - finalAmt = amount.value() - xferFee; + if (ctx.view.rules().enabled(fixCleanup3_4_0)) + { + XRPL_ASSERT( + lockedRate >= kParityRate, + "xrpl::escrowUnlockApplyHelper : lockedRate is at least parity"); + // MPTs are integral, so round the delivered amount down and + // charge any fractional transfer fee to the escrowed amount. + auto const delivered = + mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false); + finalAmt = STAmount(amount.asset(), delivered.value()); + } + else + { + // compute transfer fee, if any + auto const xferFee = + amount.value() - divideRound(amount, lockedRate, amount.asset(), true); + // compute balance to transfer + finalAmt = amount.value() - xferFee; + } } return unlockEscrowMPT( ctx.view, diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 4f1fac82da..de02fed7d8 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -59,7 +59,6 @@ XRPL_FIX (PreviousTxnID, Supported::Yes, VoteBehavior::DefaultNo XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PriceOracle, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FIX (AMMOverflowOffer, Supported::Yes, VoteBehavior::DefaultYes) XRPL_FIX (FillOrKill, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DID, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(XChainBridge, Supported::Yes, VoteBehavior::DefaultNo) @@ -100,6 +99,7 @@ XRPL_RETIRE_FIX(1578) XRPL_RETIRE_FIX(1623) XRPL_RETIRE_FIX(1781) XRPL_RETIRE_FIX(AmendmentMajorityCalc) +XRPL_RETIRE_FIX(AMMOverflowOffer) XRPL_RETIRE_FIX(CheckThreading) XRPL_RETIRE_FIX(DisallowIncomingV1) XRPL_RETIRE_FIX(InnerObjTemplate) diff --git a/include/xrpl/tx/paths/AMMLiquidity.h b/include/xrpl/tx/paths/AMMLiquidity.h index 1904445554..b08a0ec2f9 100644 --- a/include/xrpl/tx/paths/AMMLiquidity.h +++ b/include/xrpl/tx/paths/AMMLiquidity.h @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -124,17 +123,12 @@ private: generateFibSeqOffer(TAmounts const& balances) const; /** - * Generate max offer. - * If `fixAMMOverflowOffer` is active, the offer is generated as: + * Generate max offer. The offer is generated as: * takerGets = 99% * balances.out takerPays = swapOut(takerGets). * Return nullopt if takerGets is 0 or takerGets == balances.out. - * - * If `fixAMMOverflowOffer` is not active, the offer is generated as: - * takerPays = max input amount; - * takerGets = swapIn(takerPays). */ [[nodiscard]] std::optional> - maxOffer(TAmounts const& balances, Rules const& rules) const; + maxOffer(TAmounts const& balances) const; }; } // namespace xrpl diff --git a/src/libxrpl/tx/paths/AMMLiquidity.cpp b/src/libxrpl/tx/paths/AMMLiquidity.cpp index 0d1c66ead8..1b38847d7b 100644 --- a/src/libxrpl/tx/paths/AMMLiquidity.cpp +++ b/src/libxrpl/tx/paths/AMMLiquidity.cpp @@ -133,17 +133,8 @@ maxOut(T const& out, Asset const& asset) template std::optional> -AMMLiquidity::maxOffer(TAmounts const& balances, Rules const& rules) const +AMMLiquidity::maxOffer(TAmounts const& balances) const { - if (!rules.enabled(fixAMMOverflowOffer)) - { - return AMMOffer( - *this, - {maxAmount(), swapAssetIn(balances, maxAmount(), tradingFee_)}, - balances, - Quality{balances}); - } - auto const out = maxOut(balances.out, assetOut()); if (out <= TOut{0} || out >= balances.out) return std::nullopt; @@ -206,7 +197,7 @@ AMMLiquidity::getOffer(ReadView const& view, std::optional c // changed in BookStep per either deliver amount limit, or // sendmax, or available output or input funds. Might return // nullopt if the pool is small. - return maxOffer(balances, view.rules()); + return maxOffer(balances); } if (auto const amounts = changeSpotPriceQuality(balances, *clobQuality, tradingFee_, view.rules(), j_)) @@ -215,7 +206,7 @@ AMMLiquidity::getOffer(ReadView const& view, std::optional c } if (view.rules().enabled(fixAMMv1_2)) { - if (auto const maxAMMOffer = maxOffer(balances, view.rules()); + if (auto const maxAMMOffer = maxOffer(balances); maxAMMOffer && Quality{maxAMMOffer->amount()} > *clobQuality) return maxAMMOffer; } @@ -223,10 +214,6 @@ AMMLiquidity::getOffer(ReadView const& view, std::optional c catch (std::overflow_error const& e) { JLOG(j_.error()) << "AMMLiquidity::getOffer overflow " << e.what(); - if (!view.rules().enabled(fixAMMOverflowOffer)) - { - return maxOffer(balances, view.rules()); - } return std::nullopt; } diff --git a/src/libxrpl/tx/paths/AMMOffer.cpp b/src/libxrpl/tx/paths/AMMOffer.cpp index 3a7bd8f1df..a4a067c4f0 100644 --- a/src/libxrpl/tx/paths/AMMOffer.cpp +++ b/src/libxrpl/tx/paths/AMMOffer.cpp @@ -134,11 +134,13 @@ AMMOffer::checkInvariant(TAmounts const& consumed, beast:: { if (consumed.in > amounts_.in || consumed.out > amounts_.out) { + // LCOV_EXCL_START JLOG(j.error()) << "AMMOffer::checkInvariant failed: consumed " << to_string(consumed.in) << " " << to_string(consumed.out) << " amounts " << to_string(amounts_.in) << " " << to_string(amounts_.out); return false; + // LCOV_EXCL_STOP } Number const product = balances_.in * balances_.out; @@ -149,6 +151,7 @@ AMMOffer::checkInvariant(TAmounts const& consumed, beast:: if (newProduct >= product || withinRelativeDistance(product, newProduct, Number{1, -7})) return true; + // LCOV_EXCL_START JLOG(j.error()) << "AMMOffer::checkInvariant failed: balances " << to_string(balances_.in) << " " << to_string(balances_.out) << " new balances " << to_string(newBalances.in) << " " << to_string(newBalances.out) @@ -156,6 +159,7 @@ AMMOffer::checkInvariant(TAmounts const& consumed, beast:: << (product != Number{0} ? to_string((product - newProduct) / product) : "undefined"); return false; + // LCOV_EXCL_STOP } template class AMMOffer; diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index 71902ce8b9..e7c2e9ee29 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -865,12 +865,9 @@ BookStep::consumeOffer( { if (!offer.checkInvariant(ofrAmt, j_)) { - // purposely written as separate if statements so we get logging even - // when the amendment isn't active. - if (sb.rules().enabled(fixAMMOverflowOffer)) - { - Throw(tecINVARIANT_FAILED, "AMM pool product invariant failed."); - } + // LCOV_EXCL_START + Throw(tecINVARIANT_FAILED, "AMM pool product invariant failed."); + // LCOV_EXCL_STOP } // The offer owner gets the ofrAmt. The difference between ofrAmt and diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index a8c989f4df..e4d8f192c0 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -19,8 +19,9 @@ #include #include #include +#include #include -#include +#include #include #include #include @@ -376,18 +377,29 @@ CheckCash::doApply() else { // Note that for DeliverMin we don't know exactly how much - // currency we want flow to deliver. We can't ask for the - // maximum possible currency because there might be a gateway - // transfer rate to account for. Since the transfer rate cannot - // exceed 200%, we use 1/2 maxValue as our limit. + // currency we want flow to deliver. For IOUs, use a value + // higher than any real delivery as the request. MPTs are + // bounded integral amounts, so use the maximum output the check + // can actually deliver without exceeding SendMax. auto const maxDeliverMin = [&]() { return optDeliverMin->asset().visit( [&](Issue const&) { return STAmount( optDeliverMin->asset(), STAmount::kMaxValue / 2, STAmount::kMaxOffset); }, - [&](MPTIssue const&) { - return STAmount(optDeliverMin->asset(), kMaxMpTokenAmount / 2); + [&](MPTIssue const& issue) { + MPTAmount maxDeliver = sendMax.mpt(); + auto const& issuer = issue.getIssuer(); + if (srcId != issuer && accountID_ != issuer) + { + auto const rate = transferRate(psb, issue.getMptID()); + // Request at most floor(SendMax / rate). The endpoint reverse pass + // will quote ceil(output * rate), so this keeps the input + // representable and within SendMax. + maxDeliver = + mulRatio(maxDeliver, QUALITY_ONE, rate.value, /*roundUp*/ false); + } + return STAmount(maxDeliver, issue); }); }; STAmount const flowDeliver{ diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 0d1798babc..64d6d70e67 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include namespace xrpl { @@ -437,11 +438,10 @@ AMMDeposit::applyGuts(Sandbox& sb) auto const subTxType = ctx_.tx.getFlags() & tfDepositSubTx; - auto const [result, newLPTokenBalance] = [&, - &amountBalance = amountBalance, - &amount2Balance = amount2Balance, - &lptAMMBalance = - lptAMMBalance]() -> std::pair { + auto dispatchToDeposit = [&, + &amountBalance = amountBalance, + &amount2Balance = amount2Balance, + &lptAMMBalance = lptAMMBalance]() -> std::pair { if (subTxType & tfTwoAsset) { return equalDepositLimit( @@ -493,6 +493,28 @@ AMMDeposit::applyGuts(Sandbox& sb) JLOG(j_.error()) << "AMM Deposit: invalid options."; return std::make_pair(tecINTERNAL, STAmount{}); // LCOV_EXCL_STOP + }; + + auto const [result, newLPTokenBalance] = [&]() -> std::pair { + try + { + return dispatchToDeposit(); + } + catch (std::runtime_error const& e) + { + REACHABLE("xrpl::AMMDeposit::applyGuts : deposit amount out of range reached"); + // A deposit whose solved amount exceeds the integral asset's range + // throws while converting to STAmount: past int64max + // Number::operator rep() throws std::overflow_error; above the asset + // maximum STAmount::canonicalize throws std::runtime_error. Fail + // cleanly with a tec rather than letting it escape doApply as + // tefEXCEPTION. Any other exception is left to propagate. + // Gated by fixCleanup3_4_0 to preserve the legacy result pre-amendment. + if (!sb.rules().enabled(fixCleanup3_4_0)) + throw; // LCOV_EXCL_LINE - preserve legacy tefEXCEPTION + JLOG(j_.error()) << "AMMDeposit: deposit amount out of range " << e.what(); + return std::make_pair(tecAMM_FAILED, STAmount{}); + } }(); if (isTesSuccess(result)) diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 2baa7edfb4..5294dd0c7f 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -374,11 +375,10 @@ AMMWithdraw::applyGuts(Sandbox& sb) auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected; auto const subTxType = ctx_.tx.getFlags() & tfWithdrawSubTx; - auto const [result, newLPTokenBalance] = [&, - &amountBalance = amountBalance, - &amount2Balance = amount2Balance, - &lptAMMBalance = - lptAMMBalance]() -> std::pair { + auto dispatchToWithdraw = [&, + &amountBalance = amountBalance, + &amount2Balance = amount2Balance, + &lptAMMBalance = lptAMMBalance]() -> std::pair { if (subTxType & tfTwoAsset) { return equalWithdrawLimit( @@ -432,6 +432,29 @@ AMMWithdraw::applyGuts(Sandbox& sb) JLOG(j_.error()) << "AMM Withdraw: invalid options."; return std::make_pair(tecINTERNAL, STAmount{}); // LCOV_EXCL_STOP + }; + + auto const [result, newLPTokenBalance] = [&]() -> std::pair { + try + { + return dispatchToWithdraw(); + } + catch (std::runtime_error const& e) + { + // Defense in-depth for amount overflow/out-of-range: the withdrawal + // counterpart of the AMMDeposit guard. Unlike deposit, no known + // withdraw path can throw here - preclaim bounds the requested + // amounts by the pool balances, and the only historical throw + // (denom == 0 in singleWithdrawEPrice) is guarded under + // fixCleanup3_3_0. Gated by fixCleanup3_4_0 to preserve the + // legacy tefEXCEPTION pre-amendment. + if (!sb.rules().enabled(fixCleanup3_4_0)) + throw; + // LCOV_EXCL_START + JLOG(j_.error()) << "AMMWithdraw: amount out of range " << e.what(); + return std::make_pair(tecAMM_FAILED, STAmount{}); + // LCOV_EXCL_STOP + } }(); if (!isTesSuccess(result)) diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index bf0bc5c7d7..7078ea6769 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -7142,6 +7142,159 @@ private: } } + void + testDepositIntegralOverflowMPT(FeatureBitset features) + { + testcase("Deposit integral overflow (MPT)"); + + using namespace jtx; + + // Without fixCleanup3_4_0 the exception escapes and is converted to + // tefEXCEPTION by applySteps. With the amendment, applyGuts guards it + // and fails cleanly with tecAMM_FAILED. + auto const err = features[fixCleanup3_4_0] ? Ter(tecAMM_FAILED) : Ter(tefEXCEPTION); + + // MPT counterpart of AMM_test::testDepositIntegralOverflow. A two-asset + // deposit with a huge Amount against a tiny pool leg makes + // frac = Amount / balance enormous, so the computed deposit for the + // other (integral) leg exceeds Number's int64 range (kMaxRep ~= + // 9.22e18) and the conversion to an integral STAmount throws out of + // doApply - which applySteps would surface as tefEXCEPTION. + // + // The default amendments include fixCleanup3_4_0, under which applyGuts + // guards the overflow and fails cleanly with tecAMM_FAILED. This + // verifies the guarded path: no overflow escapes. + + // XRP/MPT - the exact pool the report (Antithesis) calls out. A tiny + // mpt(1) balance and a huge MPT Amount drive frac; the XRP leg is what + // overflows: XRP(10) is 1e7 drops, so getRoundedAsset(XRP, frac) is + // 1e7 * 1e13 = 1e20 drops, well past kMaxRep. + { + // The deposit intentionally overflows, which logs at error. + // Disable the log threshold to keep the test output clean. + Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled); + if (!features[fixCleanup3_4_0]) + env.disableFeature(fixCleanup3_4_0); + env.fund(XRP(30'000), gw_, alice_); + env.close(); + + // kMptDexFlags (CanTrade | CanTransfer), which AMMs require, is + // the default. alice must hold enough MPT to fund the pool and the + // oversized deposit. + MPT const mpt = MPTTester( + {.env = env, + .issuer = gw_, + .holders = {alice_}, + .pay = 100'000'000'000'000, // 1e14 + .maxAmt = 1'000'000'000'000'000}); // 1e15 + env.close(); + + AMM amm(env, alice_, XRP(10), mpt(1)); + amm.deposit( + DepositArg{ + .account = alice_, + .asset1In = mpt(10'000'000'000'000), // 1e13 + .asset2In = XRP(1), + .err = err}); + } + + // IOU/MPT - the MPT leg is the one that overflows. A classic IOU + // trustline drives frac (huge USD Amount vs USD(1) balance); the + // MPT-side deposit is then mptBalance * frac = 10'000 * 1e16 = 1e20. + { + // The deposit intentionally overflows, which logs at error. + // Disable the log threshold to keep the test output clean. + Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled); + env.fund(XRP(30'000), gw_, alice_); + env(trust(alice_, STAmount{USD, 1, 20})); + env(pay(gw_, alice_, STAmount{USD, 1, 18})); + env.close(); + + MPT const mpt = + MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 1'000'000}); + env.close(); + + AMM amm(env, alice_, mpt(10'000), USD(1)); + amm.deposit( + DepositArg{ + .account = alice_, + .asset1In = STAmount{USD, 1, 16}, + .asset2In = mpt(1), + .err = err}); + } + } + + void + testWithdrawIntegralNoOverflowMPT() + { + testcase("Withdraw integral no overflow (MPT)"); + + using namespace jtx; + + // MPT counterpart of AMM_test::testWithdrawIntegralNoOverflow and the + // sibling of testDepositIntegralOverflowMPT. AMMWithdraw:: + // equalWithdrawLimit has the same getRoundedAsset(integralBalance, + // frac) structure as the deposit path and is likewise not wrapped in a + // try/catch. It is safe only because withdraw preclaim (checkAmount) + // rejects a requested Amount greater than the pool balance with + // tecAMM_BALANCE *before* the math runs, so frac = Amount / balance + // stays <= 1 and the Number -> integral STAmount conversion cannot + // overflow. Deposit has no such bound, which is why only the deposit + // path was exposed. + // + // These mirror the deposit repros: the same oversized two-asset + // request is rejected cleanly. If the preclaim bound is ever weakened, + // equalWithdrawLimit would be reached with a huge frac and + // Number::operator rep() would escape as tefEXCEPTION, failing this. + + // XRP/MPT - the pool the report calls out. Requesting far more of the + // tiny MPT leg than the pool holds is rejected before the math. + { + Env env(*this); + env.fund(XRP(30'000), gw_, alice_); + env.close(); + + MPT const mpt = MPTTester( + {.env = env, + .issuer = gw_, + .holders = {alice_}, + .pay = 100'000'000'000'000, // 1e14 + .maxAmt = 1'000'000'000'000'000}); // 1e15 + env.close(); + + // alice holds all LPTokens of a tiny XRP/MPT pool. + AMM amm(env, alice_, XRP(10), mpt(1)); + amm.withdraw( + WithdrawArg{ + .account = alice_, + .asset1Out = mpt(10'000'000'000'000), // 1e13 > mpt(1) + .asset2Out = XRP(1), + .err = Ter(tecAMM_BALANCE)}); + } + + // IOU/MPT - requesting far more of the tiny IOU leg than the pool + // holds is likewise rejected. + { + Env env(*this); + env.fund(XRP(30'000), gw_, alice_); + env(trust(alice_, STAmount{USD, 1, 20})); + env(pay(gw_, alice_, STAmount{USD, 1, 18})); + env.close(); + + MPT const mpt = + MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 1'000'000}); + env.close(); + + AMM amm(env, alice_, mpt(10'000), USD(1)); + amm.withdraw( + WithdrawArg{ + .account = alice_, + .asset1Out = STAmount{USD, 1, 16}, // > USD(1) + .asset2Out = mpt(1), + .err = Ter(tecAMM_BALANCE)}); + } + } + void run() override { @@ -7178,6 +7331,9 @@ private: testAMMDepositWithFrozenAssets(); testAMMWithVaultShares(); testAutoDelete(); + testDepositIntegralOverflowMPT(all); + testDepositIntegralOverflowMPT(all - fixCleanup3_4_0); + testWithdrawIntegralNoOverflowMPT(); } }; diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index f19743026c..8f8079c34a 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -2292,8 +2293,9 @@ private: // ePrice = lptAMMBalance(100) * f(0.001) / amountBalance(100) = 0.001 testAMM( [&](AMM& ammAlice, Env& env) { - auto const err = - env.enabled(fixCleanup3_3_0) ? Ter(tecAMM_FAILED) : Ter(tefEXCEPTION); + auto const err = env.enabled(fixCleanup3_3_0) || env.enabled(fixCleanup3_4_0) + ? Ter(tecAMM_FAILED) + : Ter(tefEXCEPTION); ammAlice.withdraw( WithdrawArg{ .account = alice_, @@ -2304,7 +2306,7 @@ private: {{USD(100), EUR(100)}}, 1000, std::nullopt, - {all - fixCleanup3_3_0, all}); + {all - fixCleanup3_3_0 - fixCleanup3_4_0, all - fixCleanup3_4_0, all}); } void @@ -6024,7 +6026,7 @@ private: void // NOLINTNEXTLINE(readability-convert-member-functions-to-static) - testFixOverflowOffer(FeatureBitset featuresInitial) + testOverflowOffer(FeatureBitset featuresInitial) { using namespace jtx; using namespace std::chrono; @@ -6259,7 +6261,7 @@ private: }) { testcase(input.testCase); - for (auto const& features : {all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all}) + for (auto const& features : {all - fixAMMv1_1 - fixAMMv1_3, all}) { Env env(*this, features, std::make_unique(&logs)); @@ -6308,11 +6310,6 @@ private: return input.lpTokenBalanceAlt.value_or(input.lpTokenBalance); }(); - if (!features[fixAMMOverflowOffer]) - { - BEAST_EXPECT(amm.expectBalances(failUsdGH, failUsdBIT, lpTokenBalance)); - } - else { BEAST_EXPECT(amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance)); @@ -7210,6 +7207,172 @@ private: } } + void + testDepositIntegralOverflow() + { + testcase("Deposit integral overflow"); + + using namespace jtx; + auto const all = testableAmendments(); + + // Found by Antithesis: two-asset deposit with a huge Amount against a + // tiny pool leg makes frac = Amount/amountBalance enormous, so the + // computed XRP-side deposit exceeds the integral asset's range and the + // conversion to an STAmount throws out of doApply. + // + // applyGuts catches std::runtime_error around the deposit math, which + // covers both ways the conversion can throw: + // - value beyond int64 range: Number::operator rep() throws + // std::overflow_error (a std::runtime_error); and + // - value within int64 but above the asset maximum (kMaxNativeN): + // STAmount::canonicalize throws std::runtime_error. + // XRP(10) is 1e7 drops, so the computed XRP leg is 1e7 * frac: + // asset1In 1e15 => frac ~1e15 => ~1e22 drops, past int64max; and + // asset1In 1e11 => frac ~1e11 => ~1e18 drops, in [kMaxNativeN=1e17, + // int64max) - the canonicalize band, which would otherwise escape. + // + // Without fixCleanup3_4_0 the exception escapes and is converted to + // tefEXCEPTION by applySteps. With the amendment, applyGuts guards it + // and fails cleanly with tecAMM_FAILED. + auto const test = [this](FeatureBitset features, STAmount const& asset1In, TER expected) { + // These deposits intentionally trigger the overflow, which logs + // at error (guarded) or fatal (legacy tefEXCEPTION). Disable the + // log threshold to keep the test output clean. + Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled); + env.fund(XRP(30'000), gw_, alice_); + env(trust(alice_, STAmount{USD, 1, 20})); + env(pay(gw_, alice_, STAmount{USD, 1, 18})); + env.close(); + + AMM amm(env, gw_, XRP(10), USD(1)); + amm.deposit( + DepositArg{ + .account = alice_, + .asset1In = asset1In, + .asset2In = XRP(1), + .err = Ter(expected)}); + }; + + // int64-range band (overflow_error): legacy escapes as tefEXCEPTION, + // fixed returns a tec. + test(all - fixCleanup3_4_0, STAmount{USD, 1, 15}, tefEXCEPTION); + test(all, STAmount{USD, 1, 15}, tecAMM_FAILED); + // canonicalize band (runtime_error): same behavior. Regression guard + // for the band a plain overflow_error catch would miss. + test(all - fixCleanup3_4_0, STAmount{USD, 1, 11}, tefEXCEPTION); + test(all, STAmount{USD, 1, 11}, tecAMM_FAILED); + } + + void + testDepositEPriceIntegralOverflow() + { + testcase("Deposit EPrice integral overflow"); + + using namespace jtx; + auto const all = testableAmendments(); + + // Found by Antithesis: a one-sided tfLimitLPToken deposit (Amount and + // EPrice) with Amount = 0 and a large EPrice makes the solved pool-side + // deposit enormous, so it exceeds the integral asset's range and the + // conversion to an STAmount throws out of doApply. This is the + // singleDepositEPrice sibling of testDepositIntegralOverflow. + // + // applyGuts catches std::runtime_error around the deposit math, which + // covers both ways the conversion can throw: + // - value beyond int64 range: Number::operator rep() throws + // std::overflow_error (a std::runtime_error); and + // - value within int64 but above the asset maximum (kMaxNativeN): + // STAmount::canonicalize throws std::runtime_error. + // + // Without fixCleanup3_4_0 the exception escapes and is converted to + // tefEXCEPTION by applySteps. With the amendment, applyGuts guards it + // and fails cleanly with tecAMM_FAILED. + auto const test = [this](FeatureBitset features, STAmount const& ePrice, TER expected) { + // These deposits intentionally trigger the overflow, which logs + // at error (guarded) or fatal (legacy tefEXCEPTION). Disable the + // log threshold to keep the test output clean. + Env env(*this, envconfig(), features, nullptr, beast::Severity::Disabled); + env.fund(XRP(30'000), gw_, alice_); + env(trust(alice_, STAmount{USD, 1, 20})); + env(pay(gw_, alice_, STAmount{USD, 1, 18})); + env.close(); + + AMM amm(env, gw_, XRP(10), USD(1)); + // Amount = 0 (XRP), EPrice large => tfLimitLPToken. The solved XRP + // leg blows past the integral range. + amm.deposit( + DepositArg{ + .account = alice_, .asset1In = XRP(0), .maxEP = ePrice, .err = Ter(expected)}); + }; + + // For this XRP(10)/USD(1) pool the LPToken balance is + // sqrt(1e7 drops * 1) = 3162, so T^2/B = 1e7/1e7 = 1 and the solved + // XRP-side deposit is ~EPrice^2 drops. + // + // int64-range band (overflow_error): legacy escapes as tefEXCEPTION, + // fixed returns a tec. EPrice ~1e17 drops => solved deposit ~1e34 drops, + // past int64max, so Number::operator rep() throws. + auto const bigEP = STAmount{XRPAmount{99'999'999'999'999'999}}; + test(all - fixCleanup3_4_0, bigEP, tefEXCEPTION); + test(all, bigEP, tecAMM_FAILED); + // canonicalize band (runtime_error): same behavior. Regression guard + // for the band a plain overflow_error catch would miss. EPrice 1e9 drops + // => solved deposit ~1e18 drops, in [kMaxNativeN=1e17, int64max), so + // STAmount::canonicalize throws. + auto const midEP = STAmount{XRPAmount{1'000'000'000}}; + test(all - fixCleanup3_4_0, midEP, tefEXCEPTION); + test(all, midEP, tecAMM_FAILED); + } + + void + testWithdrawIntegralNoOverflow() + { + testcase("Withdraw integral no overflow"); + + using namespace jtx; + auto const all = testableAmendments(); + + // Regression guard for the sibling of testDepositIntegralOverflow. + // AMMWithdraw::equalWithdrawLimit has the same + // getRoundedAsset(integralBalance, frac) structure as the deposit + // path and is likewise not wrapped in a try/catch. It is safe only + // because withdraw preclaim (checkAmount) rejects a requested Amount + // greater than the pool balance with tecAMM_BALANCE *before* the math + // runs, so frac = Amount / balance stays <= 1 and the Number -> + // integral STAmount conversion cannot overflow. Deposit has no such + // bound (depositing more than the pool holds is legal), which is why + // only the deposit path was exposed. + // + // This asserts the withdrawal analog of the deposit repro fails cleanly + // with a tec. If the preclaim bound is ever weakened, equalWithdrawLimit + // would be reached with a huge frac and Number::operator rep() would + // escape as tefEXCEPTION, failing this test. + auto const test = [this](FeatureBitset features) { + Env env(*this, features); + env.fund(XRP(30'000), gw_, alice_); + env(trust(alice_, STAmount{USD, 1, 20})); + env(pay(gw_, alice_, STAmount{USD, 1, 18})); + env.close(); + + // gw holds all LPTokens of a tiny XRP/USD pool. + AMM amm(env, gw_, XRP(10), USD(1)); + + // Two-asset limit withdraw (tfTwoAsset) requesting far more of the + // tiny USD leg than the pool holds - the mirror of the deposit + // repro. Rejected upstream, so no overflow is possible. + amm.withdraw( + WithdrawArg{ + .account = gw_, + .asset1Out = STAmount{USD, 1, 15}, + .asset2Out = XRP(1), + .err = Ter(tecAMM_BALANCE)}); + }; + + // Bound holds regardless of the deposit-side fix amendment. + test(all - featureMPTokensV2); + test(all); + } + void run() override { @@ -7251,9 +7414,9 @@ private: testSelection(all - fixAMMv1_1 - fixAMMv1_3); testFixDefaultInnerObj(); testMalformed(); - testFixOverflowOffer(all); - testFixOverflowOffer(all - fixAMMv1_3); - testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); + testOverflowOffer(all); + testOverflowOffer(all - fixAMMv1_3); + testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3); @@ -7282,6 +7445,9 @@ private: testFailedPseudoAccount(); testStaleAuthAccountsAfterReinit(all); testStaleAuthAccountsAfterReinit(all - fixCleanup3_2_0); + testDepositIntegralOverflow(); + testDepositEPriceIntegralOverflow(); + testWithdrawIntegralNoOverflow(); } }; diff --git a/src/test/app/CheckMPT_test.cpp b/src/test/app/CheckMPT_test.cpp index 66cc582201..ffc9fb21b4 100644 --- a/src/test/app/CheckMPT_test.cpp +++ b/src/test/app/CheckMPT_test.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -653,6 +654,32 @@ class CheckMPT_test : public beast::unit_test::Suite BEAST_EXPECT(ownerCount(env, alice) == 1); BEAST_EXPECT(ownerCount(env, bob) == 1); } + + { + Env env{*this, features}; + + env.fund(XRP(1'000), gw, alice, bob); + + // MPT DeliverMin should not be capped at half of the legal range. + std::uint64_t constexpr deliverMin = (kMaxMpTokenAmount / 2) + 1; + MPT const usd = MPTTester( + {.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = kMaxMpTokenAmount}); + + env(pay(gw, alice, usd(deliverMin))); + env.close(); + + uint256 const chkId{getCheckIndex(alice, env.seq(alice))}; + env(check::create(alice, bob, usd(deliverMin))); + env.close(); + + env(check::cash(bob, chkId, check::DeliverMin(usd(deliverMin)))); + verifyDeliveredAmount(env, usd(deliverMin)); + env.require(Balance(alice, usd(0))); + env.require(Balance(bob, usd(deliverMin))); + BEAST_EXPECT(checksOnAccount(env, alice).empty()); + BEAST_EXPECT(checksOnAccount(env, bob).empty()); + } + { // Examine the effects of the asfRequireAuth flag. Env env(*this, features); @@ -807,6 +834,32 @@ class CheckMPT_test : public beast::unit_test::Suite env.require(Balance(bob, usd(0 + 100))); BEAST_EXPECT(checksOnAccount(env, alice).empty()); BEAST_EXPECT(checksOnAccount(env, bob).empty()); + + // With the maximum transfer fee, this is the largest output whose + // fee-adjusted debit is still within SendMax. + std::uint64_t constexpr maxDeliver = (kMaxMpTokenAmount / 3) * 2; + MPT const eur = MPTTester( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = kMaxTransferFee, + .maxAmt = kMaxMpTokenAmount}); + + env(pay(gw, alice, eur(kMaxMpTokenAmount))); + env.close(); + + uint256 const chkIdMax{getCheckIndex(alice, env.seq(alice))}; + env(check::create(alice, bob, eur(kMaxMpTokenAmount))); + env.close(); + + // The DeliverMin cap must divide SendMax by the rate before flow() + // computes the fee-adjusted input. + env(check::cash(bob, chkIdMax, check::DeliverMin(eur(maxDeliver)))); + verifyDeliveredAmount(env, eur(maxDeliver)); + env.require(Balance(alice, eur(1))); + env.require(Balance(bob, eur(maxDeliver))); + BEAST_EXPECT(checksOnAccount(env, alice).empty()); + BEAST_EXPECT(checksOnAccount(env, bob).empty()); } void diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index d0decaf497..7e7509c3b7 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3683,6 +3683,72 @@ struct EscrowToken_test : public beast::unit_test::Suite } } + void + testMPTSplitEscrowTransferFee(FeatureBitset features) + { + using namespace test::jtx; + using namespace std::literals; + + bool const withCleanup340 = features[fixCleanup3_4_0]; + testcase( + std::string("MPT Split Escrow Transfer Fee ") + + (withCleanup340 ? "with Cleanup340" : "without Cleanup340")); + + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + env.fund(XRP(1'000), alice, bob, gw); + env.close(); + + MPTTester const mpt({ + .env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer, + }); + env(pay(gw, alice, mpt(10'000))); + env.close(); + + static constexpr int escrowCount = 10; + static constexpr int splitAmount = 10; + static constexpr int totalLocked = escrowCount * splitAmount; + std::array seqs{}; + for (auto& seq : seqs) + { + seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(splitAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + Fee(baseFee * 150)); + env.close(); + } + + BEAST_EXPECT(env.balance(alice, mpt) == mpt(10'000 - totalLocked)); + BEAST_EXPECT(env.balance(bob, mpt) == mpt(0)); + BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10'000)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == totalLocked); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == totalLocked); + + for (auto const seq : seqs) + { + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150)); + env.close(); + } + + auto const feeBurned = withCleanup340 ? escrowCount : 0; + BEAST_EXPECT(env.balance(alice, mpt) == mpt(10'000 - totalLocked)); + BEAST_EXPECT(env.balance(bob, mpt) == mpt(totalLocked - feeBurned)); + BEAST_EXPECT(env.balance(gw, mpt) == mpt(-10'000 + feeBurned)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + } + void testMPTRequireAuth(FeatureBitset features) { @@ -4001,6 +4067,8 @@ public: testMPTWithFeats(feats); testMPTWithFeats(feats - fixTokenEscrowV1); } + testMPTSplitEscrowTransferFee(all - fixCleanup3_4_0); + testMPTSplitEscrowTransferFee(all); } }; diff --git a/src/test/rpc/Feature_test.cpp b/src/test/rpc/Feature_test.cpp index a36e51cb6f..1e2504bf7f 100644 --- a/src/test/rpc/Feature_test.cpp +++ b/src/test/rpc/Feature_test.cpp @@ -187,13 +187,13 @@ class Feature_test : public beast::unit_test::Suite using namespace test::jtx; Env env{*this}; - std::string const name = "fixAMMOverflowOffer"; + std::string const name = "fixCleanup3_1_3"; auto jrr = env.rpc("feature", name)[jss::result]; BEAST_EXPECTS(jrr[jss::status] == jss::success, "status"); jrr.removeMember(jss::status); BEAST_EXPECT(jrr.size() == 1); auto const expected = to_string(sha512Half(Slice(name.data(), name.size()))); - char const sha[] = "12523DF04B553A0B1AD74F42DDB741DE8DC06A03FC089A0EF197E2A87F1D8107"; + char const sha[] = "303ACB16CF8DBD3B5C34F131A9D19A7DE01AE05F480A8A682B869D1B4AAC8CFC"; BEAST_EXPECT(expected == sha); BEAST_EXPECT(jrr.isMember(expected)); auto feature = *(jrr.begin()); @@ -475,7 +475,7 @@ class Feature_test : public beast::unit_test::Suite using namespace test::jtx; Env env{*this, FeatureBitset{featurePriceOracle}}; - static constexpr char const* kFeatureName = "fixAMMOverflowOffer"; + static constexpr char const* kFeatureName = "fixCleanup3_1_3"; auto jrr = env.rpc("feature", kFeatureName)[jss::result]; if (!BEAST_EXPECTS(jrr[jss::status] == jss::success, "status")) From 63d8772f69fc8fff90bc6b5f982978d7fd7b1df1 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 10 Aug 2026 12:58:49 +0100 Subject: [PATCH 47/68] chore: Remove corrosion from nix (#7982) --- nix/packages.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/nix/packages.nix b/nix/packages.nix index 01ab2ecf9a..3dfc6dff98 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -146,7 +146,6 @@ in cargo-audit cargo-llvm-cov cargo-nextest - corrosion rustToolchain ]; } From 07b9c59b89aae10aec31e012f8782fec388c84fa Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 10 Aug 2026 14:08:15 +0100 Subject: [PATCH 48/68] build: Remove protobuf dependencies from Nix (#7984) --- nix/packages.nix | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nix/packages.nix b/nix/packages.nix index 3dfc6dff98..0623ff51b9 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -129,15 +129,6 @@ in perl # needed for openssl pkg-config pre-commit - # protoc generates the Go gRPC bindings and embeds its own version string into every committed - # .pb.go file. To allow CI to verify those files with a plain `git diff`, we pin the version to - # `protobuf_34` rather than the rolling `protobuf` to keep regeneration reproducible across the - # Nix frequently changing unstable channel. The protoc-gen-go* plugins have no versioned - # attributes in nixpkgs; protoc-gen-go's version is in turn constrained by the go.mod require - # on google.golang.org/protobuf. - protobuf_34 # provides protoc - protoc-gen-go # protoc plugin for the Go message bindings - protoc-gen-go-grpc # protoc plugin for the Go gRPC service stubs python3 runClangTidy vim From a24caaa6eae9fb9d1cdfa80a4ba2e26ccaa55753 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 10 Aug 2026 16:00:30 +0100 Subject: [PATCH 49/68] docs: Rearrange & simplify build/nix/environment docs (#7985) --- BUILD.md | 54 +++++++++----------------- docs/build/environment.md | 81 +++++++++++++++++++++++++-------------- docs/build/nix.md | 16 +++----- 3 files changed, 77 insertions(+), 74 deletions(-) diff --git a/BUILD.md b/BUILD.md index 238c10e17c..ad4666b141 100644 --- a/BUILD.md +++ b/BUILD.md @@ -4,34 +4,14 @@ ## Minimum Requirements -See [System Requirements](https://xrpl.org/system-requirements.html). +For the hardware needed to run a node, see +[System Requirements](https://xrpl.org/system-requirements.html). -Building xrpld generally requires Git, Python, Conan, CMake, and a C++ -compiler. - -- [Python](https://www.python.org/downloads/) -- [Conan](https://conan.io/downloads.html) -- [CMake](https://cmake.org/download/) - -You can verify that the required tools are installed and runnable with: - -```bash -./bin/check-tools.sh -``` - -`xrpld` is written in the C++23 dialect. The [tested compiler versions][cpp23-support] are: - -| Compiler | Version | -| ----------- | --------------- | -| GCC | 15.2 | -| Clang | 22 | -| Apple Clang | 21 | -| MSVC | 19.44[^windows] | +For the software needed to build xrpld, see the +[environment setup guide](./docs/build/environment.md). ## Operating Systems -Please see the [environment setup guide](./docs/build/environment.md) for detailed instructions for all platforms. - ### Linux The Ubuntu Linux distribution has received the highest level of quality @@ -47,9 +27,8 @@ CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOY ### Windows -Windows is used by some engineers for development only. - -[^windows]: Windows is not recommended for production use. +Windows is used by some engineers for development only, and is not recommended +for production use. ## Steps @@ -74,12 +53,8 @@ releases](https://github.com/XRPLF/rippled/releases). ### Set Up Conan -After you have a [C++ development environment](./docs/build/environment.md) ready with Git, Python, -Conan, CMake, and a C++ compiler, you may need to set up your Conan profile. - -These instructions assume a basic familiarity with Conan and CMake. If you are -unfamiliar with Conan, then please read [this crash course](./docs/build/conan.md) or the official -[Getting Started][conan-getting-started] walkthrough. +Once your [development environment](./docs/build/environment.md) is ready, you +may need to set up your Conan profile. #### Profiles @@ -269,10 +244,14 @@ which is only enabled when the `coverage` option is set, e.g. with Prerequisites for the coverage report: - [gcovr tool][gcovr] (can be installed e.g. with [pip][python-pip]) -- `gcov` for GCC (installed with the compiler by default) or -- `llvm-cov` for Clang (installed with the compiler by default) +- `gcov` for GCC or `llvm-cov` for Clang, usually installed with the compiler - `Debug` build type +> [!NOTE] +> Clang coverage is not available in the [Nix development shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell): +> its `clang` shells do not ship `llvm-cov`. Use a `gcc` shell instead (`.#gcc`, +> or `.#gcc-plain` on Linux), which provides a `gcov` matching its compiler. + A coverage report is created when the following steps are completed, in order: 1. `xrpld` binary built with instrumentation data, enabled by the `coverage` @@ -389,6 +368,10 @@ After any updates or changes to dependencies, you may need to do the following: 4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile). 5. Re-run [conan install](#build-and-test). +If you are using the Nix development shell, prebuilt Conan binaries may be +incompatible with it — see +[Building xrpld in the Nix shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell). + #### ERROR: Package not resolved If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, @@ -412,7 +395,6 @@ For example, if you want to build Debug: 1. For conan install, pass `--settings build_type=Debug` 2. For cmake, pass `-DCMAKE_BUILD_TYPE=Debug` -[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23 [conan-getting-started]: https://docs.conan.io/en/latest/getting_started.html [unity-build]: https://en.wikipedia.org/wiki/Unity_build [gcovr]: https://gcovr.com/en/stable/getting-started.html diff --git a/docs/build/environment.md b/docs/build/environment.md index e639ed2d5f..5616f32f37 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -6,22 +6,52 @@ This document explains how to set one up. ## Tested compiler versions -`xrpld` is built in the **C++23** dialect by default. -Make sure your toolchain is recent enough — the compiler versions currently tested in CI are: +`xrpld` is built in the **C++23** dialect by default, so your toolchain has to +support it — see [compiler support for C++23][cpp23-support]. +The versions currently tested in CI are: -| Compiler | Version | -| ----------- | ------- | -| GCC | 15.2 | -| Clang | 22 | -| Apple Clang | 17 | -| MSVC | 19.44 | +| Compiler | Version | +| ----------- | ------------------ | +| GCC | 15.2 | +| Clang | 22 | +| Apple Clang | 21 | +| MSVC | Visual Studio 2026 | LLVM tools (`clang-tidy` and `clang-format`) are also pinned to version 22. +### Older compilers + Older compilers may fail to build the latest `develop` code: the codebase now relies on C++23 features and has been adjusted for `clang-tidy`. If the latest code doesn't build for you, update your build toolchain first. +If updating isn't an option for you, we do accept pull requests that fix builds +on older compilers, as long as the change is small and doesn't make the code +harder to read. What we can't promise is that older compilers will keep working: +only the versions in the table above are tested in CI, and we won't hold back +the use of C++23 features or add invasive workarounds to keep an untested +compiler building. Treat support for anything outside the table as best-effort. + +## Required tools + +Besides a compiler, building `xrpld` requires: + +| Tool | Minimum version | +| ------------------------------------------- | --------------- | +| [Git](https://git-scm.com/downloads) | any recent | +| [Python](https://www.python.org/downloads/) | 3.11 | +| [Conan](https://conan.io/downloads.html) | 2.17 | +| [CMake](https://cmake.org/download/) | 3.16 | + +On Linux and macOS, the [Nix development shell](./nix.md) provides all of them +(see below). On Windows they have to be installed manually. + +Once they are in place, verify that everything is installed and runnable with: + +```bash +./bin/check-tools.sh +``` + ## Linux and macOS The **recommended way** to get a development environment on Linux and macOS is @@ -39,20 +69,15 @@ Clang. If you instead opt to use your system-wide Apple Clang (via below). See [Using the Nix development shell](./nix.md) for installation and usage -details, including how to select a different compiler. - -> [!NOTE] -> Using Nix is not mandatory. Any custom environment (Homebrew packages or -> anything else) will continue to work, but then it is up to you to keep it in -> sync with the environment used in CI. Nix unifies the development environment -> for everyone and synchronizes updates, which is why we recommend it. +details, including how to select a different compiler and why we recommend Nix +over a hand-maintained environment. ### macOS: managing the Apple Clang version If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`), the compiler version is whatever your installed Xcode (or Command Line Tools) provides. The following command should return a version greater than or equal to -the [minimum required](#tested-compiler-versions): +the [tested one](#tested-compiler-versions): ```bash clang --version @@ -89,23 +114,23 @@ building xrpld. You may want to install and pin a specific version of Xcode: Nix is not available on Windows, so the required tools have to be installed manually: -- [Visual Studio 2022](https://visualstudio.microsoft.com/) with the +- [Visual Studio 2026](https://visualstudio.microsoft.com/) with the **"Desktop development with C++"** workload — this provides MSVC and the - "x64 Native Tools Command Prompt". + "x64 Native Tools Command Prompt". CI configures CMake with the + `Visual Studio 18 2026` generator. - [Git for Windows](https://git-scm.com/download/win) -- [Python 3.11](https://www.python.org/downloads/), or higher -- [Conan 2.17](https://conan.io/downloads.html), or higher -- [CMake 3.22](https://cmake.org/download/), or higher - -> [!NOTE] -> Windows is used for development only and is not recommended for production. +- Python, Conan, and CMake, at the versions listed in + [Required tools](#required-tools). ## Clang-tidy `clang-tidy` is required to run static analysis checks locally (see [CONTRIBUTING.md](../../CONTRIBUTING.md)). It is not required to build the -project. This project currently uses `clang-tidy` version 22. +project. The version this project uses is listed in +[Tested compiler versions](#tested-compiler-versions). -On Linux and macOS, the [Nix development shell](./nix.md) provides `clang-tidy` -22 out of the box — run it via `run-clang-tidy`. No separate installation is -needed. +On Linux and macOS, the [Nix development shell](./nix.md) provides that exact +version out of the box — run it via `run-clang-tidy`. No separate installation +is needed. + +[cpp23-support]: https://en.cppreference.com/w/cpp/compiler_support/23 diff --git a/docs/build/nix.md b/docs/build/nix.md index d0001294e3..fad8bc701d 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -120,11 +120,15 @@ nix develop -c "$SHELL" > > If it doesn't, either adjust your shell configuration so it doesn't override `$PATH`, or use [direnv](#automatic-activation-with-direnv) (below), which loads the environment _after_ your shell config and so takes precedence regardless of the shell you use. -## Building xrpld with Nix +## Building xrpld in the Nix shell Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). -Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Linux): +Two things differ from a system environment: + +**Prebuilt Conan packages.** There is no guarantee that binaries from the Conan cache will work when using Nix. If you encounter any errors, add `--build '*'` to the `conan install` command in [Build and Test](../../BUILD.md#build-and-test) to force Conan to compile everything from source. Keep the rest of the command as it is there, so it rebuilds the `build_type` you are actually configuring. + +**Coverage builds.** `-Dcoverage=ON` works in the `gcc` shell (and `gcc-plain` on Linux): each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one. The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage. @@ -142,14 +146,6 @@ The repository already ships an `.envrc` at its root that activates the Nix flak > [!NOTE] > direnv only caches the `.direnv` directory (already listed in `.gitignore`); no other repository files are affected. -## Conan and Prebuilt Packages - -Please note that there is no guarantee that binaries from conan cache will work when using nix. If you encounter any errors, please use `--build '*'` to force conan to compile everything from source: - -```bash -conan install .. --output-folder . --build '*' --settings build_type=Release -``` - ## Updating `flake.lock` file To update `flake.lock` to the latest revision use `nix flake update` command. From 71e972cbed9006a475ae326cf41b8959ef8d25f9 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:05:18 -0400 Subject: [PATCH 50/68] refactor: Act on TODOs that are unblocked by C++23 (#7990) --- src/libxrpl/protocol/ErrorCodes.cpp | 5 ++- src/test/app/Invariants_test.cpp | 10 ++---- src/test/jtx/TestHelpers.h | 13 ++------ src/xrpld/app/misc/FeeVoteImpl.cpp | 28 +++++++--------- src/xrpld/overlay/detail/ProtocolVersion.cpp | 33 ++++++------------- .../server_info/ServerDefinitions.cpp | 1 - 6 files changed, 29 insertions(+), 61 deletions(-) diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index e81f975844..802bae100d 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -105,10 +105,9 @@ static constexpr ErrorInfo kUnorderedErrorInfos[]{ }; // clang-format on -// Sort and validate unorderedErrorInfos at compile time. Should be -// converted to consteval when get to C++20. +// Sort and validate unorderedErrorInfos at compile time. template -constexpr auto +consteval auto sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array { std::array ret = {}; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index ffdfe6bc83..ed09b7b660 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -437,16 +437,10 @@ class Invariants_test : public beast::unit_test::Suite XRPAmount{}, STTx{ttACCOUNT_DELETE, [](STObject& tx) {}}); - for (auto const& keyletInfo : kDirectAccountKeylets) + for (auto const& [keyletfunc, type, includeInTests] : kDirectAccountKeylets) { - // TODO: Use structured binding once LLVM 16 is the minimum - // supported version. See also: - // https://github.com/llvm/llvm-project/issues/48582 - // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c - if (!keyletInfo.includeInTests) + if (!includeInTests) continue; - auto const& keyletfunc = keyletInfo.function; - auto const& type = keyletInfo.expectedLEName; using namespace std::string_literals; diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 5c8486e6c5..801c3627b8 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -315,19 +316,11 @@ auto const kData = JTxFieldWrapper(sfData); auto const kAmount = JTxFieldWrapper(sfAmount); -// TODO We only need this long "requires" clause as polyfill, for C++20 -// implementations which are missing header. Replace with -// `std::ranges::range`, and accordingly use std::ranges::begin/end -// when we have moved to better compilers. -template +template auto makeVector(Input const& input) - requires requires(Input& v) { - std::begin(v); - std::end(v); - } { - return std::vector(std::begin(input), std::end(input)); + return std::vector(std::ranges::begin(input), std::ranges::end(input)); } // Functions used in debugging diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp index 76a4d8f186..f1cb944a52 100644 --- a/src/xrpld/app/misc/FeeVoteImpl.cpp +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp @@ -260,39 +260,35 @@ FeeVoteImpl::doVoting( } // choose our positions - // TODO: Use structured binding once LLVM 16 is the minimum supported - // version. See also: https://github.com/llvm/llvm-project/issues/48582 - // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c - auto const baseFee = baseFeeVote.getVotes(); - auto const baseReserve = baseReserveVote.getVotes(); - auto const incReserve = incReserveVote.getVotes(); + auto const [baseFee, baseFeeChanged] = baseFeeVote.getVotes(); + auto const [baseReserve, baseReserveChanged] = baseReserveVote.getVotes(); + auto const [incReserve, incReserveChanged] = incReserveVote.getVotes(); auto const seq = lastClosedLedger->header().seq + 1; // add transactions to our position - if (baseFee.second || baseReserve.second || incReserve.second) + if (baseFeeChanged || baseReserveChanged || incReserveChanged) { - JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee.first << "/" - << baseReserve.first << "/" << incReserve.first; + JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee << "/" << baseReserve + << "/" << incReserve; STTx const feeTx(ttFEE, [=, &rules](auto& obj) { obj[sfAccount] = AccountID(); obj[sfLedgerSequence] = seq; if (rules.enabled(featureXRPFees)) { - obj[sfBaseFeeDrops] = baseFee.first; - obj[sfReserveBaseDrops] = baseReserve.first; - obj[sfReserveIncrementDrops] = incReserve.first; + obj[sfBaseFeeDrops] = baseFee; + obj[sfReserveBaseDrops] = baseReserve; + obj[sfReserveIncrementDrops] = incReserve; } else { // Without the featureXRPFees amendment, these fields are // required. - obj[sfBaseFee] = baseFee.first.dropsAs(baseFeeVote.current()); - obj[sfReserveBase] = - baseReserve.first.dropsAs(baseReserveVote.current()); + obj[sfBaseFee] = baseFee.dropsAs(baseFeeVote.current()); + obj[sfReserveBase] = baseReserve.dropsAs(baseReserveVote.current()); obj[sfReserveIncrement] = - incReserve.first.dropsAs(incReserveVote.current()); + incReserve.dropsAs(incReserveVote.current()); obj[sfReferenceFeeUnits] = kFeeUnitsDeprecated; } }); diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp b/src/xrpld/overlay/detail/ProtocolVersion.cpp index 2d5d0a56f7..93d4fae156 100644 --- a/src/xrpld/overlay/detail/ProtocolVersion.cpp +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -32,31 +33,17 @@ constexpr ProtocolVersion const kSupportedProtocolList[]{ {2, 3}, }; -// This ugly construct ensures that supportedProtocolList is sorted in strictly -// ascending order and doesn't contain any duplicates. -// FIXME: With C++20 we can use std::is_sorted with an appropriate comparator +// There should be at least one protocol we're willing to speak. static_assert( - []() constexpr -> bool { - auto const len = - std::distance(std::begin(kSupportedProtocolList), std::end(kSupportedProtocolList)); + !std::ranges::empty(kSupportedProtocolList), + "There must be at least one supported protocol."); - // There should be at least one protocol we're willing to speak. - if (len == 0) - return false; - - // A list with only one entry is, by definition, sorted so we don't - // need to check it. - if (len != 1) - { - for (auto i = 0; i != len - 1; ++i) - { - if (kSupportedProtocolList[i] >= kSupportedProtocolList[i + 1]) - return false; - } - } - - return true; - }(), +// Searching for an adjacent pair where the first element is not less than the +// second one proves the list is sorted in strictly ascending order, which in +// turn means it holds no duplicates. +static_assert( + std::ranges::adjacent_find(kSupportedProtocolList, std::ranges::greater_equal{}) == + std::ranges::end(kSupportedProtocolList), "The list of supported protocols isn't properly sorted."); std::string diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index b561ce6d38..32a084a833 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -64,7 +64,6 @@ ServerDefinitions::translate(std::string const& inp) return out; }; - // TODO: use string::contains with C++23 auto contains = [&](std::string_view s) -> bool { return inp.contains(s); }; if (contains("UINT")) From 2967f1f0ccac0e573ffc0a3ff6c8eaca928505c3 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:06:29 -0400 Subject: [PATCH 51/68] chore: Remove unreferenced legacy documents (#7989) --- docs/NodeStoreRefactoringCaseStudy.pdf | Bin 393922 -> 0 bytes docs/sample_chart.doc | 24 ------------------------ 2 files changed, 24 deletions(-) delete mode 100644 docs/NodeStoreRefactoringCaseStudy.pdf delete mode 100644 docs/sample_chart.doc diff --git a/docs/NodeStoreRefactoringCaseStudy.pdf b/docs/NodeStoreRefactoringCaseStudy.pdf deleted file mode 100644 index 6cde8a2eedd968662ee2b3c49598705be4321628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393922 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�MI6-|wp1>+Wf*QHH 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

CH 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

}ku;=$~rH0d$c}Tu=i0qPihxjlNJ(Gm%B@!ckLH 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%0DG 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∾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�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 diff --git a/docs/sample_chart.doc b/docs/sample_chart.doc deleted file mode 100644 index 631c0554b2..0000000000 --- a/docs/sample_chart.doc +++ /dev/null @@ -1,24 +0,0 @@ -/*! - \page somestatechart Example state diagram - - \startuml SomeState "my state diagram" - scale 600 width - - [*] -> State1 - State1 --> State2 : Succeeded - State1 --> [*] : Aborted - State2 --> State3 : Succeeded - State2 --> [*] : Aborted - state State3 { - state "Accumulate Enough Data\nLong State Name" as long1 - long1 : Just a test - [*] --> long1 - long1 --> long1 : New Data - long1 --> ProcessData : Enough Data - } - State3 --> State3 : Failed - State3 --> [*] : Succeeded / Save Result - State3 --> [*] : Aborted - - \enduml -*/ From 6580b200db70bc175526868b1b632fa19220afc1 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:10:18 -0400 Subject: [PATCH 52/68] refactor: Replace boost::lexical_cast with existing alternatives (#7991) --- include/xrpl/beast/unit_test/reporter.h | 3 +-- include/xrpl/beast/unit_test/suite.h | 3 +-- src/test/unit_test/multi_runner.cpp | 3 +-- .../rpc/handlers/account/AccountChannels.cpp | 16 +++++----------- src/xrpld/rpc/handlers/account/AccountLines.cpp | 16 +++++----------- src/xrpld/rpc/handlers/account/AccountOffers.cpp | 16 +++++----------- 6 files changed, 18 insertions(+), 39 deletions(-) diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index 0fe77a7862..cbd1c7e70d 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -188,7 +187,7 @@ Reporter::fmtdur(clock_type::duration const& d) using namespace std::chrono; auto const ms = duration_cast(d); if (ms < seconds{1}) - return boost::lexical_cast(ms.count()) + "ms"; + return std::to_string(ms.count()) + "ms"; std::stringstream ss; ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s"; return ss.str(); diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index e24904a87b..a727e3fc77 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -30,7 +29,7 @@ makeReason(String const& reason, char const* file, int line) namespace fs = boost::filesystem; s.append(fs::path{file}.filename().string()); s.append("("); - s.append(boost::lexical_cast(line)); + s.append(std::to_string(line)); s.append(")"); return s; } diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp index 71208313a4..918fc7c89f 100644 --- a/src/test/unit_test/multi_runner.cpp +++ b/src/test/unit_test/multi_runner.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -36,7 +35,7 @@ fmtdur(typename clock_type::duration const& d) using namespace std::chrono; auto const ms = duration_cast(d); if (ms < seconds{1}) - return boost::lexical_cast(ms.count()) + "ms"; + return std::to_string(ms.count()) + "ms"; std::stringstream ss; ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s"; return ss.str(); diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp b/src/xrpld/rpc/handlers/account/AccountChannels.cpp index d50bf1cf07..f2da1e31ee 100644 --- a/src/xrpld/rpc/handlers/account/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -22,9 +23,6 @@ #include #include -#include -#include - #include #include #include @@ -129,7 +127,7 @@ doAccountChannels(rpc::JsonContext& context) return rpc::expectedFieldError(jss::marker, "string"); // Marker is composed of a comma separated index and start hint. The - // former will be read as hex, and the latter using boost lexical cast. + // former will be read as hex, and the latter as a decimal integer. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) @@ -141,14 +139,10 @@ doAccountChannels(rpc::JsonContext& context) if (!std::getline(marker, value, ',')) return rpcError(RpcInvalidParams); - try - { - startHint = boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { + auto const hint = toUInt64(value); + if (!hint.has_value()) return rpcError(RpcInvalidParams); - } + startHint = *hint; // We then must check if the object pointed to by the marker is actually // owned by the account in the request. diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp index f134c8af92..4a6d22d5d8 100644 --- a/src/xrpld/rpc/handlers/account/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -22,9 +23,6 @@ #include #include -#include -#include - #include #include #include @@ -153,7 +151,7 @@ doAccountLines(rpc::JsonContext& context) return rpc::expectedFieldError(jss::marker, "string"); // Marker is composed of a comma separated index and start hint. The - // former will be read as hex, and the latter using boost lexical cast. + // former will be read as hex, and the latter as a decimal integer. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) @@ -165,14 +163,10 @@ doAccountLines(rpc::JsonContext& context) if (!std::getline(marker, value, ',')) return rpcError(RpcInvalidParams); - try - { - startHint = boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { + auto const hint = toUInt64(value); + if (!hint.has_value()) return rpcError(RpcInvalidParams); - } + startHint = *hint; // We then must check if the object pointed to by the marker is actually // owned by the account in the request. diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp b/src/xrpld/rpc/handlers/account/AccountOffers.cpp index 1467b14b48..a7933f65a7 100644 --- a/src/xrpld/rpc/handlers/account/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -20,9 +21,6 @@ #include #include -#include -#include - #include #include #include @@ -97,7 +95,7 @@ doAccountOffers(rpc::JsonContext& context) return rpc::expectedFieldError(jss::marker, "string"); // Marker is composed of a comma separated index and start hint. The - // former will be read as hex, and the latter using boost lexical cast. + // former will be read as hex, and the latter as a decimal integer. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) @@ -109,14 +107,10 @@ doAccountOffers(rpc::JsonContext& context) if (!std::getline(marker, value, ',')) return rpc::invalidFieldError(jss::marker); - try - { - startHint = boost::lexical_cast(value); - } - catch (boost::bad_lexical_cast&) - { + auto const hint = toUInt64(value); + if (!hint.has_value()) return rpc::invalidFieldError(jss::marker); - } + startHint = *hint; // We then must check if the object pointed to by the marker is actually // owned by the account in the request. From 4f8819565a8b42e536c5b6d92d299e1c21cd70af Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:18:22 -0400 Subject: [PATCH 53/68] fix: Assorted cleanup fixes (#7988) --- include/xrpl/basics/Number.h | 2 +- include/xrpl/protocol/AMMCore.h | 2 +- include/xrpl/protocol/TxFlags.h | 3 +- include/xrpl/protocol/detail/sfields.macro | 50 ++--- include/xrpl/tx/invariants/InvariantCheck.h | 4 +- src/libxrpl/protocol/InnerObjectFormats.cpp | 6 +- src/test/app/lending/LoanBroker_test.cpp | 2 +- src/test/protocol/Hooks_test.cpp | 189 ------------------ .../rpc/handlers/account/AccountInfo.cpp | 24 +-- 9 files changed, 33 insertions(+), 249 deletions(-) delete mode 100644 src/test/protocol/Hooks_test.cpp diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index f90800c715..ec75724b5d 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -304,7 +304,7 @@ concept Integral64 = std::is_same_v || std::is_same_v // IWYU pragma: keep - -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace xrpl { - -class Hooks_test : public beast::unit_test::Suite -{ - /** - * This unit test was requested here: - * https://github.com/XRPLF/rippled/pull/4089#issuecomment-1050274539 - * These are tests that exercise facilities that are reserved for when Hooks - * is merged in the future. - **/ - - void - testHookFields() - { - testcase("Test Hooks fields"); - - using namespace test::jtx; - - std::vector> const fieldsToTest = { - sfHookResult, - sfHookStateChangeCount, - sfHookEmitCount, - sfHookExecutionIndex, - sfHookApiVersion, - sfHookStateCount, - sfEmitGeneration, - sfHookOn, - sfHookInstructionCount, - sfEmitBurden, - sfHookReturnCode, - sfReferenceCount, - sfEmitParentTxnID, - sfEmitNonce, - sfEmitHookHash, - sfHookStateKey, - sfHookHash, - sfHookNamespace, - sfHookSetTxnID, - sfHookStateData, - sfHookReturnString, - sfHookParameterName, - sfHookParameterValue, - sfEmitCallback, - sfHookAccount, - sfEmittedTxn, - sfHook, - sfHookDefinition, - sfHookParameter, - sfHookGrant, - sfEmitDetails, - sfHookExecutions, - sfHookExecution, - sfHookParameters, - sfHooks, - sfHookGrants}; - - for (auto const& rf : fieldsToTest) - { - SField const& f = rf.get(); - - STObject dummy{sfGeneric}; - - BEAST_EXPECT(!dummy.isFieldPresent(f)); - - switch (f.fieldType) - { - case STI_UINT8: { - dummy.setFieldU8(f, 0); - BEAST_EXPECT(dummy.getFieldU8(f) == 0); - - dummy.setFieldU8(f, 255); - BEAST_EXPECT(dummy.getFieldU8(f) == 255); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT16: { - dummy.setFieldU16(f, 0); - BEAST_EXPECT(dummy.getFieldU16(f) == 0); - - dummy.setFieldU16(f, 0xFFFFU); - BEAST_EXPECT(dummy.getFieldU16(f) == 0xFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT32: { - dummy.setFieldU32(f, 0); - BEAST_EXPECT(dummy.getFieldU32(f) == 0); - - dummy.setFieldU32(f, 0xFFFFFFFFU); - BEAST_EXPECT(dummy.getFieldU32(f) == 0xFFFFFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT64: { - dummy.setFieldU64(f, 0); - BEAST_EXPECT(dummy.getFieldU64(f) == 0); - - dummy.setFieldU64(f, 0xFFFFFFFFFFFFFFFFU); - BEAST_EXPECT(dummy.getFieldU64(f) == 0xFFFFFFFFFFFFFFFFU); - - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_UINT256: { - uint256 const u = uint256::fromVoid( - "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBE" - "EFDEADBEEF"); - dummy.setFieldH256(f, u); - BEAST_EXPECT(dummy.getFieldH256(f) == u); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_VL: { - std::vector const v{1, 2, 3}; - dummy.setFieldVL(f, v); - BEAST_EXPECT(dummy.getFieldVL(f) == v); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_ACCOUNT: { - // NOLINTBEGIN(bugprone-unchecked-optional-access) - AccountID const id = - *parseBase58("rwfSjJNK2YQuN64bSWn7T2eY9FJAyAPYJT"); - // NOLINTEND(bugprone-unchecked-optional-access) - dummy.setAccountID(f, id); - BEAST_EXPECT(dummy.getAccountID(f) == id); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_OBJECT: { - dummy.emplaceBack(STObject{f}); - BEAST_EXPECT(dummy.getField(f).getFName() == f); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - case STI_ARRAY: { - STArray dummy2{f, 2}; - dummy2.pushBack(STObject{sfGeneric}); - dummy2.pushBack(STObject{sfGeneric}); - dummy.setFieldArray(f, dummy2); - BEAST_EXPECT(dummy.getFieldArray(f) == dummy2); - BEAST_EXPECT(dummy.isFieldPresent(f)); - break; - } - - default: - BEAST_EXPECT(false); - } - } - } - -public: - void - run() override - { - using namespace test::jtx; - testHookFields(); - } -}; - -BEAST_DEFINE_TESTSUITE(Hooks, protocol, xrpl); - -} // namespace xrpl diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index d4232cf451..f131af01e5 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -51,22 +51,16 @@ void injectSLE(json::Value& jv, SLE const& sle) { jv = sle.getJson(JsonOptions::Values::None); - if (sle.getType() == ltACCOUNT_ROOT) + XRPL_ASSERT(sle.getType() == ltACCOUNT_ROOT, "xrpl::injectSLE : sle is account root"); + if (sle.isFieldPresent(sfEmailHash)) { - if (sle.isFieldPresent(sfEmailHash)) - { - auto const& hash = sle.getFieldH128(sfEmailHash); - Blob const b(hash.begin(), hash.end()); - std::string md5 = strHex(makeSlice(b)); - boost::to_lower(md5); - // VFALCO TODO Give a name to this constant and move it - // to a more visible location. - jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); - } - } - else - { - jv[jss::Invalid] = true; + auto const& hash = sle.getFieldH128(sfEmailHash); + Blob const b(hash.begin(), hash.end()); + std::string md5 = strHex(makeSlice(b)); + boost::to_lower(md5); + // VFALCO TODO Give a name to this constant and move it + // to a more visible location. + jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); } } From 07aa97fda4aae2f999708a8a39f3a7d62c07221e Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:22:40 -0400 Subject: [PATCH 54/68] test: Use std::string::starts_with/ends_with instead of Boost (#7992) --- src/test/core/SociDB_test.cpp | 3 +-- src/test/jtx/TrustedPublisherServer.h | 33 ++++++++++++--------------- src/test/rpc/NoRippleCheck_test.cpp | 10 ++++---- src/test/server/ServerStatus_test.cpp | 13 +++++------ 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp index 373ec66cd1..7a57641b64 100644 --- a/src/test/core/SociDB_test.cpp +++ b/src/test/core/SociDB_test.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include #include // IWYU pragma: keep @@ -108,7 +107,7 @@ public: for (auto const& i : d) { DBConfig const sc(c, i.first); - BEAST_EXPECT(boost::ends_with(sc.connectionString(), i.first + i.second)); + BEAST_EXPECT(sc.connectionString().ends_with(i.first + i.second)); } } void diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h index f5ee8aac3a..941af374ef 100644 --- a/src/test/jtx/TrustedPublisherServer.h +++ b/src/test/jtx/TrustedPublisherServer.h @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -549,7 +548,7 @@ private: res.keep_alive(req.keep_alive()); bool prepare = true; - if (boost::starts_with(path, "/validators2")) + if (path.starts_with("/validators2")) { res.result(http::status::ok); res.insert("Content-Type", "application/json"); @@ -565,7 +564,7 @@ private: { int refresh = 5; static constexpr char const* kRefreshPrefix = "/validators2/refresh/"; - if (boost::starts_with(path, kRefreshPrefix)) + if (path.starts_with(kRefreshPrefix)) { refresh = boost::lexical_cast( path.substr(strlen(kRefreshPrefix))); @@ -573,7 +572,7 @@ private: res.body() = getList2_(refresh); } } - else if (boost::starts_with(path, "/validators")) + else if (path.starts_with("/validators")) { res.result(http::status::ok); res.insert("Content-Type", "application/json"); @@ -589,7 +588,7 @@ private: { int refresh = 5; static constexpr char const* kRefreshPrefix = "/validators/refresh/"; - if (boost::starts_with(path, kRefreshPrefix)) + if (path.starts_with(kRefreshPrefix)) { refresh = boost::lexical_cast( path.substr(strlen(kRefreshPrefix))); @@ -597,13 +596,13 @@ private: res.body() = getList_(refresh); } } - else if (boost::starts_with(path, "/textfile")) + else if (path.starts_with("/textfile")) { prepare = false; res.result(http::status::ok); res.insert("Content-Type", "text/example"); // if huge was requested, lie about content length - std::uint64_t const cl = boost::starts_with(path, "/textfile/huge") + std::uint64_t const cl = path.starts_with("/textfile/huge") ? std::numeric_limits::max() : 1024; res.content_length(cl); @@ -617,41 +616,39 @@ private: } } } - else if (boost::starts_with(path, "/sleep/")) + else if (path.starts_with("/sleep/")) { auto const sleepSec = boost::lexical_cast(path.substr(7)); std::this_thread::sleep_for(std::chrono::seconds(sleepSec)); } - else if (boost::starts_with(path, "/redirect")) + else if (path.starts_with("/redirect")) { - if (boost::ends_with(path, "/301")) + if (path.ends_with("/301")) { res.result(http::status::moved_permanently); } - else if (boost::ends_with(path, "/302")) + else if (path.ends_with("/302")) { res.result(http::status::found); } - else if (boost::ends_with(path, "/307")) + else if (path.ends_with("/307")) { res.result(http::status::temporary_redirect); } - else if (boost::ends_with(path, "/308")) + else if (path.ends_with("/308")) { res.result(http::status::permanent_redirect); } std::stringstream location; - if (boost::starts_with(path, "/redirect_to/")) + if (path.starts_with("/redirect_to/")) { location << path.substr(13); } - else if (!boost::starts_with(path, "/redirect_nolo")) + else if (!path.starts_with("/redirect_nolo")) { location << (ssl ? "https://" : "http://") << localEndpoint() - << (boost::starts_with(path, "/redirect_forever/") - ? path - : "/validators"); + << (path.starts_with("/redirect_forever/") ? path : "/validators"); } if (!location.str().empty()) res.insert("Location", location.str()); diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 6e30f944c7..8e719e6407 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -27,8 +27,6 @@ #include #include -#include - #include #include @@ -203,13 +201,13 @@ class NoRippleCheck_test : public beast::unit_test::Suite if (user) { - BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You appear to have set")); - BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should probably set")); + BEAST_EXPECT(pa[0u].asString().starts_with("You appear to have set")); + BEAST_EXPECT(pa[1u].asString().starts_with("You should probably set")); } else { - BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You should immediately set")); - BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should clear")); + BEAST_EXPECT(pa[0u].asString().starts_with("You should immediately set")); + BEAST_EXPECT(pa[1u].asString().starts_with("You should clear")); } } else diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp index 5adf6a08f5..f1989ed171 100644 --- a/src/test/server/ServerStatus_test.cpp +++ b/src/test/server/ServerStatus_test.cpp @@ -56,8 +56,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En static auto makeConfig(std::string const& proto, bool admin = true, bool credentials = false) { - auto const sectionName = - boost::starts_with(proto, "h") ? Sections::kPortRpc : Sections::kPortWs; + auto const sectionName = proto.starts_with("h") ? Sections::kPortRpc : Sections::kPortWs; auto p = jtx::envconfig(); p->overwrite(sectionName, Keys::kProtocol, proto); @@ -71,9 +70,9 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En } p->overwrite( - boost::starts_with(proto, "h") ? Sections::kPortWs : Sections::kPortRpc, + proto.starts_with("h") ? Sections::kPortWs : Sections::kPortRpc, Keys::kProtocol, - boost::starts_with(proto, "h") ? "ws" : "http"); + proto.starts_with("h") ? "ws" : "http"); if (proto == "https") { @@ -261,7 +260,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En } } - if (boost::starts_with(proto, "h")) + if (proto.starts_with("h")) { auto jrc = makeJSONRPCClient(env.app().config()); jrr = jrc->invoke("ledger_accept", jp); @@ -289,7 +288,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En Env env{*this, makeConfig(proto, admin, credentials)}; json::Value jrr; - auto const protoWs = boost::starts_with(proto, "w"); + auto const protoWs = proto.starts_with("w"); // the set of checks we do are different depending // on how the admin config options are set @@ -485,7 +484,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En boost::beast::http::response resp; boost::system::error_code ec; - if (boost::starts_with(clientProtocol, "h")) + if (clientProtocol.starts_with("h")) { doHTTPRequest(env, yield, clientProtocol == "https", resp, ec); BEAST_EXPECT(ec); From a0e1e578a0a3977f4466d0ff274613088022cc29 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 10 Aug 2026 13:23:02 -0400 Subject: [PATCH 55/68] refactor: Remove operator!= overloads that C++20 synthesizes (#7994) --- include/xrpl/basics/Buffer.h | 6 --- include/xrpl/basics/IntrusivePointer.h | 3 -- include/xrpl/basics/IntrusivePointer.ipp | 7 --- include/xrpl/basics/Number.h | 6 --- include/xrpl/basics/SHAMapHash.h | 6 --- include/xrpl/basics/Slice.h | 6 --- .../xrpl/basics/partitioned_unordered_map.h | 12 ----- .../container/detail/aged_ordered_container.h | 19 -------- .../detail/aged_unordered_container.h | 22 ---------- include/xrpl/beast/core/List.h | 7 --- include/xrpl/beast/net/IPEndpoint.h | 6 --- include/xrpl/beast/rfc2616.h | 6 --- include/xrpl/conditions/Condition.h | 6 --- include/xrpl/conditions/Fulfillment.h | 6 --- include/xrpl/json/json_value.h | 30 ------------- include/xrpl/ledger/BookDirs.h | 6 --- include/xrpl/ledger/CanonicalTXSet.h | 6 --- include/xrpl/ledger/Dir.h | 6 --- include/xrpl/ledger/detail/ReadViewFwdRange.h | 3 -- .../xrpl/ledger/detail/ReadViewFwdRange.ipp | 7 --- include/xrpl/protocol/Quality.h | 13 ------ include/xrpl/protocol/Rules.h | 3 -- include/xrpl/protocol/STAmount.h | 6 --- include/xrpl/protocol/STArray.h | 9 ---- include/xrpl/protocol/STBase.h | 2 - include/xrpl/protocol/STCurrency.h | 6 --- include/xrpl/protocol/STObject.h | 38 ---------------- include/xrpl/protocol/STPathSet.h | 9 ---- include/xrpl/protocol/SeqProxy.h | 6 --- include/xrpl/protocol/Serializer.h | 10 ----- include/xrpl/protocol/Units.h | 7 --- include/xrpl/protocol/detail/STVar.h | 6 --- include/xrpl/server/Manifest.h | 6 --- include/xrpl/shamap/SHAMap.h | 6 --- include/xrpl/shamap/SHAMapNodeID.h | 44 +++++++------------ include/xrpl/tx/paths/detail/Steps.h | 13 ------ src/libxrpl/protocol/Rules.cpp | 6 --- src/libxrpl/protocol/STBase.cpp | 6 --- src/test/jtx/amount.h | 6 --- 39 files changed, 16 insertions(+), 362 deletions(-) diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index 05af6c409a..705a5ef51a 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -226,10 +226,4 @@ operator==(Buffer const& lhs, Buffer const& rhs) noexcept return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0; } -inline bool -operator!=(Buffer const& lhs, Buffer const& rhs) noexcept -{ - return !(lhs == rhs); -} - } // namespace xrpl diff --git a/include/xrpl/basics/IntrusivePointer.h b/include/xrpl/basics/IntrusivePointer.h index 59853ad4d0..b978016860 100644 --- a/include/xrpl/basics/IntrusivePointer.h +++ b/include/xrpl/basics/IntrusivePointer.h @@ -96,9 +96,6 @@ public: SharedIntrusive& operator=(SharedIntrusive const& rhs); - bool - operator!=(std::nullptr_t) const; - bool operator==(std::nullptr_t) const; diff --git a/include/xrpl/basics/IntrusivePointer.ipp b/include/xrpl/basics/IntrusivePointer.ipp index 67d43b05d6..6c2a71f7eb 100644 --- a/include/xrpl/basics/IntrusivePointer.ipp +++ b/include/xrpl/basics/IntrusivePointer.ipp @@ -111,13 +111,6 @@ SharedIntrusive::operator=(SharedIntrusive&& rhs) return *this; } -template -bool -SharedIntrusive::operator!=(std::nullptr_t) const -{ - return this->get() != nullptr; -} - template bool SharedIntrusive::operator==(std::nullptr_t) const diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index ec75724b5d..f6ce0b300d 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -449,12 +449,6 @@ public: x.exponent_ == y.exponent_; } - friend constexpr bool - operator!=(Number const& x, Number const& y) noexcept - { - return !(x == y); - } - friend constexpr bool operator<(Number const& l, Number const& r) noexcept { diff --git a/include/xrpl/basics/SHAMapHash.h b/include/xrpl/basics/SHAMapHash.h index 3c3d525022..1902a3b2ec 100644 --- a/include/xrpl/basics/SHAMapHash.h +++ b/include/xrpl/basics/SHAMapHash.h @@ -85,12 +85,6 @@ public: } }; -inline bool -operator!=(SHAMapHash const& x, SHAMapHash const& y) -{ - return !(x == y); -} - template <> inline std::size_t extract(SHAMapHash const& key) diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 75c9b8c7bd..92b777ab98 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -208,12 +208,6 @@ operator==(Slice const& lhs, Slice const& rhs) noexcept return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0; } -inline bool -operator!=(Slice const& lhs, Slice const& rhs) noexcept -{ - return !(lhs == rhs); -} - inline bool operator<(Slice const& lhs, Slice const& rhs) noexcept { diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h index e78043e252..c6b0107b93 100644 --- a/include/xrpl/basics/partitioned_unordered_map.h +++ b/include/xrpl/basics/partitioned_unordered_map.h @@ -116,12 +116,6 @@ public: { return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit; } - - friend bool - operator!=(Iterator const& lhs, Iterator const& rhs) - { - return !(lhs == rhs); - } }; struct ConstIterator @@ -189,12 +183,6 @@ public: { return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit; } - - friend bool - operator!=(ConstIterator const& lhs, ConstIterator const& rhs) - { - return !(lhs == rhs); - } }; private: diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 5b60ef7e6d..9dd83d466b 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -1038,25 +1038,6 @@ public: Compare, OtherAllocator> const& other) const; - template < - bool OtherIsMulti, - bool OtherIsMap, - class OtherT, - class OtherDuration, - class OtherAllocator> - bool - operator!=(AgedOrderedContainer< - OtherIsMulti, - OtherIsMap, - Key, - OtherT, - OtherDuration, - Compare, - OtherAllocator> const& other) const - { - return !(this->operator==(other)); - } - template < bool OtherIsMulti, bool OtherIsMap, diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index c4287b1ca1..ea271feed0 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -1340,28 +1340,6 @@ public: OtherAllocator> const& other) const requires MaybeMulti; - template < - bool OtherIsMulti, - bool OtherIsMap, - class OtherKey, - class OtherT, - class OtherDuration, - class OtherHash, - class OtherAllocator> - bool - operator!=(AgedUnorderedContainer< - OtherIsMulti, - OtherIsMap, - OtherKey, - OtherT, - OtherDuration, - OtherHash, - KeyEqual, - OtherAllocator> const& other) const - { - return !(this->operator==(other)); - } - private: bool wouldExceed(size_type additional) const diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h index b9b6829d31..076ac3028b 100644 --- a/include/xrpl/beast/core/List.h +++ b/include/xrpl/beast/core/List.h @@ -82,13 +82,6 @@ public: return node_ == other.node_; } - template - bool - operator!=(ListIterator const& other) const noexcept - { - return !((*this) == other); - } - reference operator*() const noexcept { diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index d4d3b2ab12..a5fb5b4318 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -110,12 +110,6 @@ public: operator==(Endpoint const& lhs, Endpoint const& rhs); friend bool operator<(Endpoint const& lhs, Endpoint const& rhs); - - friend bool - operator!=(Endpoint const& lhs, Endpoint const& rhs) - { - return !(lhs == rhs); - } friend bool operator>(Endpoint const& lhs, Endpoint const& rhs) { diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index 1986568553..0e061845fb 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -229,12 +229,6 @@ public: return other.it_ == it_ && other.end_ == end_ && other.value_.size() == value_.size(); } - bool - operator!=(ListIterator const& other) const - { - return !(*this == other); - } - reference operator*() const { diff --git a/include/xrpl/conditions/Condition.h b/include/xrpl/conditions/Condition.h index 365a41a087..04e571a028 100644 --- a/include/xrpl/conditions/Condition.h +++ b/include/xrpl/conditions/Condition.h @@ -92,10 +92,4 @@ operator==(Condition const& lhs, Condition const& rhs) lhs.fingerprint == rhs.fingerprint; } -inline bool -operator!=(Condition const& lhs, Condition const& rhs) -{ - return !(lhs == rhs); -} - } // namespace xrpl::cryptoconditions diff --git a/include/xrpl/conditions/Fulfillment.h b/include/xrpl/conditions/Fulfillment.h index 11f3165a58..6fd75aa5a3 100644 --- a/include/xrpl/conditions/Fulfillment.h +++ b/include/xrpl/conditions/Fulfillment.h @@ -93,12 +93,6 @@ operator==(Fulfillment const& lhs, Fulfillment const& rhs) lhs.fingerprint() == rhs.fingerprint(); } -inline bool -operator!=(Fulfillment const& lhs, Fulfillment const& rhs) -{ - return !(lhs == rhs); -} - /** * Determine whether the given fulfillment and condition match */ diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 260917face..be126d8b8e 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -72,36 +72,18 @@ operator==(StaticString x, StaticString y) return strcmp(x.cStr(), y.cStr()) == 0; } -inline bool -operator!=(StaticString x, StaticString y) -{ - return !(x == y); -} - inline bool operator==(std::string const& x, StaticString y) { return strcmp(x.c_str(), y.cStr()) == 0; } -inline bool -operator!=(std::string const& x, StaticString y) -{ - return !(x == y); -} - inline bool operator==(StaticString x, std::string const& y) { return y == x; } -inline bool -operator!=(StaticString x, std::string const& y) -{ - return !(y == x); -} - /** * @brief Represents a JSON value. * @@ -489,12 +471,6 @@ toJson(xrpl::Number const& number) bool operator==(Value const&, Value const&); -inline bool -operator!=(Value const& x, Value const& y) -{ - return !(x == y); -} - bool operator<(Value const&, Value const&); @@ -562,12 +538,6 @@ public: return isEqual(other); } - bool - operator!=(SelfType const& other) const - { - return !isEqual(other); - } - /** * Return either the index or the member name of the referenced value as a * Value. diff --git a/include/xrpl/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h index dc4361136d..b9aa87ae52 100644 --- a/include/xrpl/ledger/BookDirs.h +++ b/include/xrpl/ledger/BookDirs.h @@ -49,12 +49,6 @@ public: bool operator==(const_iterator const& other) const; - bool - operator!=(const_iterator const& other) const - { - return !(*this == other); - } - reference operator*() const; diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h index 11aadf4e92..3fe17d6eef 100644 --- a/include/xrpl/ledger/CanonicalTXSet.h +++ b/include/xrpl/ledger/CanonicalTXSet.h @@ -59,12 +59,6 @@ private: return lhs.txId_ == rhs.txId_; } - friend bool - operator!=(Key const& lhs, Key const& rhs) - { - return !(lhs == rhs); - } - [[nodiscard]] uint256 const& getAccount() const { diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h index 233719cdeb..eb70b3b6a3 100644 --- a/include/xrpl/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -59,12 +59,6 @@ public: bool operator==(ConstIterator const& other) const; - bool - operator!=(ConstIterator const& other) const - { - return !(*this == other); - } - reference operator*() const; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h index 19ac0698c2..bfa2527bbd 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.h +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h @@ -85,9 +85,6 @@ public: bool operator==(Iterator const& other) const; - bool - operator!=(Iterator const& other) const; - // Can throw reference operator*() const; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp index c7cbc5ee61..2003280ea6 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp @@ -64,13 +64,6 @@ ReadViewFwdRange::Iterator::operator==(Iterator const& other) const return impl_ == other.impl_; } -template -bool -ReadViewFwdRange::Iterator::operator!=(Iterator const& other) const -{ - return !(*this == other); -} - template auto ReadViewFwdRange::Iterator::operator*() const -> reference diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index 3475efa977..d0d0f10cd2 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -75,13 +75,6 @@ operator==(TAmounts const& lhs, TAmounts const& rhs) noexcept return lhs.in == rhs.in && lhs.out == rhs.out; } -template -bool -operator!=(TAmounts const& lhs, TAmounts const& rhs) noexcept -{ - return !(lhs == rhs); -} - //------------------------------------------------------------------------------ // XRPL specific constant used for parsing qualities and other things @@ -271,12 +264,6 @@ public: return lhs.value_ == rhs.value_; } - friend bool - operator!=(Quality const& lhs, Quality const& rhs) noexcept - { - return !(lhs == rhs); - } - friend std::ostream& operator<<(std::ostream& os, Quality const& quality) { diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 2c2136b6e8..d67e0d8654 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -98,9 +98,6 @@ public: */ bool operator==(Rules const&) const; - - bool - operator!=(Rules const& other) const; }; std::optional const& diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index cc80481582..4b2f1cc9fb 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -642,12 +642,6 @@ operator==(STAmount const& lhs, STAmount const& rhs); bool operator<(STAmount const& lhs, STAmount const& rhs); -inline bool -operator!=(STAmount const& lhs, STAmount const& rhs) -{ - return !(lhs == rhs); -} - inline bool operator>(STAmount const& lhs, STAmount const& rhs) { diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index 573bb6dad8..e88563fb1a 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -133,9 +133,6 @@ public: bool operator==(STArray const& s) const; - bool - operator!=(STArray const& s) const; - iterator erase(iterator pos); @@ -283,12 +280,6 @@ STArray::operator==(STArray const& s) const return v_ == s.v_; } -inline bool -STArray::operator!=(STArray const& s) const -{ - return v_ != s.v_; -} - inline STArray::iterator STArray::erase(iterator pos) { diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index acc5500a57..a8bda8f614 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -140,8 +140,6 @@ public: bool operator==(STBase const& t) const; - bool - operator!=(STBase const& t) const; template D& diff --git a/include/xrpl/protocol/STCurrency.h b/include/xrpl/protocol/STCurrency.h index 18642b20cf..933abaedb8 100644 --- a/include/xrpl/protocol/STCurrency.h +++ b/include/xrpl/protocol/STCurrency.h @@ -93,12 +93,6 @@ operator==(STCurrency const& lhs, STCurrency const& rhs) return lhs.currency() == rhs.currency(); } -inline bool -operator!=(STCurrency const& lhs, STCurrency const& rhs) -{ - return !operator==(lhs, rhs); -} - inline bool operator<(STCurrency const& lhs, STCurrency const& rhs) { diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index c7fc4fa796..dcbd08170e 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -432,8 +432,6 @@ public: bool operator==(STObject const& o) const; - bool - operator!=(STObject const& o) const; class FieldErr; @@ -667,36 +665,6 @@ public: return !lhs.engaged() || *lhs == *rhs; } - friend bool - operator!=(OptionalProxy const& lhs, std::nullopt_t) noexcept - { - return !(lhs == std::nullopt); - } - - friend bool - operator!=(std::nullopt_t, OptionalProxy const& rhs) noexcept - { - return !(rhs == std::nullopt); - } - - friend bool - operator!=(OptionalProxy const& lhs, optional_type const& rhs) noexcept - { - return !(lhs == rhs); - } - - friend bool - operator!=(optional_type const& lhs, OptionalProxy const& rhs) noexcept - { - return !(lhs == rhs); - } - - friend bool - operator!=(OptionalProxy const& lhs, OptionalProxy const& rhs) noexcept - { - return !(lhs == rhs); - } - // Emulate std::optional::value_or [[nodiscard]] value_type valueOr(value_type val) const; @@ -1202,12 +1170,6 @@ STObject::setFieldH160(SField const& field, BaseUInt<160, Tag> const& v) } } -inline bool -STObject::operator!=(STObject const& o) const -{ - return !(*this == o); -} - template V STObject::getFieldByValue(SField const& field) const diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index d527e2479f..5768721111 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -115,9 +115,6 @@ public: bool operator==(STPathElement const& t) const; - bool - operator!=(STPathElement const& t) const; - private: static std::size_t getHash(STPathElement const& element); @@ -432,12 +429,6 @@ STPathElement::operator==(STPathElement const& t) const accountID_ == t.accountID_ && assetID_ == t.assetID_ && issuerID_ == t.issuerID_; } -inline bool -STPathElement::operator!=(STPathElement const& t) const -{ - return !operator==(t); -} - // ------------ STPath ------------ inline STPath::STPath(std::vector p) : path_(std::move(p)) diff --git a/include/xrpl/protocol/SeqProxy.h b/include/xrpl/protocol/SeqProxy.h index fa72914591..3686d123d6 100644 --- a/include/xrpl/protocol/SeqProxy.h +++ b/include/xrpl/protocol/SeqProxy.h @@ -123,12 +123,6 @@ public: return (lhs.value() == rhs.value()); } - friend constexpr bool - operator!=(SeqProxy lhs, SeqProxy rhs) - { - return !(lhs == rhs); - } - friend constexpr bool operator<(SeqProxy lhs, SeqProxy rhs) { diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index 73bd9c8289..c1ea5c16ba 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -265,20 +265,10 @@ public: return v == data_; } bool - operator!=(Blob const& v) const - { - return v != data_; - } - bool operator==(Serializer const& v) const { return v.data_ == data_; } - bool - operator!=(Serializer const& v) const - { - return v.data_ != data_; - } static int decodeLengthLength(int b1); diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 169ee2c543..94afd72f53 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -258,13 +258,6 @@ public: return value_ == other; } - template Other> - constexpr bool - operator!=(ValueUnit const& other) const - { - return !operator==(other); - } - constexpr bool operator<(ValueUnit const& other) const { diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 12026f3d09..56f868b665 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -152,10 +152,4 @@ operator==(STVar const& lhs, STVar const& rhs) return lhs.get().isEquivalent(rhs.get()); } -inline bool -operator!=(STVar const& lhs, STVar const& rhs) -{ - return !(lhs == rhs); -} - } // namespace xrpl::detail diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 786967b057..1b726f2c0c 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -306,12 +306,6 @@ operator==(Manifest const& lhs, Manifest const& rhs) lhs.serialized == rhs.serialized; } -inline bool -operator!=(Manifest const& lhs, Manifest const& rhs) -{ - return !(lhs == rhs); -} - struct ValidatorToken { std::string manifest; diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index e198c472fa..97ab2e9f7a 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -789,12 +789,6 @@ operator==(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y) return x.item_ == y.item_; } -inline bool -operator!=(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y) -{ - return !(x == y); -} - inline SHAMap::ConstIterator SHAMap::begin() const { diff --git a/include/xrpl/shamap/SHAMapNodeID.h b/include/xrpl/shamap/SHAMapNodeID.h index 6094892091..1189304aa7 100644 --- a/include/xrpl/shamap/SHAMapNodeID.h +++ b/include/xrpl/shamap/SHAMapNodeID.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -65,45 +66,32 @@ public: static SHAMapNodeID createID(int depth, uint256 const& key); - // FIXME-C++20: use spaceship and operator synthesis /** * Comparison operators + * + * <, >, <= and >= are synthesized from the spaceship. It is written out + * rather than defaulted because the ordering is by depth first, and the + * members are not declared in that order. */ - bool - operator<(SHAMapNodeID const& n) const + std::strong_ordering + operator<=>(SHAMapNodeID const& n) const { - return std::tie(depth_, id_) < std::tie(n.depth_, n.id_); - } - - bool - operator>(SHAMapNodeID const& n) const - { - return n < *this; - } - - bool - operator<=(SHAMapNodeID const& n) const - { - return !(n < *this); - } - - bool - operator>=(SHAMapNodeID const& n) const - { - return !(*this < n); + return std::tie(depth_, id_) <=> std::tie(n.depth_, n.id_); } + /** + * Equality, which the spaceship above does not provide. + * + * Only a *defaulted* operator<=> implicitly declares a defaulted + * operator==; the one above is user-provided, so == has to be written. + * It cannot be defaulted either, because a defaulted == would also compare + * the CountedObject base, which is not equality comparable. + */ bool operator==(SHAMapNodeID const& n) const { return (depth_ == n.depth_) && (id_ == n.id_); } - - bool - operator!=(SHAMapNodeID const& n) const - { - return !(*this == n); - } }; inline std::string diff --git a/include/xrpl/tx/paths/detail/Steps.h b/include/xrpl/tx/paths/detail/Steps.h index 8ee37c026c..1d68860adc 100644 --- a/include/xrpl/tx/paths/detail/Steps.h +++ b/include/xrpl/tx/paths/detail/Steps.h @@ -274,19 +274,6 @@ public: return lhs.equal(rhs); } - /** - * Return true if lhs != rhs. - * - * @param lhs Step to compare. - * @param rhs Step to compare. - * @return true if lhs != rhs. - */ - friend bool - operator!=(Step const& lhs, Step const& rhs) - { - return !(lhs == rhs); - } - /** * Streaming operator for a Step. */ diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 197139027a..cb71133d8f 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -193,12 +193,6 @@ Rules::operator==(Rules const& other) const return *impl_ == *other.impl_; } -bool -Rules::operator!=(Rules const& other) const -{ - return !(*this == other); -} - bool isFeatureEnabled(uint256 const& feature, bool resultIfNoRules) { diff --git a/src/libxrpl/protocol/STBase.cpp b/src/libxrpl/protocol/STBase.cpp index f029f10e75..1e56897e30 100644 --- a/src/libxrpl/protocol/STBase.cpp +++ b/src/libxrpl/protocol/STBase.cpp @@ -38,12 +38,6 @@ STBase::operator==(STBase const& t) const return (getSType() == t.getSType()) && isEquivalent(t); } -bool -STBase::operator!=(STBase const& t) const -{ - return (getSType() != t.getSType()) || !isEquivalent(t); -} - STBase* STBase::copy(std::size_t n, void* buf) const { diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h index 57a4502db9..94dd8aef9e 100644 --- a/src/test/jtx/amount.h +++ b/src/test/jtx/amount.h @@ -162,12 +162,6 @@ operator==(PrettyAmount const& lhs, PrettyAmount const& rhs) return lhs.value() == rhs.value(); } -inline bool -operator!=(PrettyAmount const& lhs, PrettyAmount const& rhs) -{ - return !operator==(lhs, rhs); -} - std::ostream& operator<<(std::ostream& os, PrettyAmount const& amount); From b19c3c64f24ae2e7f7e40b5ca0cbf5e94f44b803 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Mon, 10 Aug 2026 13:47:16 -0400 Subject: [PATCH 56/68] fix: Add zero keylet check in credential (#7971) --- .../xrpl/ledger/helpers/CredentialHelpers.h | 3 +- .../ledger/helpers/CredentialHelpers.cpp | 24 ++++++++++- .../tx/transactors/account/AccountDelete.cpp | 2 +- .../tx/transactors/escrow/EscrowFinish.cpp | 2 +- .../tx/transactors/payment/Payment.cpp | 2 +- .../payment_channel/PaymentChannelClaim.cpp | 2 +- .../transactors/token/ConfidentialMPTSend.cpp | 2 +- src/test/app/DepositAuth_test.cpp | 41 +++++++++++++++++++ 8 files changed, 71 insertions(+), 7 deletions(-) diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index 8e78a00923..8b1c819bf4 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j); // Amendment and parameters checks for sfCredentialIDs field NotTEC -checkFields(STTx const& tx, beast::Journal j); +checkFields(STTx const& tx, Rules const& rules, beast::Journal j); // Accessing the ledger to check if provided credentials are valid. Do not use // in doApply (only in preclaim) since it does not remove expired credentials. diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index 226ea100e9..5ba832957d 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -52,6 +53,9 @@ removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j) for (auto const& h : arr) { // Credentials already checked in preclaim. Look only for expired here. + if (view.rules().enabled(fixCleanup3_4_0) && h.isZero()) + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE + auto const k = keylet::credential(h); auto const sleCred = view.peek(k); @@ -124,7 +128,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } NotTEC -checkFields(STTx const& tx, beast::Journal j) +checkFields(STTx const& tx, Rules const& rules, beast::Journal j) { if (!tx.isFieldPresent(sfCredentialIDs)) return tesSUCCESS; @@ -137,6 +141,13 @@ checkFields(STTx const& tx, beast::Journal j) return temMALFORMED; } + if (rules.enabled(fixCleanup3_4_0) && + std::ranges::any_of(credentials, [](uint256 const& id) { return id.isZero(); })) + { + JLOG(j.trace()) << "Malformed transaction: zero credential ID."; + return temMALFORMED; + } + std::unordered_set duplicates; for (auto const& cred : credentials) { @@ -160,6 +171,14 @@ valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal auto const& credIDs(tx.getFieldV256(sfCredentialIDs)); for (auto const& h : credIDs) { + if (view.rules().enabled(fixCleanup3_4_0) && h.isZero()) + { + // LCOV_EXCL_START + JLOG(j.trace()) << "Zero credential ID."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } + auto const sleCred = view.read(keylet::credential(h)); if (!sleCred) { @@ -234,6 +253,9 @@ authorizedDepositPreauth(ReadView const& view, STVector256 const& credIDs, Accou lifeExtender.reserve(credIDs.size()); for (auto const& h : credIDs) { + if (view.rules().enabled(fixCleanup3_4_0) && h.isZero()) + return tefINTERNAL; // LCOV_EXCL_LINE + auto sleCred = view.read(keylet::credential(h)); if (!sleCred) // already checked in preclaim return tefINTERNAL; // LCOV_EXCL_LINE diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index ce027f4cad..0936fe26dc 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -50,7 +50,7 @@ AccountDelete::preflight(PreflightContext const& ctx) return temDST_IS_SRC; } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 5fc0aef853..32f4d9ec48 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -111,7 +111,7 @@ EscrowFinish::preflightSigValidated(PreflightContext const& ctx) } } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 17c96a1919..c8b00f0193 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -281,7 +281,7 @@ Payment::preflight(PreflightContext const& ctx) } } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index b8118bc49f..9143a675f6 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -87,7 +87,7 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx) return temBAD_SIGNATURE; } - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index d121ec2634..f4c7b98c41 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -82,7 +82,7 @@ ConfidentialMPTSend::preflight(PreflightContext const& ctx) if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount])) return temBAD_CIPHERTEXT; - if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err)) + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) return err; return tesSUCCESS; diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 881441e0f9..c987e603be 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -934,6 +934,46 @@ struct DepositPreauth_test : public beast::unit_test::Suite } } + void + testZeroCredentialID(FeatureBitset features) + { + testcase("Zero credential ID"); + + using namespace jtx; + + char const credType[] = "abcde"; + Account const issuer{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + Env env(*this, features); + + env.fund(XRP(5000), issuer, alice, bob); + env.close(); + + env(credentials::create(alice, issuer, credType)); + env.close(); + env(credentials::accept(alice, issuer, credType)); + env.close(); + + auto const jv = credentials::ledgerEntry(env, alice, issuer, credType); + std::string const credIdx = jv[jss::result][jss::index].asString(); + + std::string const zeroIdx(64, '0'); + + // post-fixCleanup3_4_0: a zero ID is rejected by checkFields in + // preflight; pre-fixCleanup3_4_0, it will trigger assertion, so it is not testable. + env(pay(alice, bob, XRP(100)), credentials::Ids({zeroIdx}), Ter(temMALFORMED)); + env.close(); + + env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, zeroIdx}), Ter(temMALFORMED)); + env.close(); + + // A valid credential succeeds + env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx})); + env.close(); + } + void testCredentialsCreation() { @@ -1446,6 +1486,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite testPayment(supported - featureCredentials); testPayment(supported); testCredentialsPayment(); + testZeroCredentialID(supported); testCredentialsCreation(); testExpiredCreds(); testSortingCredentials(); From 9c292fbe4fbd62aa8780842a95ddd9b5291093f5 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 10 Aug 2026 18:49:29 +0100 Subject: [PATCH 57/68] build: Install conan configuration/profiles inside Nix devshell (#7997) --- .envrc | 4 ++++ BUILD.md | 42 +++++++++++++++++------------------------- conan/init.sh | 21 +++++++++++++++++++++ conan/profiles/default | 3 --- docs/build/nix.md | 28 +++++++++++++++++++++++----- nix/devshell.nix | 22 +++++++++++++++++++++- 6 files changed, 86 insertions(+), 34 deletions(-) create mode 100755 conan/init.sh diff --git a/.envrc b/.envrc index cecf4b4767..ec38b75f5c 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,7 @@ watch_file nix/*.nix +# The dev shell derivation includes all of conan/ (see nix/devshell.nix), so any +# change in there has to invalidate direnv's cached environment. +watch_dir conan + use flake diff --git a/BUILD.md b/BUILD.md index ad4666b141..ae2e69bb97 100644 --- a/BUILD.md +++ b/BUILD.md @@ -53,33 +53,25 @@ releases](https://github.com/XRPLF/rippled/releases). ### Set Up Conan -Once your [development environment](./docs/build/environment.md) is ready, you -may need to set up your Conan profile. - -#### Profiles - -We recommend that you install our Conan profiles: +Once your [development environment](./docs/build/environment.md) is ready, set +Conan up for this repository: ```bash -conan config install conan/profiles/ -tf $(conan config home)/profiles/ +./conan/init.sh ``` -You can check your Conan profile by running: +That installs our [`global.conf`](./conan/global.conf), our Conan +[profiles](./conan/profiles), and the `xrplf` remote that hosts some of our +dependencies. It honours `CONAN_HOME` and never deletes an existing Conan home, +so it is safe to re-run — it only overwrites the files it manages. -```bash -conan profile show -``` +> [!TIP] +> In the [Nix development shell](./docs/build/nix.md#conan-configuration) this is +> already done for you: the script runs on entry. -If the default profile is not suitable for your environment, you can create a custom profile and pass it to Conan. -More information on customizing Conan can be found in the [Advanced Conan configuration](./docs/build/advanced_conan.md). - -#### Add xrplf remote - -Run the following command to add the `xrplf` remote, which hosts some of our dependencies: - -```bash -conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/ -``` +You can inspect the resulting profile with `conan profile show`. If it is not +suitable for your environment, create a custom profile and pass it to Conan — see +[Advanced Conan configuration](./docs/build/advanced_conan.md). ### Set Up Ccache @@ -368,14 +360,14 @@ After any updates or changes to dependencies, you may need to do the following: 4. [Regenerate lockfile](./docs/build/advanced_conan.md#conan-lockfile). 5. Re-run [conan install](#build-and-test). -If you are using the Nix development shell, prebuilt Conan binaries may be -incompatible with it — see -[Building xrpld in the Nix shell](./docs/build/nix.md#building-xrpld-in-the-nix-shell). +If you are using the Nix development shell, whether prebuilt Conan binaries apply +depends on your platform — see +[Prebuilt packages](./docs/build/nix.md#prebuilt-packages). #### ERROR: Package not resolved If you're seeing an error like `ERROR: Package 'snappy/1.1.10' not resolved: Unable to find 'snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1756234314.246' in remotes.`, -please [add `xrplf` remote](#add-xrplf-remote) or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes). +please [set Conan up](#set-up-conan) so the `xrplf` remote is configured, or re-run `conan export` for [patched recipes](./docs/build/advanced_conan.md#patched-recipes). ### `protobuf/port_def.inc` file not found diff --git a/conan/init.sh b/conan/init.sh new file mode 100755 index 0000000000..287ee83001 --- /dev/null +++ b/conan/init.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Install our Conan configuration, profiles and the xrplf remote into CONAN_HOME. +# Safe to re-run; never deletes the Conan home. + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CONAN_DIR="$(conan config home)" + +echo "Installing Conan configuration into ${CONAN_DIR}" +conan config install "${SCRIPT_DIR}/global.conf" +conan config install "${SCRIPT_DIR}/profiles" -tf "${CONAN_DIR}/profiles" +# This script manages these files, so make them read-only - Conan does not +# preserve the source mode. Only the files: the directories must stay writable +# for `conan config install` to replace them. +chmod a-w "${CONAN_DIR}/global.conf" +find "${CONAN_DIR}/profiles" -type f -exec chmod a-w {} + + +echo "Adding the xrplf Conan remote" +# --index 0: our patched recipes must win over Conan Center. +conan remote add --index 0 --force xrplf https://conan.xrplf.org/repository/conan/ diff --git a/conan/profiles/default b/conan/profiles/default index f2d93213ac..1b7eaff980 100644 --- a/conan/profiles/default +++ b/conan/profiles/default @@ -1,10 +1,7 @@ {% set os = detect_api.detect_os() %} {% set arch = detect_api.detect_arch() %} {% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %} -{% set compiler_version = version %} -{% if os == "Linux" %} {% set compiler_version = detect_api.default_compiler_version(compiler, version) %} -{% endif %} {% if os == "Macos" %} {# Minimum macOS the dependencies target. #} {# Without this, Conan builds each dependency against the (possibly newer) host SDK, so the #} diff --git a/docs/build/nix.md b/docs/build/nix.md index fad8bc701d..d1e40fcc89 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -124,14 +124,32 @@ nix develop -c "$SHELL" Once inside the Nix development shell, follow the standard [build instructions](../../BUILD.md#steps). The Nix shell provides all necessary tools (CMake, Ninja, Conan, etc.). -Two things differ from a system environment: - -**Prebuilt Conan packages.** There is no guarantee that binaries from the Conan cache will work when using Nix. If you encounter any errors, add `--build '*'` to the `conan install` command in [Build and Test](../../BUILD.md#build-and-test) to force Conan to compile everything from source. Keep the rest of the command as it is there, so it rebuilds the `build_type` you are actually configuring. - -**Coverage builds.** `-Dcoverage=ON` works in the `gcc` shell (and `gcc-plain` on Linux): +Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Linux): each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one. The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage. +## Conan configuration + +The shell runs [`conan/init.sh`](../../conan/init.sh) on entry, so +[Set Up Conan](../../BUILD.md#set-up-conan) is already done for you. It installs +into the shell's own Conan home: `CONAN_HOME=~/.conan2-nix`. + +### Prebuilt packages + +On **Linux**, the binaries on the `xrplf` remote are built in this same Nix +environment — CI runs in Docker images that bundle the dev shell's toolchain (see +[`nix/docker`](../../nix/docker)) — so `.#gcc` and `.#clang` can reuse them. The +`-plain` shells do not match that toolchain's glibc, so binaries from the remote +are not a reliable match there. + +On **macOS**, CI builds with Apple Clang, so the remote holds nothing for the Nix +`clang` toolchain and dependencies are compiled locally. We do not publish +Nix-built macOS binaries because a Conan package ID records the compiler version +but not the nixpkgs revision. + +To compile everything from source, add `--build '*'` to the `conan install` +command. + ## Automatic Activation with direnv [direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory. diff --git a/nix/devshell.nix b/nix/devshell.nix index cb4a99c76a..ac0b84e169 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -30,10 +30,29 @@ let }; customGccGcov = if pkgs.stdenv.isLinux then customCompilers.customGcov else plainGcov; + # Whole directory: init.sh locates the profiles relative to itself. + conanDir = ../conan; + + # Own Conan home, so Nix-built packages never share a cache with a system + # Conan. The stamp holds a content-addressed store path, so init.sh re-runs + # only when something in conan/ changes. + conanHook = '' + export CONAN_HOME=~/.conan2-nix + _xrpl_conan_stamp="$CONAN_HOME/.xrpld-devshell" + if [ "$(cat "$_xrpl_conan_stamp" 2>/dev/null)" != "${conanDir}" ]; then + if ${conanDir}/init.sh; then + printf '%s' "${conanDir}" >"$_xrpl_conan_stamp" + else + echo "⚠️ Conan setup failed - run ./conan/init.sh from the repository root to retry." + fi + fi + unset _xrpl_conan_stamp + ''; + # Shown when entering a *-plain shell. These exist only on Linux (see below), # where the stock toolchain diverges from CI. plainWarningHook = '' - echo "⚠️ WARNING: this is the stock nixpkgs toolchain and does not match CI's glibc. Prefer 'nix develop .#gcc' / '.#clang' unless you need to skip the custom-glibc build." + echo "⚠️ WARNING: this is the stock nixpkgs toolchain and does not match CI's glibc. Prefer 'nix develop .#gcc' / '.#clang' unless you need to skip the custom-glibc build." ''; # Tools to expose under version-suffixed names (see mkVersionedToolLinks). @@ -87,6 +106,7 @@ let shellHook = '' echo "Welcome to xrpld development shell"; ${compilerVersionHook} + ${conanHook} ${warningHook} ''; } From 4173f7e499e3dd55900bf72b3c05f68577448060 Mon Sep 17 00:00:00 2001 From: Braedon Klock Date: Mon, 10 Aug 2026 21:30:06 +0000 Subject: [PATCH 58/68] fix: Validate account_lines peer field type (#7728) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- API-CHANGELOG.md | 1 + src/test/rpc/AccountLines_test.cpp | 47 +++++++++++++++++++ .../rpc/handlers/account/AccountLines.cpp | 5 ++ 3 files changed, 53 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 79fb8ff522..bc3672588e 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -54,6 +54,7 @@ This section contains changes targeting a future version. - `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529) - `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529) - Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529) +- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728) ## XRP Ledger server version 3.1.0 diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index cb20de9bf5..3de2bdefa3 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -94,6 +94,24 @@ public: LedgerHeader const ledger3Info = env.closed()->header(); BEAST_EXPECT(ledger3Info.seq == 3); + { + // test peer non-string + auto testInvalidPeerParam = [&](auto const& param) { + json::Value params; + params[jss::account] = alice.human(); + params[jss::peer] = param; + auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "invalidParams"); + BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'."); + }; + + testInvalidPeerParam(1); + testInvalidPeerParam(1.1); + testInvalidPeerParam(true); + testInvalidPeerParam(json::Value(json::ValueType::Null)); + testInvalidPeerParam(json::Value(json::ValueType::Object)); + testInvalidPeerParam(json::Value(json::ValueType::Array)); + } { // alice is funded but has no lines. An empty array is returned. json::Value params; @@ -775,6 +793,35 @@ public: LedgerHeader const ledger3Info = env.closed()->header(); BEAST_EXPECT(ledger3Info.seq == 3); + { + // test peer non-string + auto testInvalidPeerParam = [&](auto const& param) { + json::Value params; + params[jss::account] = alice.human(); + params[jss::peer] = param; + + json::Value request; + request[jss::method] = "account_lines"; + request[jss::jsonrpc] = "2.0"; + request[jss::ripplerpc] = "2.0"; + request[jss::id] = 5; + request[jss::params] = params; + + auto const lines = env.rpc("json2", to_string(request)); + BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams"); + BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'."); + BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0"); + BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0"); + BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5); + }; + + testInvalidPeerParam(1); + testInvalidPeerParam(1.1); + testInvalidPeerParam(true); + testInvalidPeerParam(json::Value(json::ValueType::Null)); + testInvalidPeerParam(json::Value(json::ValueType::Object)); + testInvalidPeerParam(json::Value(json::ValueType::Array)); + } { // alice is funded but has no lines. An empty array is returned. json::Value params; diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp index 4a6d22d5d8..ac98e271b6 100644 --- a/src/xrpld/rpc/handlers/account/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp @@ -107,7 +107,12 @@ doAccountLines(rpc::JsonContext& context) std::string strPeer; if (params.isMember(jss::peer)) + { + if (!params[jss::peer].isString()) + return rpc::invalidFieldError(jss::peer); + strPeer = params[jss::peer].asString(); + } auto const raPeerAccount = [&]() -> std::optional { return strPeer.empty() ? std::nullopt : parseBase58(strPeer); From 60291c3ed613a749f6aeced06d485b1d478d9843 Mon Sep 17 00:00:00 2001 From: Kassaking7 <96991820+Kassaking7@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:34:28 +0000 Subject: [PATCH 59/68] fix: Allow OverrideFreeze to bypass individual/deep freeze on AMM trust lines (#6959) --- include/xrpl/tx/invariants/FreezeInvariant.h | 6 +- src/libxrpl/tx/invariants/FreezeInvariant.cpp | 23 +- src/test/app/AMMClawback_test.cpp | 204 ++++++++++++++++++ 3 files changed, 222 insertions(+), 11 deletions(-) diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h index 4b3e9beec4..c66e002872 100644 --- a/include/xrpl/tx/invariants/FreezeInvariant.h +++ b/include/xrpl/tx/invariants/FreezeInvariant.h @@ -69,7 +69,8 @@ private: IssuerChanges const& changes, STTx const& tx, beast::Journal const& j, - bool enforce); + bool enforce, + bool fixOverrideFreeze); static bool validateFrozenState( @@ -78,7 +79,8 @@ private: STTx const& tx, beast::Journal const& j, bool enforce, - bool globalFreeze); + bool globalFreeze, + bool fixOverrideFreeze); }; } // namespace xrpl diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp b/src/libxrpl/tx/invariants/FreezeInvariant.cpp index 0a604d4c39..c4340b9aec 100644 --- a/src/libxrpl/tx/invariants/FreezeInvariant.cpp +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp @@ -73,6 +73,7 @@ TransfersNotFrozen::finalize( * view.rules().enabled(fixFreezeExploit); */ [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze); + bool const fixOverrideFreeze = view.rules().enabled(fixCleanup3_4_0); return std::ranges::all_of(balanceChanges_, [&](auto const& entry) { auto const& [issue, changes] = entry; @@ -90,7 +91,7 @@ TransfersNotFrozen::finalize( return !enforce; } - return validateIssuerChanges(issuerSle, changes, tx, j, enforce); + return validateIssuerChanges(issuerSle, changes, tx, j, enforce, fixOverrideFreeze); }); } @@ -199,7 +200,8 @@ TransfersNotFrozen::validateIssuerChanges( IssuerChanges const& changes, STTx const& tx, beast::Journal const& j, - bool enforce) + bool enforce, + bool fixOverrideFreeze) { if (!issuer) { @@ -225,7 +227,7 @@ TransfersNotFrozen::validateIssuerChanges( { bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount); - if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze)) + if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze, fixOverrideFreeze)) { return false; } @@ -241,26 +243,29 @@ TransfersNotFrozen::validateFrozenState( STTx const& tx, beast::Journal const& j, bool enforce, - bool globalFreeze) + bool globalFreeze, + bool fixOverrideFreeze) { bool const freeze = change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze); bool const frozen = globalFreeze || deepFreeze || freeze; - bool const isAMMLine = change.line->isFlag(lsfAMMNode); - if (!frozen) { return true; } - // AMMClawbacks are allowed to override some freeze rules - if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze)) + // Pre-fixCleanup3_4_0: the isAMMLine check incorrectly blocked clawback on + // individually-frozen or deep-frozen AMM trust lines. + // Post-fixCleanup3_4_0: AMMClawbacks are allowed to override all freeze types. + bool const isAMMLine = change.line->isFlag(lsfAMMNode); + if ((fixOverrideFreeze || !isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze)) { JLOG(j.debug()) << "Invariant check allowing funds to be moved " << (change.balanceChangeSign > 0 ? "to" : "from") - << " a frozen trustline for AMMClawback " << tx.getTransactionID(); + << " a frozen trustline for a freeze privileged transaction " + << tx.getTransactionID(); return true; } diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index ba416d8192..90bface1fb 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2155,6 +2155,209 @@ class AMMClawback_test : public beast::unit_test::Suite } BEAST_EXPECT(env.balance(carol, eur) == eur(7750)); } + + // gw (USD issuer) individually freezes the AMM-USD trust line. + // AMMClawback must still succeed because the freeze invariant + // short-circuits before reaching the AMM line check (no receivers in + // the USD issuer's change set). Behavior is identical with or without + // fixCleanup3_4_0. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + // gw individually freezes the AMM-USD trust line (AMM pseudo-account + // <-> gw), not alice's trust line. + env(trust(gw, STAmount{Issue{usd.currency, amm.ammAccount()}, 0}, tfSetFreeze)); + env.close(); + + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + + // gw2 (EUR issuer) individually freezes the AMM-EUR trust line. + // The EUR flow (AMM → alice) is a genuine P2P transfer checked by the + // freeze invariant. Pre-fixCleanup3_4_0 the isAMMNode guard incorrectly + // blocked AMMClawback's overrideFreeze privilege on that trust line. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + // gw2 individually freezes the AMM-EUR trust line. + env(trust(gw2, STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, tfSetFreeze)); + env.close(); + + if (features[fixCleanup3_4_0]) + { + // Post-fixCleanup3_4_0: overrideFreeze privilege applies to + // all freeze types on AMM trust lines. + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT( + amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + else + { + // Pre-fixCleanup3_4_0: the isAMMNode guard prevents the + // overrideFreeze privilege from applying to individually-frozen + // AMM trust lines, so the invariant blocks the clawback. + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED)); + } + } + + // gw2 (EUR issuer) globally freezes its issued assets. AMMClawback + // must still be able to return EUR from the AMM to alice. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + env(fset(gw2, asfGlobalFreeze)); + env.close(); + + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + + // Same as above but gw2 deep-freezes the AMM-EUR trust line. + if (features[featureDeepFreeze]) + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(1000000), gw, gw2, alice); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + env.require(Flags(gw, asfAllowTrustLineClawback)); + + auto const usd = gw["USD"]; + env.trust(usd(100000), alice); + env(pay(gw, alice, usd(3000))); + env.close(); + + auto const eur = gw2["EUR"]; + env.trust(eur(100000), alice); + env(pay(gw2, alice, eur(3000))); + env.close(); + + AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12})); + + // gw2 deep-freezes the AMM-EUR trust line. + env(trust( + gw2, + STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, + tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + if (features[fixCleanup3_4_0]) + { + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS)); + env.close(); + + env.require(Balance(alice, usd(1000))); + env.require(Balance(alice, eur(2500))); + BEAST_EXPECT( + amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13})); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13})); + } + else + { + // Pre-fixCleanup3_4_0: same isAMMNode guard issue blocks the + // clawback on deep-frozen AMM trust lines. + env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED)); + } + } } void @@ -2530,6 +2733,7 @@ class AMMClawback_test : public beast::unit_test::Suite // precision loss caught in transaction layer -> tecPRECISION_LOSS all - fixAMMClawbackRounding - featureMPTokensV2, all - featureMPTokensV2, + all - fixCleanup3_4_0, all}) { testAMMClawbackSpecificAmount(features); From 6f5de9067aedad3ae5f7bb555d102ca67a67fb60 Mon Sep 17 00:00:00 2001 From: Peter Chen <34582813+PeterChen13579@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:37:38 +0000 Subject: [PATCH 60/68] chore: Mark unreachable branches in Confidential Transfer with UNREACHABLE (#7903) --- src/libxrpl/protocol/ConfidentialTransfer.cpp | 91 ++++++++++++++++--- .../token/ConfidentialMPTClawback.cpp | 55 +++++++++-- .../token/ConfidentialMPTConvert.cpp | 53 +++++++++-- .../token/ConfidentialMPTConvertBack.cpp | 46 +++++++++- .../token/ConfidentialMPTMergeInbox.cpp | 35 ++++++- .../transactors/token/ConfidentialMPTSend.cpp | 59 ++++++++++-- 6 files changed, 298 insertions(+), 41 deletions(-) diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index fe8a08c2ef..ecd4832928 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -124,7 +125,12 @@ std::optional makeEcPair(Slice const& buffer) { if (buffer.length() != 2 * kEcCiphertextComponentLength) - return std::nullopt; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::makeEcPair : callers must pre-validate ciphertext length"); + return std::nullopt; + // LCOV_EXCL_STOP + } auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) { return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length()); @@ -266,7 +272,13 @@ std::optional encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId) { if (pubKeySlice.size() != kEcPubKeyLength) - return std::nullopt; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::encryptCanonicalZeroAmount : callers must pre-validate public key length"); + return std::nullopt; + // LCOV_EXCL_STOP + } EcPair pair{}; secp256k1_pubkey pubKey; @@ -274,14 +286,24 @@ encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, M secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength); res != 1) { - return std::nullopt; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::encryptCanonicalZeroAmount : public key read from the ledger must already be " + "valid"); + return std::nullopt; + // LCOV_EXCL_STOP } if (auto res = generate_canonical_encrypted_zero( secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data()); res != 1) { - return std::nullopt; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::encryptCanonicalZeroAmount : canonical zero generation cannot fail for a " + "valid public key"); + return std::nullopt; + // LCOV_EXCL_STOP } return serializeEcPair(pair); @@ -301,7 +323,11 @@ verifyRevealedAmount( issuer.publicKey.size() != kEcPubKeyLength || issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyRevealedAmount : callers must pre-validate holder/issuer field lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } auto const holderP = toParticipant(holder); @@ -313,7 +339,11 @@ verifyRevealedAmount( if (auditor->publicKey.size() != kEcPubKeyLength || auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyRevealedAmount : callers must pre-validate auditor field lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } auditorP = toParticipant(*auditor); auditorPtr = &auditorP; @@ -337,7 +367,12 @@ checkEncryptedAmountFormat(STObject const& object) if (!object.isFieldPresent(sfHolderEncryptedAmount) || !object.isFieldPresent(sfIssuerEncryptedAmount)) { - return temMALFORMED; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::checkEncryptedAmountFormat : callers already enforce that these fields are " + "present"); + return temMALFORMED; + // LCOV_EXCL_STOP } if (object[sfHolderEncryptedAmount].length() != kEcGamalEncryptedTotalLength || @@ -366,7 +401,12 @@ TER verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) { if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::verifySchnorrProof : callers must pre-validate proof/public key length"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0) return tecBAD_PROOF; @@ -385,7 +425,12 @@ verifyClawbackProof( if (ciphertext.size() != kEcGamalEncryptedTotalLength || pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyClawbackProof : callers must pre-validate ciphertext/public " + "key/proof length"); + return tecINTERNAL; + // LCOV_EXCL_STOP } if (mpt_verify_clawback_proof( @@ -420,7 +465,12 @@ verifySendProof( amountCommitment.size() != kEcPedersenCommitmentLength || balanceCommitment.size() != kEcPedersenCommitmentLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifySendProof : callers must pre-validate proof/participant/commitment " + "lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } std::vector participants; @@ -433,12 +483,22 @@ verifySendProof( if (auditor->publicKey.size() != kEcPubKeyLength || auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE("xrpl::verifySendProof : callers must pre-validate auditor field lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } participants.push_back(toParticipant(*auditor)); } if (participants.size() != recipientCount) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifySendProof : participant count must match the requested recipient " + "count"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } if (mpt_verify_send_proof( proof.data(), @@ -468,7 +528,12 @@ verifyConvertBackProof( spendingBalance.size() != kEcGamalEncryptedTotalLength || balanceCommitment.size() != kEcPedersenCommitmentLength) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyConvertBackProof : callers must pre-validate proof/public " + "key/balance/commitment lengths"); + return tecINTERNAL; + // LCOV_EXCL_STOP } if (mpt_verify_convert_back_proof( diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp index 6366e99105..19ec99702a 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -70,7 +71,14 @@ ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx) // Sanity check: account must be the same as issuer if (sleIssuance->getAccountID(sfIssuer) != account) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::preclaim : preflight already validated the " + "submitter is the issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } // Check if issuance has issuer ElGamal public key if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) @@ -127,7 +135,14 @@ ConfidentialMPTClawback::doApply() auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder)); if (!sleIssuance || !sleHolderMPToken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : preclaim already validated these " + "objects exist"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const clawAmount = ctx_.tx[sfMPTAmount]; @@ -137,11 +152,25 @@ ConfidentialMPTClawback::doApply() // After clawback, the balance should be encrypted zero. auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID); if (!encZeroForHolder) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail " + "for an already-valid holder public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID); if (!encZeroForIssuer) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail " + "for an already-valid issuer public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } // Set holder's confidential balances to encrypted zero (*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder; @@ -154,14 +183,28 @@ ConfidentialMPTClawback::doApply() // Sanity check: the issuance must have an auditor public key if // auditing is enabled. if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey)) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : the holder's auditor balance implies " + "the issuance has an auditor public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey]; auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID); if (!encZeroForAuditor) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot " + "fail for an already-valid auditor public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } (*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor); } diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 454eb39ead..5be3892151 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -89,7 +90,14 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) // already checked in preflight, but should also check that issuer on the // issuance isn't the account either if (sleIssuance->getAccountID(sfIssuer) == account) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::preclaim : issuer derived from the MPT ID must " + "match the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey); @@ -207,11 +215,25 @@ ConfidentialMPTConvert::doApply() auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); if (!sleMptoken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the MPToken " + "exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); if (!sleIssuance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the issuance " + "exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const amtToConvert = ctx_.tx[sfMPTAmount]; auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0); @@ -273,7 +295,14 @@ ConfidentialMPTConvert::doApply() if (auditorEc) { if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : issuance-level auditing implies " + "the MPToken already carries an auditor balance"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]); if (!sum) @@ -308,7 +337,14 @@ ConfidentialMPTConvert::doApply() (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID); if (!zeroBalance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : canonical zero encryption cannot fail " + "for an already-valid holder public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance); } @@ -316,7 +352,12 @@ ConfidentialMPTConvert::doApply() { // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should // exist together - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvert::doApply : confidential balance fields must be all " + "present or all absent"); + return tecINTERNAL; + // LCOV_EXCL_STOP } view().update(sleIssuance); diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 87f9e476d6..1e3617ffbd 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -72,7 +73,14 @@ verifyProofs( std::shared_ptr const& mptoken) { if (!mptoken->isFieldPresent(sfHolderEncryptionKey)) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::verifyProofs : preclaim already validated the holder encryption key is " + "present"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const mptIssuanceID = tx[sfMPTokenIssuanceID]; auto const account = tx[sfAccount]; @@ -169,7 +177,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx) // already checked in preflight, but should also check that issuer on // the issuance isn't the account either if (sleIssuance->getAccountID(sfIssuer) == account) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::preclaim : issuer derived from the MPT ID must " + "match the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account)); if (!sleMptoken) @@ -185,7 +200,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx) // Sanity check: holder's MPToken must have auditor balance field if auditing // is enabled if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::preclaim : issuance-level auditing implies the " + "MPToken already carries an auditor balance"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } // if the total circulating confidential balance is smaller than what the // holder is trying to convert back, we know for sure this txn should @@ -215,11 +237,25 @@ ConfidentialMPTConvertBack::doApply() auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); if (!sleMptoken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the " + "MPToken exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID)); if (!sleIssuance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the " + "issuance exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const amtToConvertBack = ctx_.tx[sfMPTAmount]; auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0); diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp index 0b98382a61..6485578cb4 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -49,7 +50,14 @@ ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx) // already checked in preflight, but should also check that issuer on the // issuance isn't the account either if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount]) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::preclaim : issuer derived from the MPT ID must " + "match the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } auto const sleMptoken = ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount])); @@ -82,14 +90,26 @@ ConfidentialMPTMergeInbox::doApply() auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_)); if (!sleMptoken) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated the " + "MPToken exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } // sanity check if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) || !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) || !sleMptoken->isFieldPresent(sfHolderEncryptionKey)) { - return tecINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated these " + "fields are present"); + return tecINTERNAL; + // LCOV_EXCL_STOP } // Merge inbox into spending: spending = spending + inbox @@ -114,7 +134,14 @@ ConfidentialMPTMergeInbox::doApply() encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID); if (!zeroEncryption) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMergeInbox::doApply : canonical zero encryption cannot fail " + "for an already-valid holder public key"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption); diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index f4c7b98c41..e713ae5029 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -105,7 +106,14 @@ verifySendProofs( { // Sanity check if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::detail::verifySendProofs : caller must pre-validate sender/destination/" + "issuance existence"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); @@ -204,7 +212,14 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx) // Sanity check: issuer isn't the sender if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount]) - return tefINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTSend::preclaim : issuer derived from the MPT ID must match " + "the ledger's stored issuer"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } // Check sender's MPToken existence auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account)); @@ -238,7 +253,12 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx) (!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) || !sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance))) { - return tefINTERNAL; // LCOV_EXCL_LINE + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTSend::preclaim : issuance-level auditing implies both " + "MPTokens already carry an auditor balance"); + return tefINTERNAL; + // LCOV_EXCL_STOP } // Check lock @@ -283,7 +303,14 @@ ConfidentialMPTSend::doApply() auto const sleDestAcct = view().read(keylet::account(destination)); if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTSend::doApply : preclaim already validated these objects " + "exist"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } // Deposit preauth authorization was already verified in preclaim. // Remove any expired credentials. @@ -353,7 +380,13 @@ ConfidentialMPTSend::doApply() auto rerandomizedDestEc = rerandomizeCiphertext( destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge); if (!rerandomizedDestEc) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed to rerandomize destination inbox ciphertext."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox]; auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc); @@ -374,7 +407,13 @@ ConfidentialMPTSend::doApply() auto rerandomizedIssuerEc = rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge); if (!rerandomizedIssuerEc) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed to rerandomize destination issuer ciphertext."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance]; auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc); @@ -396,7 +435,13 @@ ConfidentialMPTSend::doApply() auto rerandomizedAuditorEc = rerandomizeCiphertext( *auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge); if (!rerandomizedAuditorEc) - return tecINTERNAL; // LCOV_EXCL_LINE + { + // LCOV_EXCL_START + JLOG(ctx_.journal.error()) + << "ConfidentialMPTSend failed to rerandomize destination auditor ciphertext."; + return tecINTERNAL; + // LCOV_EXCL_STOP + } auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance]; auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc); From 909cc5bba90879b6187595d46af743fa38df7c51 Mon Sep 17 00:00:00 2001 From: Bryan Date: Mon, 10 Aug 2026 21:37:53 +0000 Subject: [PATCH 61/68] fix: Prevent silent zero AMM clawbacks due to integer MPT rounding (#7704) Co-authored-by: Bart --- .../tx/transactors/dex/AMMClawback.cpp | 9 +- src/test/app/AMMClawbackMPT_test.cpp | 155 +++++++++++++++++- 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index c1ef9f875e..455b2ad5c5 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -256,7 +256,7 @@ AMMClawback::applyGuts(Sandbox& sb) } if (!isTesSuccess(result)) - return result; // LCOV_EXCL_LINE + return result; if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3)) { @@ -353,6 +353,13 @@ AMMClawback::equalWithdrawMatchingOneAmount( auto amountRounded = getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); + // The requested clawback amount is likely too small and results in + // one-sided pool withdrawal due to round off. Fail so the issuer can + // clawback a larger amount. + if (rules.enabled(fixCleanup3_4_0) && + (amountRounded == beast::kZero || amount2Rounded == beast::kZero)) + return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; + return AMMWithdraw::withdraw( sb, ammSle, diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp index 6facafde4a..6c7aa99156 100644 --- a/src/test/app/AMMClawbackMPT_test.cpp +++ b/src/test/app/AMMClawbackMPT_test.cpp @@ -137,7 +137,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite AMM amm(env, gw, btc(100), usd(100)); env.close(); amm.deposit(alice, 1'000); - env.close(); // can not clawback when tfMPTCanClawback is not enabled env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION)); @@ -503,6 +502,150 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite } } + void + testAMMClawbackAmountRoundsToZero(FeatureBitset features) + { + // Ensure a clawback that rounds down to zero MPT fails with + // tecAMM_FAILED instead of silently burning the holder's LP. + testcase("test AMMClawback amount that rounds down to zero"); + using namespace jtx; + + Env env(*this, features); + Account const gw{"gateway"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10'000'000), gw, alice, bob); + env.close(); + + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + // The clawed asset (amountRounded) rounds to zero while its XRP + // counterpart is always large. + { + MPTTester const mptBtc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 1'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + MPT const btc = mptBtc; + + AMM amm(env, alice, btc(3), XRP(333'000)); + amm.deposit(bob, btc(3), XRP(333'000)); + + [[maybe_unused]] auto const [poolBtcBefore, poolXrpBefore, lptBefore] = amm.balances(); + BEAST_EXPECT(poolBtcBefore == btc(6)); + + auto const issuerOABefore = mptBtc.getBalance(gw); + auto const aliceLpBefore = amm.getLPTokensBalance(alice.id()); + auto const bobLpBefore = amm.getLPTokensBalance(bob.id()); + + // Attempt to clawback 1/6th of the BTC pool. When the zero-rounding + // guard is active (gated by fixCleanup3_4_0) the rounded amount + // drops to 0 and should trigger tecAMM_FAILED. + env(amm::ammClawback(gw, alice, btc, XRP, btc(1)), + Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS})); + env.close(); + + [[maybe_unused]] auto const [poolBtcAfter, poolXrpAfter, lptAfter] = amm.balances(); + auto const issuerOAAfter = mptBtc.getBalance(gw); + auto const aliceLpAfter = amm.getLPTokensBalance(alice.id()); + auto const bobLpAfter = amm.getLPTokensBalance(bob.id()); + + if (features[fixCleanup3_4_0]) + { + // Post-fixCleanup3_4_0: Clawback fails because the BTC balance + // would round to zero. All balances must remain untouched. + BEAST_EXPECT(poolBtcAfter == poolBtcBefore); + BEAST_EXPECT(poolXrpAfter == poolXrpBefore); + BEAST_EXPECT(issuerOAAfter == issuerOABefore); + BEAST_EXPECT(aliceLpAfter == aliceLpBefore); + BEAST_EXPECT(bobLpAfter == bobLpBefore); + } + else + { + // Pre-fixCleanup3_4_0: BTC rounds to zero and the clawback + // silently burns alice's LP without clawing back any BTC. + BEAST_EXPECT(poolBtcAfter == poolBtcBefore); + BEAST_EXPECT(poolXrpAfter < poolXrpBefore); + BEAST_EXPECT(issuerOAAfter == issuerOABefore); + BEAST_EXPECT(aliceLpAfter < aliceLpBefore); + BEAST_EXPECT(bobLpAfter == bobLpBefore); + } + } + + // The pool above only ever rounds the clawed asset (amountRounded) to + // zero; its XRP counterpart is always large. Exercise the other operand + // of the guard (amount2Rounded == 0) with an MPT/MPT pool where the + // *paired* asset is the tiny integer that floors to zero while the + // clawed asset still rounds non-zero. + { + Account const carol{"carol"}; + Account const dan{"dan"}; + env.fund(XRP(10'000'000), carol, dan); + env.close(); + + MPTTester const mptBtc( + {.env = env, + .issuer = gw, + .holders = {carol, dan}, + .pay = 100'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + MPT const btc = mptBtc; + + MPTTester const mptEth( + {.env = env, + .issuer = gw, + .holders = {carol, dan}, + .pay = 1'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + MPT const eth = mptEth; + + // btc pool dwarfs the eth pool, so a ~1/12th claw withdraws a + // non-zero btc amount while the eth counterpart rounds to zero. + AMM amm(env, carol, btc(3'000), eth(3)); + amm.deposit(dan, btc(3'000), eth(3)); + + [[maybe_unused]] auto const [poolBtcBefore, poolEthBefore, lptBefore] = amm.balances(); + BEAST_EXPECT(poolBtcBefore == btc(6'000)); + BEAST_EXPECT(poolEthBefore == eth(6)); + + auto const carolLpBefore = amm.getLPTokensBalance(carol.id()); + auto const danLpBefore = amm.getLPTokensBalance(dan.id()); + + env(amm::ammClawback(gw, carol, btc, eth, btc(500)), + Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS})); + env.close(); + + [[maybe_unused]] auto const [poolBtcAfter, poolEthAfter, lptAfter] = amm.balances(); + auto const carolLpAfter = amm.getLPTokensBalance(carol.id()); + auto const danLpAfter = amm.getLPTokensBalance(dan.id()); + + if (features[fixCleanup3_4_0]) + { + // Post-fixCleanup3_4_0: clawback fails because the ETH (Asset2) + // balance would round to zero (guard fires via + // amount2Rounded == 0). All balances must remain untouched. + BEAST_EXPECT(poolBtcAfter == poolBtcBefore); + BEAST_EXPECT(poolEthAfter == poolEthBefore); + BEAST_EXPECT(carolLpAfter == carolLpBefore); + BEAST_EXPECT(danLpAfter == danLpBefore); + } + else + { + // Pre-fixCleanup3_4_0: the asymmetric round-off goes through. + // btc is clawed (non-zero) but eth rounds to zero, so the eth + // pool is untouched while carol's LP is burned. This asymmetry + // proves amount2Rounded == 0 is the trigger. + BEAST_EXPECT(poolBtcAfter < poolBtcBefore); + BEAST_EXPECT(poolEthAfter == poolEthBefore); + BEAST_EXPECT(carolLpAfter < carolLpBefore); + BEAST_EXPECT(danLpAfter == danLpBefore); + } + } + } + void testAMMClawbackAll(FeatureBitset features) { @@ -543,7 +686,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite // gw clawback all BTC from alice amm.deposit(bob, btc(1'000'000000), usd(2000)); - env.close(); BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(3000), IOUAmount(3000000))); auto aliceBTC = env.balance(alice, btc); @@ -921,7 +1063,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(8'000), IOUAmount(4'000'000))); amm.deposit(bob, btc(1'000'000000), usd(4'000)); - env.close(); BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(12'000), IOUAmount(6'000'000))); auto aliceBTC = env.balance(alice, btc); @@ -1361,7 +1502,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(amm.expectBalances(XRP(100), btc(400), IOUAmount(200000))); amm.deposit(alice, btc(400)); - env.close(); BEAST_EXPECT(amm.expectBalances(XRP(100), btc(800), IOUAmount{282842'712474619, -9})); auto aliceBTC = env.balance(alice, MPT(btc)); @@ -1407,7 +1547,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200))); amm.deposit(alice, btc(400)); - env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12})); auto aliceBTC = env.balance(alice, MPT(btc)); @@ -1462,7 +1601,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200))); amm.deposit(alice, btc(400)); - env.close(); BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12})); auto aliceBTC = env.balance(alice, MPT(btc)); @@ -1669,7 +1807,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION)); // Although USD is clawable with asfAllowTrustLineClawback. - // When tfClawTwoAssets is set, we will claw Asser2 as well. + // When tfClawTwoAssets is set, we will claw Asset2 as well. // But Asset2 is not clawable. tfMPTCanClawback was not set for BTC. env(amm::ammClawback(gw, alice, usd, btc, std::nullopt), Txflags(tfClawTwoAssets), @@ -1819,6 +1957,9 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite testInvalidRequest(all); testFeatureDisabled(all); testAMMClawbackAmount(all); + testAMMClawbackAmount(all - fixCleanup3_4_0); + testAMMClawbackAmountRoundsToZero(all); + testAMMClawbackAmountRoundsToZero(all - fixCleanup3_4_0); testAMMClawbackAll(all); testAMMClawbackAmountSameIssuer(all); testAMMClawbackAllSameIssuer(all); From 639943123cced8fe0aff2b6477527558d31b5f6b Mon Sep 17 00:00:00 2001 From: Chenna Keshava B S <21219765+ckeshava@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:49:02 +0000 Subject: [PATCH 62/68] fix: Validate buy/sell flag in nft RPC input (#7725) --- src/test/app/NFToken_test.cpp | 82 +++++++++++++++++++ .../rpc/handlers/orderbook/NFTOffersHelpers.h | 11 +++ 2 files changed, 93 insertions(+) diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index a7437eea7f..7fcd34640b 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -4790,6 +4790,87 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite checkOffers("nft_buy_offers", 501, 2, __LINE__); } + void + testNftXxxOffersMarkerWrongSide(FeatureBitset features) + { + // A pagination marker passed to nft_buy_offers / nft_sell_offers must + // reference an offer on the same side (buy vs. sell) as the directory + // being enumerated. A wrong-side marker is rejected with invalidParams. + // + // Note: the pre-fix code also returned invalidParams for a wrong-side + // marker, but only after scanning the entire target directory (an + // O(directory size) walk usable to burn CPU). The fix short-circuits + // that scan. The scan-avoidance is not observable from the RPC + // response, so this test locks the rejection contract (wrong-side -> + // error, same-side -> success) rather than the performance property. + testcase("nft_buy_offers and nft_sell_offers wrong-side marker"); + + using namespace test::jtx; + + Env env{*this, features}; + + Account const issuer{"issuer"}; + Account const buyer{"buyer"}; + + env.fund(XRP(10000), issuer, buyer); + env.close(); + + // Mint a transferable NFT. + uint256 const nftID{token::getNextID(env, issuer, 0u, tfTransferable)}; + env(token::mint(issuer, 0), Txflags(tfTransferable)); + env.close(); + + // Create one sell offer (from the issuer, who owns the NFT) and one + // buy offer (from the buyer) for the same NFT. + env(token::createOffer(issuer, nftID, XRP(100)), Txflags(tfSellNFToken)); + env(token::createOffer(buyer, nftID, XRP(50)), token::Owner(issuer)); + env.close(); + + // Grab the index of the single offer on each side from the RPC + // response so we can use it as a marker. + auto firstOfferIndex = [this, &env, &nftID](char const* request) { + json::Value params; + params[jss::nft_id] = to_string(nftID); + json::Value const result = env.rpc("json", request, to_string(params))[jss::result]; + BEAST_EXPECT(result.isMember(jss::offers) && result[jss::offers].size() == 1); + return result[jss::offers][0u][jss::nft_offer_index].asString(); + }; + + std::string const sellOfferIndex = firstOfferIndex("nft_sell_offers"); + std::string const buyOfferIndex = firstOfferIndex("nft_buy_offers"); + + auto queryWithMarker = [&env, &nftID](char const* request, std::string const& marker) { + json::Value params; + params[jss::nft_id] = to_string(nftID); + params[jss::marker] = marker; + return env.rpc("json", request, to_string(params))[jss::result]; + }; + + // A marker referencing an offer on the wrong side is rejected with + // invalidParams. + { + // Sell-side marker passed to nft_buy_offers. + json::Value const result = queryWithMarker("nft_buy_offers", sellOfferIndex); + BEAST_EXPECT(result[jss::error].asString() == "invalidParams"); + } + { + // Buy-side marker passed to nft_sell_offers. + json::Value const result = queryWithMarker("nft_sell_offers", buyOfferIndex); + BEAST_EXPECT(result[jss::error].asString() == "invalidParams"); + } + + // A same-side marker is still accepted. With a single offer on each + // side, resuming after it simply yields no further offers. + { + json::Value const result = queryWithMarker("nft_buy_offers", buyOfferIndex); + BEAST_EXPECT(!result.isMember(jss::error)); + } + { + json::Value const result = queryWithMarker("nft_sell_offers", sellOfferIndex); + BEAST_EXPECT(!result.isMember(jss::error)); + } + } + void testNFTokenNegOffer(FeatureBitset features) { @@ -7305,6 +7386,7 @@ protected: testNFTokenWithTickets(features); testNFTokenDeleteAccount(features); testNftXxxOffers(features); + testNftXxxOffersMarkerWrongSide(features); testNFTokenNegOffer(features); testIOUWithTransferFee(features); testBrokeredSaleToSelf(features); diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h index e03830ae0d..21bf3f8be8 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h @@ -93,6 +93,17 @@ enumerateNFTOffers(rpc::JsonContext& context, uint256 const& nftId, Keylet const if (!sle || nftId != sle->getFieldH256(sfNFTokenID)) return rpcError(RpcInvalidParams); + // Reject a marker that references an offer on the opposite side + // (buy vs. sell) of the directory being enumerated. Without this + // check the marker's node hint points into the other directory, so + // forEachItemAfter never finds `startAfter` and instead scans every + // page of `directory` before returning invalidParams -- turning an + // O(1) rejection into an O(directory size) walk. + auto const offerDir = + sle->isFlag(lsfSellNFToken) ? keylet::nftSells(nftId) : keylet::nftBuys(nftId); + if (directory.key != offerDir.key) + return rpcError(RpcInvalidParams); + startHint = sle->getFieldU64(sfNFTokenOfferNode); appendNftOfferJson(context.app, sle, jsonOffers); offers.reserve(reserve); From 0a572833eae96c28a30e7f5dcc143fb26cfa33bd Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Tue, 11 Aug 2026 12:38:40 +0000 Subject: [PATCH 63/68] chore: Gtest migration followups second pass (#7888) --- .cspell.config.yaml | 1 + cmake/XrplCov.cmake | 1 + include/xrpl/basics/Buffer.h | 14 + src/benchmarks/libxrpl/nodestore/Backend.cpp | 19 +- .../libxrpl/nodestore/NodeStoreBench.h | 5 +- src/tests/libxrpl/basics/Buffer.cpp | 517 +++++++++++------- src/tests/libxrpl/basics/IntrusiveShared.cpp | 11 +- src/tests/libxrpl/basics/base_uint.cpp | 227 ++++---- src/tests/libxrpl/shamap/SHAMap.cpp | 3 +- 9 files changed, 446 insertions(+), 352 deletions(-) diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 21b0145f43..bb763e9935 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -365,6 +365,7 @@ words: - xchain - ximinez - XMACRO + - xored - xrpkuwait - xrpl - xrpld diff --git a/cmake/XrplCov.cmake b/cmake/XrplCov.cmake index 86ba534a88..05d9ed3806 100644 --- a/cmake/XrplCov.cmake +++ b/cmake/XrplCov.cmake @@ -44,6 +44,7 @@ setup_target_for_coverage_gcovr( EXCLUDE "src/test" "src/tests" + "src/benchmarks" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb" diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index 705a5ef51a..00a6b7ecf9 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -156,6 +157,19 @@ public: } /** @} */ + /** + * Set every byte in the buffer to the given value. + * + * The size is unchanged, and this is a no-op on an empty buffer. + * + * @param value the byte to write to every position. + */ + void + fill(std::uint8_t value) noexcept + { + std::fill_n(p_.get(), size_, value); + } + /** * Reset the buffer. * All memory is deallocated. The resulting size is 0. diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp index cd3e15bd65..9d5937f869 100644 --- a/src/benchmarks/libxrpl/nodestore/Backend.cpp +++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp @@ -41,10 +41,11 @@ struct RunState release() { harness.reset(); - Batch{}.swap(present); - Batch{}.swap(recent); - std::vector{}.swap(missing); - std::vector{}.swap(shuffle); + present = Batch{}; + recent = Batch{}; + missing = std::vector{}; + shuffle = std::vector{}; + avgPayload = 0; } }; @@ -239,9 +240,13 @@ registerWorkload(BackendConfig const& bc, Workload const& w) if (!w.pinToPool) { auto rs = std::make_shared(); - auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)); - b->RangeMultiplier(10)->Range(kPoolSizes.front(), kPoolSizes.back()); - b->Threads(1)->Threads(4)->Threads(8)->UseRealTime(); + benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)) + ->RangeMultiplier(10) + ->Range(kPoolSizes.front(), kPoolSizes.back()) + ->Threads(1) + ->Threads(4) + ->Threads(8) + ->UseRealTime(); return; } diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h index debdc5d47a..6122dd2535 100644 --- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -297,12 +297,11 @@ struct BackendConfig inline std::vector const& backendConfigs() { + // Use factory settings for each DB static std::vector const kConfigs = { {.name = "nudb", .config = "type=nudb"}, #if XRPL_ROCKSDB_AVAILABLE - {.name = "rocksdb", - .config = "type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256," - "file_size_mb=8,file_size_mult=2"}, + {.name = "rocksdb", .config = "type=rocksdb"}, #endif }; return kConfigs; diff --git a/src/tests/libxrpl/basics/Buffer.cpp b/src/tests/libxrpl/basics/Buffer.cpp index 9cdf610282..a3f78e8bcf 100644 --- a/src/tests/libxrpl/basics/Buffer.cpp +++ b/src/tests/libxrpl/basics/Buffer.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -12,8 +13,18 @@ namespace xrpl::test { +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); + struct BufferTest : public ::testing::Test { + static constexpr auto kRandomData = std::to_array( + {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, + 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, + 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}); + + static constexpr std::size_t kHalf = kRandomData.size() / 2; + static bool sane(Buffer const& b) { @@ -22,239 +33,321 @@ struct BufferTest : public ::testing::Test return b.data() != nullptr; } + + /** + * Check the state Buffer documents for a moved-from buffer: "the other buffer is reset", i.e. + * empty and sane. + * + * Zeroing the size is not incidental tidiness. Moving the member unique_ptr nulls the data + * pointer whether Buffer wants it or not, so a moved-from buffer that kept its old size would + * lie about itself everywhere: alloc() would take its `n == size_` early-out and hand back a + * null pointer while still reporting the old size, fill() would run std::fill_n over a null + * pointer, and the Slice conversion would publish {nullptr, oldSize} to callers. A moved-from + * Buffer has to be a usable empty Buffer rather than a landmine, which is why the tests below + * assert this state instead of treating a moved-from buffer as untouchable. + */ + static void + checkEmptyAfterMove(Buffer const& buf) + { + EXPECT_TRUE(sane(buf)); + EXPECT_TRUE(buf.empty()); + } + + Buffer const emptyBuffer; + Buffer const firstHalf{kRandomData.data(), kHalf}; + Buffer const secondHalf{kRandomData.data() + kHalf, kHalf}; + Buffer const whole{kRandomData.data(), kRandomData.size()}; }; -TEST_F(BufferTest, buffer) +TEST_F(BufferTest, default_constructed_is_empty) { - std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a, - 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c, - 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3}; + Buffer const b; - Buffer const b0; - EXPECT_TRUE(sane(b0)); - EXPECT_TRUE(b0.empty()); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); + EXPECT_EQ(b.data(), nullptr); +} - Buffer b1{0}; - EXPECT_TRUE(sane(b1)); - EXPECT_TRUE(b1.empty()); - std::memcpy(b1.alloc(16), data, 16); - EXPECT_TRUE(sane(b1)); - EXPECT_FALSE(b1.empty()); - EXPECT_EQ(b1.size(), 16); +TEST_F(BufferTest, zero_sized_construction_is_empty) +{ + Buffer const b{0}; - Buffer b2{b1.size()}; - EXPECT_TRUE(sane(b2)); - EXPECT_FALSE(b2.empty()); - EXPECT_EQ(b2.size(), b1.size()); - std::memcpy(b2.data(), data + 16, 16); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); +} - Buffer b3{data, sizeof(data)}; - EXPECT_TRUE(sane(b3)); - EXPECT_FALSE(b3.empty()); - EXPECT_EQ(b3.size(), sizeof(data)); - EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0); +TEST_F(BufferTest, alloc_grows_an_empty_buffer) +{ + Buffer b{0}; + std::memcpy(b.alloc(kHalf), kRandomData.data(), kHalf); - // Check equality and inequality comparisons. - // For code readability, we want to use general - // EXPECT_TRUE instead of specific EXPECT_EQ etc. - EXPECT_TRUE(b0 == b0); - EXPECT_TRUE(b0 != b1); - EXPECT_TRUE(b1 == b1); - EXPECT_TRUE(b1 != b2); - EXPECT_TRUE(b2 != b3); + EXPECT_TRUE(sane(b)); + EXPECT_FALSE(b.empty()); + EXPECT_EQ(b.size(), kHalf); + EXPECT_EQ(b, firstHalf); +} - // Check copy constructors and copy assignments: - { - Buffer x{b0}; - EXPECT_EQ(x, b0); - EXPECT_TRUE(sane(x)); - Buffer y{b1}; - EXPECT_EQ(y, b1); - EXPECT_TRUE(sane(y)); - x = b2; - EXPECT_EQ(x, b2); - EXPECT_TRUE(sane(x)); - x = y; - EXPECT_EQ(x, y); - EXPECT_TRUE(sane(x)); - y = b3; - EXPECT_EQ(y, b3); - EXPECT_TRUE(sane(y)); - x = b0; - EXPECT_EQ(x, b0); - EXPECT_TRUE(sane(x)); +TEST_F(BufferTest, sized_construction_reserves_without_filling) +{ + Buffer b{kHalf}; + + EXPECT_TRUE(sane(b)); + EXPECT_FALSE(b.empty()); + EXPECT_EQ(b.size(), kHalf); + + std::memcpy(b.data(), kRandomData.data() + kHalf, kHalf); + EXPECT_EQ(b, secondHalf); +} + +TEST_F(BufferTest, construction_copies_raw_memory) +{ + Buffer const b{kRandomData.data(), kRandomData.size()}; + + EXPECT_TRUE(sane(b)); + EXPECT_FALSE(b.empty()); + EXPECT_EQ(b.size(), kRandomData.size()); + EXPECT_EQ(std::memcmp(b.data(), kRandomData.data(), b.size()), 0); +} + +TEST_F(BufferTest, equality_compares_contents) +{ + // Uses EXPECT_TRUE rather than EXPECT_EQ/EXPECT_NE because the operators are what is under test + // here. + EXPECT_TRUE(emptyBuffer == emptyBuffer); + EXPECT_TRUE(firstHalf == firstHalf); + + EXPECT_TRUE(emptyBuffer != firstHalf); + EXPECT_TRUE(firstHalf != secondHalf); + EXPECT_TRUE(secondHalf != whole); +} + +TEST_F(BufferTest, copy_construction) +{ + Buffer const fromEmpty{emptyBuffer}; + EXPECT_TRUE(sane(fromEmpty)); + EXPECT_EQ(fromEmpty, emptyBuffer); + + Buffer const fromNonEmpty{firstHalf}; + EXPECT_TRUE(sane(fromNonEmpty)); + EXPECT_EQ(fromNonEmpty, firstHalf); +} + +TEST_F(BufferTest, copy_assignment) +{ + Buffer b{emptyBuffer}; + + // empty <- non-empty + b = secondHalf; + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, secondHalf); + + // non-empty <- non-empty of a different size + b = whole; + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, whole); + + // non-empty <- empty + b = emptyBuffer; + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, emptyBuffer); +} + +TEST_F(BufferTest, self_assignment_preserves_contents) +{ #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wself-assign-overloaded" #endif - x = x; - EXPECT_EQ(x, b0); - EXPECT_TRUE(sane(x)); - y = y; - EXPECT_EQ(y, b3); - EXPECT_TRUE(sane(y)); + Buffer emptyCopy{emptyBuffer}; + emptyCopy = emptyCopy; + EXPECT_TRUE(sane(emptyCopy)); + EXPECT_EQ(emptyCopy, emptyBuffer); + + Buffer wholeCopy{whole}; + wholeCopy = wholeCopy; + EXPECT_TRUE(sane(wholeCopy)); + EXPECT_EQ(wholeCopy, whole); #ifdef __clang__ #pragma clang diagnostic pop #endif - } +} - // Check move constructor & move assignments: +TEST_F(BufferTest, move_construct_from_empty) +{ + Buffer source; + Buffer const moved{std::move(source)}; + + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(moved)); + EXPECT_TRUE(moved.empty()); +} + +TEST_F(BufferTest, move_construct_from_non_empty) +{ + Buffer source{firstHalf}; + Buffer const moved{std::move(source)}; + + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(sane(moved)); + EXPECT_EQ(moved, firstHalf); +} + +TEST_F(BufferTest, move_assign_empty_to_empty) +{ + Buffer target; + Buffer source; + + target = std::move(source); + + EXPECT_TRUE(sane(target)); + EXPECT_TRUE(target.empty()); + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, move_assign_non_empty_to_empty) +{ + Buffer target; + Buffer source{firstHalf}; + + target = std::move(source); + + EXPECT_TRUE(sane(target)); + EXPECT_EQ(target, firstHalf); + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, move_assign_empty_to_non_empty) +{ + Buffer target{firstHalf}; + Buffer source; + + target = std::move(source); + + EXPECT_TRUE(sane(target)); + EXPECT_TRUE(target.empty()); + checkEmptyAfterMove(source); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, move_assign_non_empty_to_non_empty) +{ + Buffer target{firstHalf}; + Buffer sameSize{secondHalf}; + Buffer largerSize{whole}; + + target = std::move(sameSize); + EXPECT_TRUE(sane(target)); + EXPECT_EQ(target, secondHalf); + checkEmptyAfterMove(sameSize); // NOLINT(bugprone-use-after-move) + + target = std::move(largerSize); + EXPECT_TRUE(sane(target)); + EXPECT_EQ(target, whole); + checkEmptyAfterMove(largerSize); // NOLINT(bugprone-use-after-move) +} + +TEST_F(BufferTest, construction_from_slice) +{ + Buffer const fromEmpty{static_cast(emptyBuffer)}; + EXPECT_TRUE(sane(fromEmpty)); + EXPECT_EQ(fromEmpty, emptyBuffer); + + Buffer const fromNonEmpty{static_cast(whole)}; + EXPECT_TRUE(sane(fromNonEmpty)); + EXPECT_EQ(fromNonEmpty, whole); +} + +TEST_F(BufferTest, assignment_from_slice) +{ + Buffer b; + + // empty <- empty slice + b = static_cast(emptyBuffer); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, emptyBuffer); + + // empty <- non-empty slice + b = static_cast(firstHalf); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, firstHalf); + + // non-empty <- non-empty slice + b = static_cast(secondHalf); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, secondHalf); + + // non-empty <- empty slice + b = static_cast(emptyBuffer); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b, emptyBuffer); +} + +TEST_F(BufferTest, resize_allocates_and_clear_releases) +{ + auto check = [](Buffer const& original, std::size_t size) { + SCOPED_TRACE(::testing::Message() << "size: " << size); + + Buffer b{original}; + + // Resizing to zero is equivalent to clearing. + b(size); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b.size(), size); + EXPECT_EQ(b.data() == nullptr, size == 0); + + b(size + 1); + EXPECT_TRUE(sane(b)); + EXPECT_EQ(b.size(), size + 1); + EXPECT_NE(b.data(), nullptr); + + b.clear(); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); + EXPECT_EQ(b.data(), nullptr); + + // clear() is idempotent. + b.clear(); + EXPECT_TRUE(sane(b)); + EXPECT_TRUE(b.empty()); + EXPECT_EQ(b.data(), nullptr); + }; + + for (auto size = 0uz; size < kHalf; ++size) { - static_assert(std::is_nothrow_move_constructible_v); - static_assert(std::is_nothrow_move_assignable_v); - - { // Move-construct from empty buf - Buffer x; - Buffer const y{std::move(x)}; - EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(sane(y)); - EXPECT_TRUE(y.empty()); - EXPECT_EQ(x, y); // NOLINT(bugprone-use-after-move) - } - - { // Move-construct from non-empty buf - Buffer x{b1}; - Buffer const y{std::move(x)}; - EXPECT_TRUE(sane(x)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(x.empty()); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(sane(y)); - EXPECT_EQ(y, b1); - } - - { // Move assign empty buf to empty buf - Buffer x; - Buffer y; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to empty buf - Buffer x; - Buffer y{b1}; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x, b1); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign empty buf to non-empty buf - Buffer x{b1}; - Buffer y; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - } - - { // Move assign non-empty buf to non-empty buf - Buffer x{b1}; - Buffer y{b2}; - Buffer z{b3}; - - x = std::move(y); - EXPECT_TRUE(sane(x)); - EXPECT_FALSE(x.empty()); - EXPECT_TRUE(sane(y)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(y.empty()); // NOLINT(bugprone-use-after-move) - - x = std::move(z); - EXPECT_TRUE(sane(x)); - EXPECT_FALSE(x.empty()); - EXPECT_TRUE(sane(z)); // NOLINT(bugprone-use-after-move) - EXPECT_TRUE(z.empty()); // NOLINT(bugprone-use-after-move) - } - } - - { - Buffer w{static_cast(b0)}; - EXPECT_TRUE(sane(w)); - EXPECT_EQ(w, b0); - - Buffer x{static_cast(b1)}; - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x, b1); - - Buffer y{static_cast(b2)}; - EXPECT_TRUE(sane(y)); - EXPECT_EQ(y, b2); - - Buffer z{static_cast(b3)}; - EXPECT_TRUE(sane(z)); - EXPECT_EQ(z, b3); - - // Assign empty slice to empty buffer - w = static_cast(b0); - EXPECT_TRUE(sane(w)); - EXPECT_EQ(w, b0); - - // Assign non-empty slice to empty buffer - w = static_cast(b1); - EXPECT_TRUE(sane(w)); - EXPECT_EQ(w, b1); - - // Assign non-empty slice to non-empty buffer - x = static_cast(b2); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x, b2); - - // Assign non-empty slice to non-empty buffer - y = static_cast(z); - EXPECT_TRUE(sane(y)); - EXPECT_EQ(y, z); - - // Assign empty slice to non-empty buffer: - z = static_cast(b0); - EXPECT_TRUE(sane(z)); - EXPECT_EQ(z, b0); - } - - { - auto test = [](Buffer const& b, std::size_t i) { - Buffer x{b}; - - // Try to allocate some number of bytes, possibly - // zero (which means clear) and sanity check - x(i); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x.size(), i); - EXPECT_EQ((x.data() == nullptr), (i == 0)); - - // Try to allocate some more data (always non-zero) - x(i + 1); - EXPECT_TRUE(sane(x)); - EXPECT_EQ(x.size(), i + 1); - EXPECT_NE(x.data(), nullptr); - - // Try to clear: - x.clear(); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_EQ(x.data(), nullptr); - - // Try to clear again: - x.clear(); - EXPECT_TRUE(sane(x)); - EXPECT_TRUE(x.empty()); - EXPECT_EQ(x.data(), nullptr); - }; - - for (std::size_t i = 0; i < 16; ++i) - { - test(b0, i); - test(b1, i); - } + check(emptyBuffer, size); + check(firstHalf, size); } } +TEST_F(BufferTest, fill_sets_every_byte) +{ + Buffer b{4}; + b.fill(0xab); + + EXPECT_EQ(b.size(), 4); + for (auto const byte : Slice{b}) + EXPECT_EQ(byte, 0xab); +} + +TEST_F(BufferTest, fill_overwrites_and_keeps_size) +{ + Buffer b{4}; + b.fill(0xab); + b.fill(0x00); + + EXPECT_EQ(b.size(), 4); + for (auto const byte : Slice{b}) + EXPECT_EQ(byte, 0x00); +} + +TEST_F(BufferTest, fill_on_empty_buffer_is_a_noop) +{ + Buffer empty; + empty.fill(0xff); + + EXPECT_TRUE(empty.empty()); + EXPECT_EQ(empty.data(), nullptr); +} + } // namespace xrpl::test diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp index b9f8930b7b..c6c9fcfef0 100644 --- a/src/tests/libxrpl/basics/IntrusiveShared.cpp +++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp @@ -92,6 +92,7 @@ public: static constexpr std::size_t kMaxStates = 128; static std::array, kMaxStates> state; static std::atomic nextId; + static TrackedState getState(std::size_t id) { @@ -100,13 +101,12 @@ public: return state[id].load(std::memory_order_acquire); } + static void resetStates(bool resetCallback) { for (std::size_t i = 0; i < kMaxStates; ++i) - { state[i].store(TrackedState::Uninitialized, std::memory_order_release); - } nextId.store(0, std::memory_order_release); if (resetCallback) TIBase::tracingCallback = [](TrackedState, std::optional) {}; @@ -120,6 +120,7 @@ public: { TIBase::resetStates(resetCallback); } + ~ResetStatesGuard() { TIBase::resetStates(resetCallback); @@ -130,6 +131,7 @@ public: { state[id].store(TrackedState::Alive, std::memory_order_relaxed); } + ~TIBase() override { using enum TrackedState; @@ -218,9 +220,7 @@ TEST(IntrusiveSharedTest, basics) EXPECT_EQ(TIBase::getState(id), Alive); EXPECT_EQ(b->useCount(), 1); for (auto i = 0uz; i < 10; ++i) - { strong.push_back(b); - } b.reset(); EXPECT_EQ(TIBase::getState(id), Alive); strong.resize(strong.size() - 1); @@ -244,8 +244,7 @@ TEST(IntrusiveSharedTest, basics) EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); while (!weak.empty()) { - weak.resize(weak.size() - 1); - if (!weak.empty()) + if (weak.resize(weak.size() - 1); !weak.empty()) { EXPECT_EQ(TIBase::getState(id), PartiallyDeleted); } diff --git a/src/tests/libxrpl/basics/base_uint.cpp b/src/tests/libxrpl/basics/base_uint.cpp index 10795f4563..969705b5b7 100644 --- a/src/tests/libxrpl/basics/base_uint.cpp +++ b/src/tests/libxrpl/basics/base_uint.cpp @@ -6,6 +6,7 @@ #include +#include #include #include @@ -205,125 +206,119 @@ TEST_F(BaseUintTest, base_uint) Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; EXPECT_EQ(BaseUInt96::kBytes, raw.size()); - BaseUInt96 u = BaseUInt96::fromRaw(raw); - uset.insert(u); - EXPECT_EQ(raw.size(), u.size()); - EXPECT_EQ(to_string(u), "0102030405060708090A0B0C"); - EXPECT_EQ(toShortString(u), "01020304..."); - EXPECT_EQ(*u.data(), 1); - EXPECT_EQ(u.signum(), 1); - EXPECT_FALSE(!u); - EXPECT_FALSE(u.isZero()); - EXPECT_TRUE(u.isNonZero()); - unsigned char t = 0; - for (auto& d : u) - { - EXPECT_EQ(d, ++t); - } + BaseUInt96 ascending = BaseUInt96::fromRaw(raw); + uset.insert(ascending); + EXPECT_EQ(raw.size(), ascending.size()); + EXPECT_EQ(to_string(ascending), "0102030405060708090A0B0C"); + EXPECT_EQ(toShortString(ascending), "01020304..."); + EXPECT_EQ(*ascending.data(), 1); + EXPECT_EQ(ascending.signum(), 1); + EXPECT_FALSE(!ascending); + EXPECT_FALSE(ascending.isZero()); + EXPECT_TRUE(ascending.isNonZero()); + unsigned char expectedByte = 0; + for (auto& byte : ascending) + EXPECT_EQ(byte, ++expectedByte); - // Test hash_append by "hashing" with a no-op hasher (h) + // Test hash_append by "hashing" with a no-op hasher (hasher) // and then extracting the bytes that were written during hashing - // back into another base_uint (w) for comparison with the original - Nonhash<96> h{}; - hash_append(h, u); - BaseUInt96 const w = - BaseUInt96::fromRaw(std::vector(h.data.begin(), h.data.end())); - EXPECT_EQ(w, u); + // back into another base_uint (rehashed) for comparison with the original + Nonhash<96> hasher{}; + hash_append(hasher, ascending); + BaseUInt96 const rehashed = + BaseUInt96::fromRaw(std::vector(hasher.data.begin(), hasher.data.end())); + EXPECT_EQ(rehashed, ascending); - BaseUInt96 v{~u}; - uset.insert(v); - EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3"); - EXPECT_EQ(toShortString(v), "FEFDFCFB..."); - EXPECT_EQ(*v.data(), 0xfe); - EXPECT_EQ(v.signum(), 1); - EXPECT_FALSE(!v); - EXPECT_FALSE(v.isZero()); - EXPECT_TRUE(v.isNonZero()); + BaseUInt96 complement{~ascending}; + uset.insert(complement); + EXPECT_EQ(to_string(complement), "FEFDFCFBFAF9F8F7F6F5F4F3"); + EXPECT_EQ(toShortString(complement), "FEFDFCFB..."); + EXPECT_EQ(*complement.data(), 0xfe); + EXPECT_EQ(complement.signum(), 1); + EXPECT_FALSE(!complement); + EXPECT_FALSE(complement.isZero()); + EXPECT_TRUE(complement.isNonZero()); - t = 0xff; - for (auto& d : v) - { - EXPECT_EQ(d, --t); - } + expectedByte = 0xff; + for (auto& byte : complement) + EXPECT_EQ(byte, --expectedByte); - EXPECT_LT(u, v); - EXPECT_GT(v, u); + EXPECT_LT(ascending, complement); + EXPECT_GT(complement, ascending); - v = u; - EXPECT_EQ(v, u); + complement = ascending; + EXPECT_EQ(complement, ascending); - BaseUInt96 z{beast::kZero}; - uset.insert(z); - EXPECT_EQ(to_string(z), "000000000000000000000000"); - EXPECT_EQ(toShortString(z), "00000000..."); - EXPECT_EQ(*z.data(), 0); - EXPECT_EQ(*z.begin(), 0); - EXPECT_EQ(*std::prev(z.end(), 1), 0); - EXPECT_EQ(z.signum(), 0); - EXPECT_TRUE(!z); - EXPECT_TRUE(z.isZero()); - EXPECT_FALSE(z.isNonZero()); - for (auto& d : z) - { - EXPECT_EQ(d, 0); - } + BaseUInt96 zero{beast::kZero}; + uset.insert(zero); + EXPECT_EQ(to_string(zero), "000000000000000000000000"); + EXPECT_EQ(toShortString(zero), "00000000..."); + EXPECT_EQ(*zero.data(), 0); + EXPECT_EQ(*zero.begin(), 0); + EXPECT_EQ(*std::prev(zero.end(), 1), 0); + EXPECT_EQ(zero.signum(), 0); + EXPECT_TRUE(!zero); + EXPECT_TRUE(zero.isZero()); + EXPECT_FALSE(zero.isNonZero()); + for (auto& byte : zero) + EXPECT_EQ(byte, 0); { // There are several ways to create a zero. beast::kZero is tested above. Test some // others. - BaseUInt96 const z1; - EXPECT_EQ(z1, z) << to_string(z1); + BaseUInt96 const defaultZero; + EXPECT_EQ(defaultZero, zero) << to_string(defaultZero); - BaseUInt96 const z2{}; - EXPECT_EQ(z2, z) << to_string(z2); + BaseUInt96 const bracedZero{}; + EXPECT_EQ(bracedZero, zero) << to_string(bracedZero); - BaseUInt96 const z3{0u}; - EXPECT_EQ(z3, z) << to_string(z3); + BaseUInt96 const zeroFromUInt{0u}; + EXPECT_EQ(zeroFromUInt, zero) << to_string(zeroFromUInt); } - BaseUInt96 n{z}; - n++; - EXPECT_EQ(n, BaseUInt96(1)); - n--; - EXPECT_EQ(n, beast::kZero); - EXPECT_EQ(n, z); - n--; - EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF"); - EXPECT_EQ(toShortString(n), "FFFFFFFF..."); - n = beast::kZero; - EXPECT_EQ(n, z); + BaseUInt96 counter{zero}; + counter++; + EXPECT_EQ(counter, BaseUInt96(1)); + counter--; + EXPECT_EQ(counter, beast::kZero); + EXPECT_EQ(counter, zero); + counter--; + EXPECT_EQ(to_string(counter), "FFFFFFFFFFFFFFFFFFFFFFFF"); + EXPECT_EQ(toShortString(counter), "FFFFFFFF..."); + counter = beast::kZero; + EXPECT_EQ(counter, zero); - BaseUInt96 zp1{z}; - zp1++; - BaseUInt96 zm1{z}; - zm1--; - BaseUInt96 const x{zm1 ^ zp1}; - uset.insert(x); - EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x); - EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x); + BaseUInt96 zeroPlusOne{zero}; + zeroPlusOne++; + BaseUInt96 zeroMinusOne{zero}; + zeroMinusOne--; + BaseUInt96 const xored{zeroMinusOne ^ zeroPlusOne}; + uset.insert(xored); + EXPECT_EQ(to_string(xored), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(xored); + EXPECT_EQ(toShortString(xored), "FFFFFFFF...") << toShortString(xored); EXPECT_EQ(uset.size(), 4); - BaseUInt96 tmp; - EXPECT_TRUE(tmp.parseHex(to_string(u))); - EXPECT_EQ(tmp, u); - tmp = z; + BaseUInt96 parsed; + EXPECT_TRUE(parsed.parseHex(to_string(ascending))); + EXPECT_EQ(parsed, ascending); + parsed = zero; // fails with extra char - EXPECT_FALSE(tmp.parseHex("A" + to_string(u))); - tmp = z; + EXPECT_FALSE(parsed.parseHex("A" + to_string(ascending))); + parsed = zero; // fails with extra char at end - EXPECT_FALSE(tmp.parseHex(to_string(u) + "A")); + EXPECT_FALSE(parsed.parseHex(to_string(ascending) + "A")); // fails with a non-hex character at some point in the string: - tmp = z; + parsed = zero; for (std::size_t i = 0; i != 24; ++i) { - std::string x = to_string(z); - x[i] = ('G' + (i % 10)); - EXPECT_FALSE(tmp.parseHex(x)); + std::string xored = to_string(zero); + xored[i] = ('G' + (i % 10)); + EXPECT_FALSE(parsed.parseHex(xored)); } // Walking 1s: @@ -332,8 +327,8 @@ TEST_F(BaseUintTest, base_uint) std::string s1 = "000000000000000000000000"; s1[i] = '1'; - EXPECT_TRUE(tmp.parseHex(s1)); - EXPECT_EQ(to_string(tmp), s1); + EXPECT_TRUE(parsed.parseHex(s1)); + EXPECT_EQ(to_string(parsed), s1); } // Walking 0s: @@ -342,8 +337,8 @@ TEST_F(BaseUintTest, base_uint) std::string s1 = "111111111111111111111111"; s1[i] = '0'; - EXPECT_TRUE(tmp.parseHex(s1)); - EXPECT_EQ(to_string(tmp), s1); + EXPECT_TRUE(parsed.parseHex(s1)); + EXPECT_EQ(to_string(parsed), s1); } // Constexpr constructors @@ -357,39 +352,27 @@ TEST_F(BaseUintTest, base_uint) // Using the constexpr constructor in a non-constexpr context // with an error in the parsing throws an exception. { - // Invalid length for string. - bool caught = false; - try - { - // Try to prevent constant evaluation. - std::vector str(23, '7'); + // Invalid length for string. The vector keeps this out of a constant + // expression, so the constructor throws instead of failing to compile. + auto tooShort = [] { + std::vector const str(23, '7'); std::string_view const sView(str.data(), str.size()); [[maybe_unused]] BaseUInt96 const t96(sView); - } - catch (std::invalid_argument const& e) - { - EXPECT_EQ(e.what(), std::string("invalid length for hex string")); - caught = true; - } - EXPECT_TRUE(caught); + }; + EXPECT_THAT( + tooShort, + ::testing::ThrowsMessage("invalid length for hex string")); } { // Invalid character in string. - bool caught = false; - try - { - // Try to prevent constant evaluation. + auto badCharacter = [] { std::vector str(23, '7'); str.push_back('G'); std::string_view const sView(str.data(), str.size()); [[maybe_unused]] BaseUInt96 const t96(sView); - } - catch (std::range_error const& e) - { - EXPECT_EQ(e.what(), std::string("invalid hex character")); - caught = true; - } - EXPECT_TRUE(caught); + }; + EXPECT_THAT( + badCharacter, ::testing::ThrowsMessage("invalid hex character")); } // Verify that constexpr base_uints interpret a string the same @@ -412,11 +395,11 @@ TEST_F(BaseUintTest, base_uint) "fFfFfFfFfFfFfFfFfFfFfFfF", }); - for (StrBaseUInt const& t : kTestCases) + for (StrBaseUInt const& expectedByte : kTestCases) { BaseUInt96 t96; - EXPECT_TRUE(t96.parseHex(t.str)); - EXPECT_EQ(t96, t.tst); + EXPECT_TRUE(t96.parseHex(expectedByte.str)); + EXPECT_EQ(t96, expectedByte.tst); } } } diff --git a/src/tests/libxrpl/shamap/SHAMap.cpp b/src/tests/libxrpl/shamap/SHAMap.cpp index e662e16be4..c84cdf504f 100644 --- a/src/tests/libxrpl/shamap/SHAMap.cpp +++ b/src/tests/libxrpl/shamap/SHAMap.cpp @@ -16,7 +16,6 @@ #include #include -#include #include #include #include @@ -113,7 +112,7 @@ protected: intToVuc(std::uint8_t v) { Buffer vuc{32}; - std::fill_n(vuc.data(), vuc.size(), v); + vuc.fill(v); return vuc; } }; From c74724a7197da3db16cc1c7274543c7ba4ce2dbc Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 11 Aug 2026 12:44:01 +0000 Subject: [PATCH 64/68] build: Reimagine linker warnings in different scenarios (#7974) --- cmake/XrplCompiler.cmake | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cmake/XrplCompiler.cmake b/cmake/XrplCompiler.cmake index e262acf1c9..21566add01 100644 --- a/cmake/XrplCompiler.cmake +++ b/cmake/XrplCompiler.cmake @@ -188,6 +188,32 @@ else() endif() endif() +# Linker warnings are errors where we control the toolchain and the dependencies: CI and the Nix dev shell. +# On non-Nix macOS we suppress the deployment target warning: an old Conan profile may not pin os.version. +if(is_macos OR is_linux) + if(is_ci OR is_nix_compiler) + if(is_macos) + set(fatal_warnings_flag "-Wl,-fatal_warnings") + else() + set(fatal_warnings_flag "-Wl,--fatal-warnings") + endif() + message( + STATUS + "Treating all linker warnings as errors (${fatal_warnings_flag})" + ) + target_link_options(common INTERFACE "${fatal_warnings_flag}") + unset(fatal_warnings_flag) + elseif(is_macos) + set(silence_flag "-Wl,-deployment_target_mismatches,suppress") + message( + STATUS + "Silencing macOS deployment target mismatch warnings (${silence_flag})" + ) + target_link_options(common INTERFACE "${silence_flag}") + unset(silence_flag) + endif() +endif() + # Antithesis instrumentation will only be built and deployed using machines running Linux. if(voidstar) if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") From d43e5acaa70f8d45a1691ab522a9c84c4fc95006 Mon Sep 17 00:00:00 2001 From: luisfernandomendozav <109832400+luisfernandomendozav@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:23:07 +0000 Subject: [PATCH 65/68] fix: Validate account/ident type in gateway_balances (#7655) --- API-CHANGELOG.md | 1 + src/test/rpc/GatewayBalances_test.cpp | 40 +++++++++++++++++++ .../rpc/handlers/account/GatewayBalances.cpp | 6 +++ 3 files changed, 47 insertions(+) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index bc3672588e..c853cfb07c 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -54,6 +54,7 @@ This section contains changes targeting a future version. - `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529) - `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529) - Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529) +- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655) - `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728) ## XRP Ledger server version 3.1.0 diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp index 106b9b5f1a..91d9126f61 100644 --- a/src/test/rpc/GatewayBalances_test.cpp +++ b/src/test/rpc/GatewayBalances_test.cpp @@ -176,6 +176,45 @@ public: }); } + void + testGWBInvalidAccount(FeatureBitset features) + { + testcase("Gateway Balances with non-string account/ident"); + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this, features); + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + auto wsc = makeWSClient(env.app().config()); + + // A non-string "account" must be rejected cleanly with invalidParams + // rather than throwing a Json::LogicError that surfaces as internal. + json::Value qry; + qry[jss::account] = 42; + qry[jss::hotwallet] = alice.human(); + + forAllApiVersions([&, this](unsigned apiVersion) { + qry[jss::api_version] = apiVersion; + auto jv = wsc->invoke("gateway_balances", qry); + expect(jv[jss::status] == "error"); + BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams"); + }); + + // The same applies to a non-string "ident". + json::Value qry2; + qry2[jss::ident] = 42; + + forAllApiVersions([&, this](unsigned apiVersion) { + qry2[jss::api_version] = apiVersion; + auto jv = wsc->invoke("gateway_balances", qry2); + expect(jv[jss::status] == "error"); + BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams"); + }); + } + void testGWBOverflow() { @@ -280,6 +319,7 @@ public: { testGWB(feature); testGWBApiVersions(feature); + testGWBInvalidAccount(feature); } testGWBWithMPT(); testGWBOverflow(); diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index ff19d1d1e5..041e878a3f 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -63,6 +63,12 @@ doGatewayBalances(rpc::JsonContext& context) if (!(params.isMember(jss::account) || params.isMember(jss::ident))) return rpc::missingFieldError(jss::account); + if (params.isMember(jss::account) && !params[jss::account].isString()) + return rpc::invalidFieldError(jss::account); + + if (params.isMember(jss::ident) && !params[jss::ident].isString()) + return rpc::invalidFieldError(jss::ident); + std::string const strIdent( params.isMember(jss::account) ? params[jss::account].asString() : params[jss::ident].asString()); From a3147740f2f610f0810a6e4eb56aa7cbe4c5cc12 Mon Sep 17 00:00:00 2001 From: klemenfn <102049210+klemenfn@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:24:56 +0000 Subject: [PATCH 66/68] build: Fix GCC 14 compilation (#7981) Co-authored-by: Ayaz Salikhov --- include/xrpl/json/json_value.h | 2 ++ src/xrpld/rpc/detail/RPCLedgerHelpers.cpp | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index be126d8b8e..57936a774f 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -524,6 +525,7 @@ public: class ValueIteratorBase { public: + using iterator_category = std::bidirectional_iterator_tag; using size_t = unsigned int; using difference_type = int; using SelfType = ValueIteratorBase; diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp index 52e68e87f1..19fe294924 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp @@ -331,6 +331,13 @@ getLedger<>(std::shared_ptr&, LedgerShortcut shortcut, Context c template Status getLedger<>(std::shared_ptr&, uint256 const&, Context const&); +// explicit instantiation of ledgerFromSpecifier +template Status +ledgerFromSpecifier<>( + std::shared_ptr&, + org::xrpl::rpc::v1::LedgerSpecifier const&, + Context const&); + // The previous version of the lookupLedger command would accept the // "ledger_index" argument as a string and silently treat it as a request to // return the current ledger which, while not strictly wrong, could cause a lot From 6ca2fb84d4da09d01af6d1f233147374599aa440 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Tue, 11 Aug 2026 18:15:35 +0000 Subject: [PATCH 67/68] refactor: Replace Boost trim and to_lower with libxrpl helpers (#7995) --- include/xrpl/basics/StringUtilities.h | 22 ++++++++++ src/libxrpl/basics/StringUtilities.cpp | 40 +++++++++++++++++-- src/libxrpl/crypto/RFC1751.cpp | 4 +- src/libxrpl/server/Manifest.cpp | 6 +-- src/libxrpl/server/Port.cpp | 4 +- src/tests/libxrpl/basics/StringUtilities.cpp | 40 +++++++++++++++++++ src/xrpld/app/main/GRPCServer.cpp | 4 +- src/xrpld/app/main/Main.cpp | 6 +-- src/xrpld/core/detail/Config.cpp | 3 +- src/xrpld/rpc/detail/ServerHandler.cpp | 4 +- .../rpc/handlers/account/AccountInfo.cpp | 4 +- .../rpc/handlers/admin/data/CanDelete.cpp | 5 +-- .../server_info/ServerDefinitions.cpp | 4 +- 13 files changed, 118 insertions(+), 28 deletions(-) diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 2b360d2fda..d606613c65 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -125,9 +125,31 @@ struct ParsedUrl bool parseUrl(ParsedUrl& pUrl, std::string const& strUrl); +/** + * Remove leading and trailing ASCII whitespace. + * + * Whitespace is the fixed set " \t\n\v\f\r"; the current locale is not + * consulted, so the result depends only on the input. + * + * @param str The string to trim. + * @return @p str without leading or trailing whitespace. + */ std::string trimWhitespace(std::string str); +/** + * Fold ASCII upper case letters to lower case. + * + * Only 'A' through 'Z' are remapped; every other byte is left alone and the + * current locale is not consulted, so the result depends only on the input. + * + * @param str The string to fold. + * @return @p str with each ASCII upper case letter replaced by its lower case + * equivalent. + */ +std::string +toLower(std::string str); + std::optional toUInt64(std::string const& s); diff --git a/src/libxrpl/basics/StringUtilities.cpp b/src/libxrpl/basics/StringUtilities.cpp index 2b7deecb8e..9eb1bff995 100644 --- a/src/libxrpl/basics/StringUtilities.cpp +++ b/src/libxrpl/basics/StringUtilities.cpp @@ -5,15 +5,15 @@ #include #include -#include -#include #include #include #include +#include #include #include #include +#include #include #include @@ -67,7 +67,7 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) } pUrl.scheme = smMatch[1]; - boost::algorithm::to_lower(pUrl.scheme); + pUrl.scheme = toLower(pUrl.scheme); pUrl.username = smMatch[2]; pUrl.password = smMatch[3]; std::string const domain = smMatch[4]; @@ -93,10 +93,42 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) return true; } +namespace { + +// Deliberately not std::isspace / std::tolower: those consult the current C +// locale, so the same input could trim or fold differently depending on +// process-wide state set by something else entirely. Everything these helpers +// are used on (config keys and values, URL schemes, hex digests) is ASCII, and +// the callers want a fixed answer, so spell the ASCII rules out. + +constexpr bool +isAsciiSpace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +constexpr char +toAsciiLower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; +} + +} // namespace + std::string trimWhitespace(std::string str) { - boost::trim(str); + auto const end = std::ranges::find_if_not(str | std::views::reverse, isAsciiSpace).base(); + str.erase(end, str.end()); + str.erase(str.begin(), std::ranges::find_if_not(str, isAsciiSpace)); + + return str; +} + +std::string +toLower(std::string str) +{ + std::ranges::transform(str, str.begin(), toAsciiLower); return str; } diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp index 4b17e1443c..f6342928ab 100644 --- a/src/libxrpl/crypto/RFC1751.cpp +++ b/src/libxrpl/crypto/RFC1751.cpp @@ -1,11 +1,11 @@ #include +#include #include #include #include #include -#include #include #include @@ -397,7 +397,7 @@ RFC1751::getKeyFromEnglish(std::string& strKey, std::string const& strHuman) std::string strTrimmed(strHuman); - boost::algorithm::trim(strTrimmed); + strTrimmed = trimWhitespace(strTrimmed); boost::algorithm::split( vWords, strTrimmed, boost::algorithm::is_space(), boost::algorithm::token_compress_on); diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index 0760196a3b..c85c8445f0 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -23,8 +23,6 @@ #include #include -#include - #include #include #include @@ -277,7 +275,7 @@ loadValidatorToken(std::vector const& blob, beast::Journal journal) [](std::size_t init, std::string const& s) { return init + s.size(); })); for (auto const& line : blob) - tokenStr += boost::algorithm::trim_copy(line); + tokenStr += trimWhitespace(line); tokenStr = base64Decode(tokenStr); @@ -653,7 +651,7 @@ ManifestCache::load( [](std::size_t init, std::string const& s) { return init + s.size(); })); for (auto const& line : configRevocation) - revocationStr += boost::algorithm::trim_copy(line); + revocationStr += trimWhitespace(line); auto mo = deserializeManifest(base64Decode(revocationStr)); diff --git a/src/libxrpl/server/Port.cpp b/src/libxrpl/server/Port.cpp index 694d4448d5..a7892bc0e8 100644 --- a/src/libxrpl/server/Port.cpp +++ b/src/libxrpl/server/Port.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -9,7 +10,6 @@ #include #include -#include #include #include #include @@ -98,7 +98,7 @@ populate( while (std::getline(ss, ip, ',')) { - boost::algorithm::trim(ip); + ip = trimWhitespace(ip); bool v4 = false; boost::asio::ip::network_v4 v4Net; boost::asio::ip::network_v6 v6Net; diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp index a10711abdb..0180e25db0 100644 --- a/src/tests/libxrpl/basics/StringUtilities.cpp +++ b/src/tests/libxrpl/basics/StringUtilities.cpp @@ -290,4 +290,44 @@ TEST_F(StringUtilitiesTest, to_string) EXPECT_EQ(result, "hello"); } +TEST_F(StringUtilitiesTest, trimWhitespace) +{ + EXPECT_EQ(trimWhitespace(""), ""); + EXPECT_EQ(trimWhitespace(" "), ""); + EXPECT_EQ(trimWhitespace("abc"), "abc"); + EXPECT_EQ(trimWhitespace(" abc"), "abc"); + EXPECT_EQ(trimWhitespace("abc "), "abc"); + EXPECT_EQ(trimWhitespace(" \t\n\v\f\r abc \t\n\v\f\r "), "abc"); + + // Interior whitespace is preserved. + EXPECT_EQ(trimWhitespace(" a b\tc "), "a b\tc"); +} + +TEST_F(StringUtilitiesTest, toLower) +{ + EXPECT_EQ(toLower(""), ""); + EXPECT_EQ(toLower("ABC"), "abc"); + EXPECT_EQ(toLower("AbC123"), "abc123"); + EXPECT_EQ(toLower("already lower"), "already lower"); + + // Only 'A'-'Z' are remapped. Neighbouring punctuation and digits, which a + // buggy range check could catch, must survive untouched. + EXPECT_EQ(toLower("@[`{_^"), "@[`{_^"); +} + +// Both helpers are documented as depending only on their input. Guard that by +// checking the bytes just outside ASCII, which a locale-aware isspace/tolower +// could classify differently. +TEST_F(StringUtilitiesTest, trimAndLowerIgnoreLocale) +{ + // 0xA0 is NO-BREAK SPACE in Latin-1 and is whitespace to some locales. + std::string const nbsp("\xA0", 1); + EXPECT_EQ(trimWhitespace(nbsp), nbsp); + EXPECT_EQ(trimWhitespace(" " + nbsp + " "), nbsp); + + // 0xC0 is LATIN CAPITAL LETTER A WITH GRAVE in Latin-1. + std::string const agrave("\xC0", 1); + EXPECT_EQ(toLower(agrave), agrave); +} + } // namespace xrpl diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index 1b20ff1d49..fc4a9794bd 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -24,7 +25,6 @@ #include #include -#include #include #include #include @@ -371,7 +371,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app) std::string ip; while (std::getline(ss, ip, ',')) { - boost::algorithm::trim(ip); + ip = trimWhitespace(ip); auto const addr = boost::asio::ip::make_address(ip); if (addr.is_unspecified()) diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index a23b84f2e8..ba6520db5f 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include -#include #include #include // IWYU pragma: keep #include @@ -211,7 +211,7 @@ public: boost::split(v, patterns, boost::algorithm::is_any_of(",")); selectors_.reserve(v.size()); std::ranges::for_each(v, [this](std::string s) { - boost::trim(s); + s = trimWhitespace(s); if (selectors_.empty() || !s.empty()) selectors_.emplace_back(beast::unit_test::Selector::ModeT::Automatch, s); }); @@ -614,7 +614,7 @@ run(int argc, char** argv) std::vector result; for (auto& s : strVec) { - boost::trim(s); + s = trimWhitespace(s); if (!s.empty()) result.push_back(std::stoi(s)); } diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index e93ccec56e..f263fb49ab 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -185,7 +184,7 @@ parseIniFile(std::string const& strInput, bool const bTrim) for (auto& strValue : vLines) { if (bTrim) - boost::algorithm::trim(strValue); + strValue = trimWhitespace(strValue); if (strValue.empty() || strValue[0] == '#') { diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 0181d5b10f..827d8705fd 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -44,7 +45,6 @@ #include #include -#include #include #include #include @@ -113,7 +113,7 @@ authorized(Port const& port, std::map const& h) if ((it == h.end()) || (!it->second.starts_with("Basic "))) return false; std::string strUserPass64 = it->second.substr(6); - boost::trim(strUserPass64); + strUserPass64 = trimWhitespace(strUserPass64); std::string const strUserPass = base64Decode(strUserPass64); std::string::size_type const nColon = strUserPass.find(':'); if (nColon == std::string::npos) diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index f131af01e5..eed4e4cfe3 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -22,7 +23,6 @@ #include #include -#include #include #include @@ -57,7 +57,7 @@ injectSLE(json::Value& jv, SLE const& sle) auto const& hash = sle.getFieldH128(sfEmailHash); Blob const b(hash.begin(), hash.end()); std::string md5 = strHex(makeSlice(b)); - boost::to_lower(md5); + md5 = toLower(md5); // VFALCO TODO Give a name to this constant and move it // to a more visible location. jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp index 91db16bb4f..5c96bfb215 100644 --- a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp @@ -3,14 +3,13 @@ #include #include +#include #include #include #include #include #include -#include - #include #include #include @@ -38,7 +37,7 @@ doCanDelete(rpc::JsonContext& context) else { std::string canDeleteStr = canDelete.asString(); - boost::to_lower(canDeleteStr); + canDeleteStr = toLower(canDeleteStr); if (canDeleteStr.find_first_not_of("0123456789") == std::string::npos) { diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index 32a084a833..c297c2482d 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -14,7 +15,6 @@ #include #include -#include #include #include @@ -106,7 +106,7 @@ ServerDefinitions::translate(std::string const& inp) std::string token = inpToProcess.substr(0, pos); if (token.size() > 1) { - boost::algorithm::to_lower(token); + token = toLower(token); token[0] -= ('a' - 'A'); out += token; } From 26cc683ec143e8a5fcc6dd09c2c1fe25ac08b94c Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 11 Aug 2026 18:15:51 +0000 Subject: [PATCH 68/68] fix: Assorted MPT/DEX fixes (#7299) Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> --- include/xrpl/ledger/helpers/AMMHelpers.h | 28 +- include/xrpl/ledger/helpers/MPTokenHelpers.h | 8 + include/xrpl/protocol/AmountConversions.h | 2 +- include/xrpl/protocol/QualityFunction.h | 9 + include/xrpl/tx/paths/detail/StrandFlow.h | 25 +- include/xrpl/tx/transactors/dex/AMMWithdraw.h | 7 + src/libxrpl/ledger/helpers/MPTokenHelpers.cpp | 13 +- src/libxrpl/protocol/QualityFunction.cpp | 17 +- src/libxrpl/protocol/STAmount.cpp | 90 ++- src/libxrpl/tx/invariants/MPTInvariant.cpp | 7 +- src/libxrpl/tx/paths/BookStep.cpp | 246 ++++-- src/libxrpl/tx/paths/MPTEndpointStep.cpp | 3 +- src/libxrpl/tx/paths/OfferStream.cpp | 63 +- .../tx/transactors/check/CheckCash.cpp | 2 +- .../tx/transactors/dex/AMMClawback.cpp | 12 + .../tx/transactors/dex/AMMWithdraw.cpp | 47 +- src/test/app/AMMClawbackMPT_test.cpp | 252 +++++++ src/test/app/AMMExtendedMPT_test.cpp | 133 +++- src/test/app/AMMExtended_test.cpp | 48 +- src/test/app/AMMMPT_test.cpp | 157 +++- src/test/app/AMM_test.cpp | 71 +- src/test/app/EscrowToken_test.cpp | 181 +++++ src/test/app/FlowMPT_test.cpp | 160 ++++ src/test/app/OfferMPT_test.cpp | 711 ++++++++++++++++++ src/test/protocol/STAmount_test.cpp | 85 +++ 25 files changed, 2198 insertions(+), 179 deletions(-) diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index 7d41bfce81..a68171c426 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -226,7 +226,7 @@ getAMMOfferStartWithTakerGets( auto getAmounts = [&pool, &tfee](Number const& nTakerGetsProposed) { // Round downward to minimize the offer and to maximize the quality. - // This has the most impact when takerGets is XRP. + // This has the most impact when takerGets is integral. auto const takerGets = toAmount(getAsset(pool.out), nTakerGetsProposed, Number::RoundingMode::Downward); return TAmounts{swapAssetOut(pool, takerGets, tfee), takerGets}; @@ -294,7 +294,7 @@ getAMMOfferStartWithTakerPays( auto getAmounts = [&pool, &tfee](Number const& nTakerPaysProposed) { // Round downward to minimize the offer and to maximize the quality. - // This has the most impact when takerPays is XRP. + // This has the most impact when takerPays is integral. auto const takerPays = toAmount(getAsset(pool.in), nTakerPaysProposed, Number::RoundingMode::Downward); return TAmounts{takerPays, swapAssetIn(pool, takerPays, tfee)}; @@ -313,11 +313,11 @@ getAMMOfferStartWithTakerPays( * is equal to LOB quality (in this case AMM offer quality is * better than LOB quality) or AMM offer is equal to LOB quality * (in this case SPQ is better than LOB quality). - * Pre-amendment code calculates takerPays first. If takerGets is XRP, - * it is rounded down, which results in worse offer quality than - * LOB quality, and the offer might fail to generate. - * Post-amendment code calculates the XRP offer side first. The result - * is rounded down, which makes the offer quality better. + * Pre-amendment code calculates takerPays first. If takerGets is the + * economically coarser integral side, it is rounded down, which results in + * worse offer quality than LOB quality, and the offer might fail to generate. + * Post-amendment code calculates the economically coarser integral offer side + * first. The result is rounded down, which makes the offer quality better. * It might not be possible to match either SPQ or AMM offer to LOB * quality. This generally happens at higher fees. * @param pool AMM pool balances @@ -396,10 +396,18 @@ changeSpotPriceQuality( return std::nullopt; } - // Generate the offer starting with XRP side. Return seated offer amounts - // if the offer can be generated, otherwise nullopt. auto amounts = [&]() { - if (isXRP(getAsset(pool.out))) + bool const inIntegral = getAsset(pool.in).integral(); + bool const outIntegral = getAsset(pool.out).integral(); + + // Preserve historical behavior for fractional pairs and XRP/IOU-style + // one-integral-side pairs. For two integral assets, pick the side whose + // minimum unit is economically coarser at this quality. + // + // Quality::rate() is input units per output unit, so one output unit is + // coarser when it costs at least one input unit. Ties use takerGets, + // matching the historical XRP-output behavior. + if (outIntegral && (!inIntegral || Number(quality.rate()) >= 1)) return getAMMOfferStartWithTakerGets(pool, quality, tfee); return getAMMOfferStartWithTakerPays(pool, quality, tfee); }(); diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index 5418e5b26a..7babefd196 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -261,6 +261,14 @@ checkCreateMPT( xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, SLE::ref sponsorSle, + std::uint32_t flags, + beast::Journal j); + +TER +checkCreateMPT( + xrpl::ApplyView& view, + xrpl::MPTIssue const& mptIssue, + xrpl::AccountID const& holder, beast::Journal j); //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 3bcd80e827..ed68be62fe 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -154,7 +154,7 @@ T toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround()) { SaveNumberRoundMode const rm(Number::getround()); - if (isXRP(asset)) + if (asset.integral()) Number::setround(mode); if constexpr (std::is_same_v) diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h index 128b37ce12..4fcc730c42 100644 --- a/include/xrpl/protocol/QualityFunction.h +++ b/include/xrpl/protocol/QualityFunction.h @@ -60,6 +60,15 @@ public: std::optional outFromAvgQ(Quality const& quality); + /** + * Return whether `out` produces at least the requested + * average quality. + * @param quality requested average quality (quality limit) + * @param out output amount to test + */ + [[nodiscard]] bool + satisfiesAvgQ(Quality const& quality, Number const& out) const; + /** * Return true if the quality function is constant */ diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index c932c49cca..fcca97ecfc 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -373,7 +374,7 @@ qualityUpperBound(ReadView const& v, Strand const& strand) * increases quality of AMM steps, increasing the strand's composite * quality as the result. */ -template +template inline TOutAmt limitOut( ReadView const& v, @@ -411,21 +412,29 @@ limitOut( auto const out = qf->outFromAvgQ(limitQuality); if (!out) return remainingOut; - if constexpr (std::is_same_v) + if constexpr (std::is_same_v || std::is_same_v) { - return XRPAmount{*out}; + auto const roundedOut = TOutAmt{*out}; + // Integral outputs that round above the continuous target can + // realize worse average quality than the requested limit. Keep the + // default rounded value when it still satisfies the limit, since it + // is the largest matching offer; otherwise round down. + if (v.rules().enabled(featureMPTokensV2) && roundedOut > *out && + !qf->satisfiesAvgQ(limitQuality, roundedOut)) + { + NumberRoundModeGuard const g(Number::RoundingMode::Downward); + return TOutAmt{*out}; + } + return roundedOut; } else if constexpr (std::is_same_v) { return IOUAmount{*out}; } - else if constexpr (std::is_same_v) - { - return MPTAmount{*out}; - } else { - return STAmount{remainingOut.asset(), out->mantissa(), out->exponent()}; + static constexpr bool kAlwaysFalse = !std::is_same_v; + static_assert(kAlwaysFalse, "Unhandled StepAmount type"); } }(); // A tiny difference could be due to the round off diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 7004dd57c1..6861fa7bc4 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -118,6 +118,7 @@ public: Sandbox& view, SLE const& ammSle, AccountID const account, + std::optional const& clawbackIssuer, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -138,6 +139,11 @@ public: * @param view * @param ammSle AMM ledger entry * @param ammAccount AMM account + * @param clawbackIssuer when set (AMMClawback path), the issuer performing + * the clawback. A recreated MPToken is only auto-authorized when the + * asset's issuer matches this account, so a clawback cannot grant + * authorization on behalf of a different (paired-asset) issuer. + * @param account LP account * @param amountBalance current LP asset1 balance * @param amountWithdraw asset1 withdraw amount * @param amount2Withdraw asset2 withdraw amount @@ -153,6 +159,7 @@ public: Sandbox& view, SLE const& ammSle, AccountID const& ammAccount, + std::optional const& clawbackIssuer, AccountID const& account, STAmount const& amountBalance, STAmount const& amountWithdraw, diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 6fe7328fa7..b239d0d3d1 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -952,6 +952,7 @@ checkCreateMPT( xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, SLE::ref sponsorSle, + std::uint32_t flags, beast::Journal j) { if (mptIssue.getIssuer() == holder) @@ -961,7 +962,7 @@ checkCreateMPT( auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder); if (!view.exists(mptokenID)) { - if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, 0); + if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, flags); !isTesSuccess(err)) { return err; @@ -977,6 +978,16 @@ checkCreateMPT( return tesSUCCESS; } +TER +checkCreateMPT( + xrpl::ApplyView& view, + xrpl::MPTIssue const& mptIssue, + xrpl::AccountID const& holder, + beast::Journal j) +{ + return checkCreateMPT(view, mptIssue, holder, {}, 0, j); +} + std::int64_t maxMPTAmount(SLE const& sleIssuance) { diff --git a/src/libxrpl/protocol/QualityFunction.cpp b/src/libxrpl/protocol/QualityFunction.cpp index e862770406..ffe583b7e1 100644 --- a/src/libxrpl/protocol/QualityFunction.cpp +++ b/src/libxrpl/protocol/QualityFunction.cpp @@ -38,7 +38,22 @@ QualityFunction::outFromAvgQ(Quality const& quality) return std::nullopt; return out; } - return std::nullopt; + // The sole caller (StrandFlow::limitOut) only invokes this on a non-const + // quality function, so m_ != 0 here, and a real payment/offer never yields + // a zero-rate limit quality (it would divide by zero above). This fallback + // is therefore unreachable in practice. + return std::nullopt; // LCOV_EXCL_LINE +} + +bool +QualityFunction::satisfiesAvgQ(Quality const& quality, Number const& out) const +{ + // satisfiesAvgQ is only reached from StrandFlow::limitOut *after* + // outFromAvgQ returned a value, which requires a non-zero rate. So a + // zero-rate quality never reaches here; this guard is defensive. + if (quality.rate() == beast::kZero) + return false; // LCOV_EXCL_LINE + return m_ * out + b_ >= 1 / quality.rate(); } } // namespace xrpl diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 212c34322b..83b2983756 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1445,6 +1445,59 @@ public: operator=(DontAffectNumberRoundMode const&) = delete; }; +Number::RoundingMode +roundMode(bool const resultNegative, bool const roundUp) +{ + using enum Number::RoundingMode; + // STAmount roundUp means "away from zero". The legacy scaled-mantissa + // multiply and divide paths reach that result with slightly different + // mechanics, including a final TowardsZero materialization in multiply. + // + // The MPT/V2 Number path already performs the operation under the directed + // mode below. Use the same mode again when converting back to STAmount so a + // fractional integral result stays consistently rounded after Number + // arithmetic, independent of whether the operation was multiply or divide. + return roundUp ^ resultNegative ? Upward : Downward; +} + +STAmount +roundNumberResult( + Asset const& asset, + bool const resultNegative, + bool const roundUp, + Number const& number) +{ + // MPT/V2 Number arithmetic uses directed rounding both for the operation + // and for materializing the final integral amount. + NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp)); + auto result = STAmount{asset, number}; + [[maybe_unused]] bool const nonzeroPositiveRoundUp = + roundUp && !resultNegative && number != beast::kZero; + ALWAYS( + !nonzeroPositiveRoundUp || result != beast::kZero, + "xrpl::roundNumberResult : positive rounded-up MPT result is representable"); + + if (roundUp && !resultNegative && !result) + { + // Intended to preserve existing mulRound/divRound behavior for a + // positive result too small to represent in the target asset. + // + // Unreachable in practice: when roundUp is set, roundMode() above + // selects Upward, and materializing a Number into an STAmount honors + // that mode (Number::operator rep()), so any positive value rounds up + // to at least the smallest representable unit. Hence, a positive result + // is never !result here; the only zero case is a zero operand, which + // the mulRound/divRound callers handle before reaching this function. + // LCOV_EXCL_START + if (asset.integral()) + return STAmount{asset, 1}; + return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false}; + // LCOV_EXCL_STOP + } + + return result; +} + } // anonymous namespace // Pass the canonicalizeRound function pointer as a template parameter. @@ -1486,6 +1539,22 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro return STAmount(asset, minV * maxV); } + bool const resultNegative = v1.negative() != v2.negative(); + + if (asset.holds() && isFeatureEnabled(featureMPTokensV2, false)) + { + // MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer + // rates. Use Number arithmetic under MPTokensV2 so the rounded + // operation is not limited by the legacy uint64_t scaled mantissa. + Number result; + { + NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp)); + result = Number{v1} * Number{v2}; + } + + return roundNumberResult(asset, resultNegative, roundUp, result); + } + std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa(); int offset1 = v1.exponent(), offset2 = v2.exponent(); @@ -1506,9 +1575,6 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro --offset2; } } - - bool const resultNegative = v1.negative() != v2.negative(); - // We multiply the two mantissas (each is between 10^15 // and 10^16), so their product is in the 10^30 to 10^32 // range. Dividing their product by 10^14 maintains the @@ -1575,6 +1641,22 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool if (num == beast::kZero) return {asset}; + bool const resultNegative = (num.negative() != den.negative()); + + if (asset.holds() && isFeatureEnabled(featureMPTokensV2, false)) + { + // Match the multiply path above: Number performs the rounded + // operation, then STAmount materializes the final MPT amount using the + // same final rounding mode as the legacy path below. + Number result; + { + NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp)); + result = Number{num} / Number{den}; + } + + return roundNumberResult(asset, resultNegative, roundUp, result); + } + std::uint64_t numVal = num.mantissa(), denVal = den.mantissa(); int numOffset = num.exponent(), denOffset = den.exponent(); @@ -1596,8 +1678,6 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool } } - bool const resultNegative = (num.negative() != den.negative()); - // We divide the two mantissas (each is between 10^15 // and 10^16). To maintain precision, we multiply the // numerator by 10^17 (the product is in the range of diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 77c5ad781e..12ec078c82 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -282,12 +282,13 @@ ValidMPTIssuance::finalize( "but created bad number of mptokens"; return false; } - // At most one MPToken may be created on withdraw/clawback since: + // At most two MPToken may be created on withdraw/clawback since: // - Liquidity Provider must have at least one token in order - // participate in AMM pool liquidity. + // participate in AMM pool liquidity or have LPTokens only. // - At most two MPTokens may be deleted if AMM pool, which has exactly // two tokens, is empty after withdraw/clawback. - if (mptokensCreated_ > 1 || mptokensDeleted_ > 2) + SOMETIMES(mptokensCreated_ == 2, "AMM withdraw/clawback recreated two MPTokens"); + if (mptokensCreated_ > 2 || mptokensDeleted_ > 2) { JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded " "but created/deleted bad number of mptokens"; diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index e7c2e9ee29..2823627108 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -44,7 +44,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -653,7 +655,15 @@ limitStepIn( // under an amendment. ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false); stpAmt.out = ofrAmt.out; - ownerGives = mulRatio(ofrAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); + // Round up for MPT output so the offer owner pays the full + // ceil(amount × rate) fee, matching direct Payment semantics. IOU uses + // floating-point arithmetic so the floor/ceil distinction is sub-epsilon + // there; preserve the historical false to avoid changing IOU behavior. + ownerGives = mulRatio( + ofrAmt.out, + transferRateOut, + QUALITY_ONE, + /*roundUp*/ std::is_same_v); } } @@ -672,7 +682,11 @@ limitStepOut( if (limit < stpAmt.out) { stpAmt.out = limit; - ownerGives = mulRatio(stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false); + ownerGives = mulRatio( + stpAmt.out, + transferRateOut, + QUALITY_ONE, + /*roundUp*/ std::is_same_v); ofrAmt = offer.limitOut( ofrAmt, stpAmt.out, @@ -727,17 +741,20 @@ BookStep::forEachOffer( bool const isAssetInMPT = assetIn.holds(); auto const& owner = offer.owner(); - if (isAssetInMPT) - { - // Create MPToken for the offer's owner. No need to check - // for the reserve since the offer is removed if it is consumed. - // Therefore, the owner count remains the same. - if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, {}, j_); - !isTesSuccess(err)) + auto removeOffer = [&](std::string_view logMessage = {}) { + auto const key = offer.key(); + if (!logMessage.empty()) { - return true; + JLOG(j_.trace()) << logMessage << (key ? " " + to_string(*key) : ""); } - } + if (key) + offers.permRmOffer(*key); + if (!offerAttempted) + { + // Change quality only if no previous offers were tried. + ofrQ = std::nullopt; + } + }; // It shouldn't matter from auth point of view whether it's sb // or afView. Amendment guard this change just in case. @@ -745,17 +762,15 @@ BookStep::forEachOffer( // Make sure offer owner has authorization to own Assets from issuer // and MPT assets can be traded/transferred. // An account can always own XRP or their own Assets. - if (!isTesSuccess(requireAuth(applyView, assetIn, owner)) || !checkMPTDEX(sb, owner)) + // Missing MPTokens are allowed during offer discovery; they are + // created later if the offer is actually consumed. + auto const authType = isAssetInMPT ? AuthType::WeakAuth : AuthType::Legacy; + if (!isTesSuccess(requireAuth(applyView, assetIn, owner, authType)) || + !checkMPTDEX(sb, owner)) { // Offer owner not authorized to hold IOU/MPT from issuer. // Remove this offer even if no crossing occurs. - if (auto const key = offer.key()) - offers.permRmOffer(*key); - if (!offerAttempted) - { - // Change quality only if no previous offers were tried. - ofrQ = std::nullopt; - } + removeOffer(); // Returning true causes offers.step() to delete the offer. return true; } @@ -768,52 +783,88 @@ BookStep::forEachOffer( static_cast(this)->getOfrOutRate(prevStep_, owner, strandDst_, trOut)); auto ofrAmt = offer.amount(); - TAmounts stpAmt{mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true), ofrAmt.out}; - - // owner pays the transfer fee. - auto ownerGives = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false); - - auto const funds = offer.isFunded() - ? ownerGives // Offer owner is issuer; they have unlimited funds - : offers.ownerFunds(); - - // Only if CLOB offer - if (funds < ownerGives) + TAmounts stpAmt{ofrAmt.in, ofrAmt.out}; + auto ownerGives = ofrAmt.out; + try { - // We already know offer.owner()!=offer.issueOut().account - ownerGives = funds; - stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false); - - // It turns out we can prevent order book blocking by (strictly) - // rounding down the ceil_out() result. This adjustment changes - // transaction outcomes, so it must be made under an amendment. - ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false); - + // All arithmetic in this block runs before the offer is consumed. + // A crafted MPTokensV2 offer can overflow while transfer rates or + // crossing limits are applied; remove that unusable offer instead + // of letting it persist as a tecINTERNAL source. stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true); - } - // Limit offer's input if MPT, BookStep is the first step (an issuer - // is making a cross-currency payment), and this offer is not owned - // by the issuer. Otherwise, OutstandingAmount may overflow. - auto const& issuer = assetIn.getIssuer(); - if (isAssetInMPT && !prevStep_ && offer.owner() != issuer) - { - // Funds available to issue - auto const available = toAmount(accountFunds( - sb, - issuer, - assetIn, // STAmount{0}, but the default is not used - FreezeHandling::IgnoreFreeze, - AuthHandling::IgnoreAuth, - j_)); - if (stpAmt.in > available) + // owner pays the transfer fee. + ownerGives = mulRatio( + ofrAmt.out, + ofrOutRate, + QUALITY_ONE, + /*roundUp*/ std::is_same_v); + + auto const funds = offer.isFunded() + ? ownerGives // Offer owner is issuer; they have unlimited funds + : offers.ownerFunds(); + + // Only if CLOB offer + if (funds < ownerGives) { - limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available); - } - } + // We already know offer.owner()!=offer.issueOut().account + ownerGives = funds; + stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false); - offerAttempted = true; - return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate); + // It turns out we can prevent order book blocking by (strictly) + // rounding down the ceil_out() result. This adjustment changes + // transaction outcomes, so it must be made under an amendment. + ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false); + + stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true); + } + + // Limit offer's input if MPT, BookStep is the first step (an issuer + // is making a cross-currency payment), and this offer is not owned + // by the issuer. Otherwise, OutstandingAmount may overflow. + auto const& issuer = assetIn.getIssuer(); + if (isAssetInMPT && !prevStep_ && offer.owner() != issuer) + { + // Funds available to issue + auto const available = toAmount(accountFunds( + sb, + issuer, + assetIn, // STAmount{0}, but the default is not used + FreezeHandling::IgnoreFreeze, + AuthHandling::IgnoreAuth, + j_)); + if (stpAmt.in > available) + { + limitStepIn( + offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available); + } + } + + offerAttempted = true; + return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate); + } + catch (std::overflow_error const&) + { + if (sb.rules().enabled(featureMPTokensV2)) + { + SOMETIMES( + true, + "BookStep::forEachOffer removed MPT offer after " + "overflow during crossing"); + removeOffer("Removing offer with overflowing amount calculation"); + return true; + } + // An overflow can only be produced by a crafted MPT offer, and MPT + // offers require featureMPTokensV2 (enforced at OfferCreate + // preflight). So the amendment is always enabled when we get here + // and this legacy re-throw is unreachable in practice. + // LCOV_EXCL_START + XRPL_ASSERT( + sb.rules().enabled(featureMPTokensV2), + "xrpl::BookStep::forEachOffer : overflow implies MPTokensV2"); + throw; + // LCOV_EXCL_STOP + } }; // At any payment engine iteration, AMM offer can only be consumed once. @@ -873,6 +924,22 @@ BookStep::consumeOffer( // The offer owner gets the ofrAmt. The difference between ofrAmt and // stepAmt is a transfer fee that goes to book_.in.account { + if constexpr (std::is_same_v) + { + // If the offer's TakerPays asset is an MPT, the offer owner must + // hold an MPToken to receive it. Create one here if it doesn't + // already exist. + if (auto const err = checkCreateMPT(sb, book_.in.get(), offer.owner(), j_); + !isTesSuccess(err)) + { + // checkCreateMPT only fails on tecDIR_FULL (its source line is + // itself LCOV-excluded) or a missing offer-owner account, which + // cannot happen since that account owns the offer being + // consumed. Defensive and unreachable in practice. + Throw(err); // LCOV_EXCL_LINE + } + } + auto const dr = offer.send( sb, book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_); if (!isTesSuccess(dr)) @@ -1043,6 +1110,13 @@ BookStep::revImp( auto ofrAdjAmt = ofrAmt; auto stpAdjAmt = stpAmt; auto ownerGivesAdj = ownerGives; + // This reduction can overflow via the transfer-rate mulRatio() on a + // 63-bit MPT amount (IOU rescales instead of throwing, and XRP stays + // under the int64 limit, so only MPT reaches it today), but + // savedIns/savedOuts are not updated until after it succeeds. The outer + // execOffer() catch can therefore remove the offer under + // featureMPTokensV2 (legacy propagate-the-exception behavior otherwise) + // without rolling back local state. limitStepOut( offer, ofrAdjAmt, @@ -1144,12 +1218,25 @@ BookStep::fwdImp( auto stpAdjAmt = stpAmt; auto ownerGivesAdj = ownerGives; + // limitStepIn()/limitStepOut() can throw std::overflow_error from the + // transfer-rate mulRatio() on a 63-bit MPT amount. (IOUAmount::mulRatio + // rescales rather than throwing, and XRP amounts/rates stay under the + // int64 limit, so in practice only MPT reaches this today.) execOffer() + // catches it: under featureMPTokensV2 the offending offer is removed; + // otherwise the legacy behavior (propagate the exception) is preserved. + // Keep candidate accumulator changes local until those calls succeed so + // the catch path does not observe partially updated state. Re-sum the + // staged sets to preserve historical flat_multiset summing behavior. + auto savedInsAdj = savedIns; + auto savedOutsAdj = savedOuts; + auto resultAdj = result; typename boost::container::flat_multiset::const_iterator lastOut; + if (stpAmt.in <= remainingIn) { - savedIns.insert(stpAmt.in); - lastOut = savedOuts.insert(stpAmt.out); - result = TAmounts(sum(savedIns), sum(savedOuts)); + savedInsAdj.insert(stpAmt.in); + lastOut = savedOutsAdj.insert(stpAmt.out); + resultAdj = TAmounts(sum(savedInsAdj), sum(savedOutsAdj)); // consume the offer even if stepAmt.in == remainingIn processMore = true; } @@ -1163,15 +1250,15 @@ BookStep::fwdImp( transferRateIn, transferRateOut, remainingIn); - savedIns.insert(remainingIn); - lastOut = savedOuts.insert(stpAdjAmt.out); - result.out = sum(savedOuts); - result.in = in; + savedInsAdj.insert(remainingIn); + lastOut = savedOutsAdj.insert(stpAdjAmt.out); + resultAdj.out = sum(savedOutsAdj); + resultAdj.in = in; processMore = false; } - if (result.out > cache_->out && result.in <= cache_->in) + if (resultAdj.out > cache_->out && resultAdj.in <= cache_->in) { // The step produced more output in the forward pass than the // reverse pass while consuming the same input (or less). If we @@ -1181,8 +1268,8 @@ BookStep::fwdImp( // input provided in the forward step and produce the output // requested from the reverse step. auto const lastOutAmt = *lastOut; - savedOuts.erase(lastOut); - auto const remainingOut = cache_->out - sum(savedOuts); + savedOutsAdj.erase(lastOut); + auto const remainingOut = cache_->out - sum(savedOutsAdj); auto ofrAdjAmtRev = ofrAmt; auto stpAdjAmtRev = stpAmt; auto ownerGivesAdjRev = ownerGives; @@ -1197,13 +1284,13 @@ BookStep::fwdImp( if (stpAdjAmtRev.in == remainingIn) { - result.in = in; - result.out = cache_->out; + resultAdj.in = in; + resultAdj.out = cache_->out; - savedIns.clear(); - savedIns.insert(result.in); - savedOuts.clear(); - savedOuts.insert(result.out); + savedInsAdj.clear(); + savedInsAdj.insert(resultAdj.in); + savedOutsAdj.clear(); + savedOutsAdj.insert(resultAdj.out); ofrAdjAmt = ofrAdjAmtRev; stpAdjAmt.in = remainingIn; @@ -1214,10 +1301,15 @@ BookStep::fwdImp( { // This is (likely) a problem case, and will be caught // with later checks - savedOuts.insert(lastOutAmt); + savedOutsAdj.insert(lastOutAmt); } } + // Commit the staged accounting only after limitStepIn()/limitStepOut() + // have succeeded. + savedIns = std::move(savedInsAdj); + savedOuts = std::move(savedOutsAdj); + result = resultAdj; remainingIn = in - result.in; this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj); diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp index 0a0f6a9f27..a47cfa15a5 100644 --- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp @@ -410,8 +410,7 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio // for the reserve since the offer doesn't go on the books // if crossed. Insufficient reserve is allowed if the offer // crossed. See CreateOffer::applyGuts() for reserve check. - if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, {}, j_); - !isTesSuccess(err)) + if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err)) { JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT"; resetCache(srcDebtDir); diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index ecc8416a2b..2f2fef49f0 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -29,6 +29,8 @@ #include #include +#include +#include namespace xrpl { @@ -136,17 +138,17 @@ template TOfferStreamBase::shouldRmSmallIncreasedQOffer() const { // Consider removing the offer if: - // o `TakerPays` is XRP (because of XRP drops granularity) or + // o `TakerPays` is integral (because XRP/MPT have indivisible units) or // o `TakerPays` and `TakerGets` are both IOU and `TakerPays`<`TakerGets` - static constexpr bool kInIsXrp = std::is_same_v; - static constexpr bool kOutIsXrp = std::is_same_v; + constexpr bool const kInIsIntegral = !std::is_same_v; + constexpr bool const kOutIsIntegral = !std::is_same_v; - if constexpr (kOutIsXrp) + if constexpr (!kInIsIntegral && kOutIsIntegral) { - // If `TakerGets` is XRP, the worst this offer's quality can change is - // to about 10^-81 `TakerPays` and 1 drop `TakerGets`. This will be - // remarkably good quality for any realistic asset, so these offers - // don't need this extra check. + // If only `TakerGets` is integral, the worst this offer's quality can + // change is to about 10^-81 `TakerPays` and 1 unit `TakerGets`. This + // will be perfect quality for any realistic asset, so these + // offers don't need this extra check. return false; } @@ -156,7 +158,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const TAmounts const ofrAmts{ toAmount(offer_.amount().in), toAmount(offer_.amount().out)}; - if constexpr (!kInIsXrp && !kOutIsXrp) + if constexpr (!kInIsIntegral && !kOutIsIntegral) { if (Number(ofrAmts.in) >= Number(ofrAmts.out)) return false; @@ -165,7 +167,12 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const TTakerGets const ownerFunds = toAmount(*ownerFunds_); auto const effectiveAmounts = [&] { - if (offer_.owner() != offer_.assetOut().getIssuer() && ownerFunds < ofrAmts.out) + // Issuer-owned IOU offers are self-funded without a limit. MPT issuer + // offers are bounded by remaining issuance capacity, so they still need + // to be clipped by ownerFunds. + bool const issuerHasUnlimitedFunds = offer_.owner() == offer_.assetOut().getIssuer() && + offer_.assetOut().template holds(); + if (!issuerHasUnlimitedFunds && ownerFunds < ofrAmts.out) { // adjust the amounts by owner funds. // @@ -305,7 +312,41 @@ TOfferStreamBase::step() continue; } - if (shouldRmSmallIncreasedQOffer()) + // Partially funded offers can be reduced before BookStep sees them. + // If that strict reduction overflows under MPTokensV2, remove the + // unusable offer instead of leaving it at the book tip. + bool shouldRemoveSmallIncreasedQOffer = false; + try + { + shouldRemoveSmallIncreasedQOffer = shouldRmSmallIncreasedQOffer(); + } + catch (std::overflow_error const&) + { + if (view_.rules().enabled(featureMPTokensV2)) + { + SOMETIMES( + true, + "OfferStream::step removed MPT offer with overflowing " + "reduced quality"); + permRmOffer(entry->key()); + JLOG(j_.warn()) << "Removing offer with overflowing reduced quality " + << entry->key(); + offer_ = TOffer{}; + continue; + } + // The strict reduction only overflows for a crafted MPT offer, and + // MPT offers require featureMPTokensV2 (enforced at OfferCreate + // preflight). So the amendment is always enabled here and this + // legacy re-throw is unreachable in practice. + // LCOV_EXCL_START + XRPL_ASSERT( + view_.rules().enabled(featureMPTokensV2), + "xrpl::TOfferStreamBase::step : overflow implies MPTokensV2"); + throw; + // LCOV_EXCL_STOP + } + + if (shouldRemoveSmallIncreasedQOffer) { auto const originalFunds = accountFundsHelper( cancelView_, diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index e4d8f192c0..857f759752 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -528,7 +528,7 @@ CheckCash::doApply() return tecINSUFFICIENT_RESERVE; if (auto const err = - checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_); + checkCreateMPT(psb, mptID, accountID_, *sponsorSle, 0, j_); !isTesSuccess(err)) { return err; diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index 455b2ad5c5..e690cd7693 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -227,6 +227,7 @@ AMMClawback::applyGuts(Sandbox& sb) sb, *ammSle, holder, + issuer, ammAccount, amountBalance, amount2Balance, @@ -311,6 +312,14 @@ AMMClawback::equalWithdrawMatchingOneAmount( STAmount const& holdLPtokens, STAmount const& amount) { + // The clawback issuer signs for its own asset only. Threaded into the + // withdrawal so a recreated MPToken is auto-authorized only for the + // clawback issuer's asset, never for a paired asset from another issuer. + // preflight guarantees sfAccount is the clawed asset's issuer (it rejects + // the tx as temMALFORMED when sfAsset's issuer != sfAccount), so this is + // the issuer, not just any signer. + AccountID const issuer = ctx_.tx[sfAccount]; + auto frac = Number{amount} / amountBalance; auto amount2Withdraw = amount2Balance * frac; @@ -324,6 +333,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( sb, ammSle, holder, + issuer, ammAccount, amountBalance, amount2Balance, @@ -364,6 +374,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( sb, ammSle, ammAccount, + issuer, holder, amountBalance, amountRounded, @@ -384,6 +395,7 @@ AMMClawback::equalWithdrawMatchingOneAmount( sb, ammSle, ammAccount, + issuer, holder, amountBalance, amount, diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 5294dd0c7f..edd2cc2037 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -516,6 +517,7 @@ AMMWithdraw::withdraw( view, ammSle, ammAccount, + std::nullopt, accountID_, amountBalance, amountWithdraw, @@ -536,6 +538,7 @@ AMMWithdraw::withdraw( Sandbox& view, SLE const& ammSle, AccountID const& ammAccount, + std::optional const& clawbackIssuer, AccountID const& account, STAmount const& amountBalance, STAmount const& amountWithdraw, @@ -703,14 +706,48 @@ AMMWithdraw::withdraw( if (mptokenKey && account != asset.getIssuer()) { auto const& mptIssue = asset.get(); + std::uint32_t createFlags = 0; if (auto const err = requireAuth(view, mptIssue, account, AuthType::WeakAuth); !isTesSuccess(err)) - return err; + { + if (authHandling != AuthHandling::IgnoreAuth || err != tecNO_AUTH) + { + // Unreachable in practice. Normal withdraws (authHandling + // != IgnoreAuth) are rejected for unauthorized holders in + // preclaim, so they never get here. Under clawback + // (IgnoreAuth) requireAuth returns a non-tecNO_AUTH error + // (e.g. tecEXPIRED) only for a domain-authorized MPT, but no + // such MPT can be in an AMM pool: a directly domain-gated + // RequireAuth MPT fails AMMCreate/deposit with tecNO_AUTH, + // and vault shares (whose recursive auth could yield + // tecEXPIRED) are rejected by AMMCreate with tecWRONG_ASSET. + return err; // LCOV_EXCL_LINE + } - if (auto const err = checkCreateMPT(view, mptIssue, account, {}, journal); + // AMMClawback ignores authorization so the issuer can recover + // MPT locked in the pool even if the holder deleted their + // MPToken. Only auto-authorize the recreated MPToken for the + // clawback issuer's own asset: authorization is granted by an + // asset's issuer, and the clawback transaction is signed by + // that issuer only for its own asset. For a paired asset issued + // by a different account, recreate the MPToken *unauthorized* so + // the clawback does not grant authorization on behalf of that + // issuer (which would bypass its lsfMPTRequireAuth). The holder + // still receives the paired asset (accountSend only requires the + // MPToken to exist, not to be authorized); the balance remains + // gated by its issuer until that issuer authorizes it. + if (clawbackIssuer && asset.getIssuer() == *clawbackIssuer) + createFlags = lsfMPTAuthorized; + } + + if (auto const err = checkCreateMPT(view, mptIssue, account, {}, createFlags, journal); !isTesSuccess(err)) { - return err; + // checkCreateMPT only fails on tecDIR_FULL (its source line is + // itself LCOV-excluded) or a missing account, which cannot + // happen since `account` is the withdrawing LP. Defensive and + // unreachable in practice. + return err; // LCOV_EXCL_LINE } } return tesSUCCESS; @@ -804,6 +841,7 @@ AMMWithdraw::equalWithdrawTokens( view, ammSle, accountID_, + std::nullopt, ammAccount, amountBalance, amount2Balance, @@ -856,6 +894,7 @@ AMMWithdraw::equalWithdrawTokens( Sandbox& view, SLE const& ammSle, AccountID const account, + std::optional const& clawbackIssuer, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -878,6 +917,7 @@ AMMWithdraw::equalWithdrawTokens( view, ammSle, ammAccount, + clawbackIssuer, account, amountBalance, amountBalance, @@ -913,6 +953,7 @@ AMMWithdraw::equalWithdrawTokens( view, ammSle, ammAccount, + clawbackIssuer, account, amountBalance, amountWithdraw, diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp index 6c7aa99156..1d75c4db22 100644 --- a/src/test/app/AMMClawbackMPT_test.cpp +++ b/src/test/app/AMMClawbackMPT_test.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -1476,6 +1478,60 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite } } + void + testClawbackCreatesMissingMPToken(FeatureBitset features) + { + testcase("test AMMClawback creates missing MPToken"); + using namespace jtx; + + auto test = [&](std::optional const clawAmount) { + Env env{*this, features}; + Account const gw{"gateway"}; + Account const alice{"alice"}; + env.fund(XRP(1'000'000), gw, alice); + env.close(); + + MPTTester token( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 1'000, + .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags, + .authHolder = true}); + + AMM ammAlice(env, alice, token(1'000), XRP(1'000)); + env.close(); + BEAST_EXPECT(env.balance(alice, token) == token(0)); + + // The holder can delete the zero-balance MPToken while still + // holding LP tokens. A regular AMMWithdraw remains subject to + // RequireAuth and cannot recreate the missing token. + token.authorize({.account = alice, .flags = tfMPTUnauthorize}); + env.close(); + BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id()))); + ammAlice.withdrawAll(alice, std::nullopt, Ter(tecNO_AUTH)); + env.close(); + BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id()))); + + // AMMClawback ignores authorization and must be able to recreate + // the holder MPToken so the issuer can recover MPT from the pool. + std::optional amount; + if (clawAmount) + amount = token(*clawAmount); + env(amm::ammClawback(gw, alice, token, XRP, amount)); + env.close(); + + auto const sleMpt = env.le(keylet::mptoken(token.issuanceID(), alice.id())); + BEAST_EXPECT(sleMpt && sleMpt->isFlag(lsfMPTAuthorized)); + env.require(Balance(alice, token(0))); + + BEAST_EXPECT(clawAmount ? ammAlice.ammExists() : !ammAlice.ammExists()); + }; + + test(std::nullopt); + test(400); + } + void testSingleDepositAndClawback(FeatureBitset features) { @@ -1949,6 +2005,199 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite } } + // Test that AMMClawback succeeds when the LP has previously deleted both + // zero-balance MPToken objects in an MPT/MPT pool. The fix changes the + // ValidMPTIssuance invariant threshold from > 1 to > 2 so that the two + // MPToken creations triggered by the internal AMMWithdraw are permitted. + void + testClawbackAfterDeletingMPTokens(FeatureBitset features) + { + testcase("test AMMClawback after holder deletes zero-balance MPTokens"); + using namespace jtx; + + // Partial clawback (one asset): verify both MPTokens are recreated and + // the non-claw asset is returned to alice. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const alice{"alice"}; + env.fund(XRP(100'000), gw, alice); + env.close(); + + MPTTester btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + MPTTester eth( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + // Alice deposits everything into the MPT/MPT pool; her MPT + // balances drop to zero. + AMM const amm(env, alice, btc(10'000), eth(10'000)); + env.close(); + BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000})); + + auto aliceBTC = env.balance(alice, btc); + auto aliceETH = env.balance(alice, eth); + BEAST_EXPECT(aliceBTC == btc(0)); + BEAST_EXPECT(aliceETH == eth(0)); + + // Alice deletes both zero-balance MPTokens to reclaim reserves. + btc.authorize({.account = alice, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // gw claws back some BTC from alice's share in the pool. + // AMMWithdraw internally creates both missing MPTokens + // (mptokensCreated_ == 2); the invariant (> 2) allows this. + env(amm::ammClawback(gw, alice, btc, eth, btc(1'000))); + env.close(); + + // Both MPToken objects must have been recreated. + BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // The non-claw asset (eth) was returned to alice. + BEAST_EXPECT(env.balance(alice, eth) > aliceETH); + // The claw asset (btc) was burned; alice's btc balance stays 0. + env.require(Balance(alice, aliceBTC)); + BEAST_EXPECT(amm.ammExists()); + } + + // Full clawback (two assets, tfClawTwoAssets): verify both MPTokens + // are recreated and the AMM is deleted when fully drained. + { + Env env(*this, features); + Account const gw{"gateway"}; + Account const alice{"alice"}; + env.fund(XRP(100'000), gw, alice); + env.close(); + + MPTTester btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + MPTTester eth( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | kMptDexFlags}); + + AMM const amm(env, alice, btc(10'000), eth(10'000)); + env.close(); + + auto aliceBTC = env.balance(alice, btc); + auto aliceETH = env.balance(alice, eth); + + btc.authorize({.account = alice, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // Full two-asset clawback: both assets are clawed and alice + // receives nothing back. The AMM should be empty and deleted. + env(amm::ammClawback(gw, alice, btc, eth, std::nullopt), Txflags(tfClawTwoAssets)); + env.close(); + + BEAST_EXPECT(!amm.ammExists()); + // Both assets were clawed; alice's balances remain at zero. + env.require(Balance(alice, aliceBTC)); + env.require(Balance(alice, aliceETH)); + } + } + + void + testClawbackCrossIssuerPairedAssetAuth(FeatureBitset features) + { + testcase("test AMMClawback recreates paired-issuer MPToken unauthorized"); + using namespace jtx; + + // Cross-issuer MPT/MPT pool: btc is issued by gw, eth by gw2, and both + // require authorization. Alice deposits her entire balance of both and + // deletes the resulting zero-balance MPTokens. When gw claws back its + // own asset (btc), the two-asset withdrawal must recreate both of + // Alice's MPTokens so the pool can pay her the paired asset. The + // recreated MPToken may only be auto-authorized for the clawback + // issuer's own asset (btc); the paired asset's issuer (gw2) never + // consented, so eth must be recreated *unauthorized*, leaving gw2 in + // control of its own token and preserving its RequireAuth guarantee. + Env env(*this, features); + Account const gw{"gateway"}; + Account const gw2{"gateway2"}; + Account const alice{"alice"}; + env.fund(XRP(100'000), gw, gw2, alice); + env.close(); + + MPTTester btc( + {.env = env, + .issuer = gw, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags, + .authHolder = true}); + + MPTTester eth( + {.env = env, + .issuer = gw2, + .holders = {alice}, + .pay = 10'000, + .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags, + .authHolder = true}); + + // Alice deposits everything into the pool; her MPT balances drop to 0. + AMM const amm(env, alice, btc(10'000), eth(10'000)); + env.close(); + BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000})); + BEAST_EXPECT(env.balance(alice, btc) == btc(0)); + BEAST_EXPECT(env.balance(alice, eth) == eth(0)); + + // Alice deletes both zero-balance MPTokens to reclaim reserves. + btc.authorize({.account = alice, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id()))); + + // gw (issuer of btc) claws back part of Alice's btc. This is a + // cross-issuer pool, so tfClawTwoAssets is not permitted: only btc is + // clawed back, while the paired eth is returned to Alice. + env(amm::ammClawback(gw, alice, btc, eth, btc(1'000))); + env.close(); + + // Both MPTokens were recreated so the withdrawal could pay Alice. + auto const sleBtc = env.le(keylet::mptoken(btc.issuanceID(), alice.id())); + auto const sleEth = env.le(keylet::mptoken(eth.issuanceID(), alice.id())); + BEAST_EXPECT(sleBtc); + BEAST_EXPECT(sleEth); + + // The clawback issuer's own asset (btc) may be recreated authorized: + // gw has authority over its own token. + BEAST_EXPECT(sleBtc && sleBtc->isFlag(lsfMPTAuthorized)); + + // The paired asset (eth) is issued by gw2, who did not sign this + // transaction. It must be recreated *unauthorized* so gw2's RequireAuth + // is not bypassed. This is the core assertion for the cross-issuer fix. + BEAST_EXPECT(sleEth && !sleEth->isFlag(lsfMPTAuthorized)); + + // The clawback still completed: btc was clawed back (Alice keeps a zero + // btc balance) and the paired eth was delivered into Alice's now + // unauthorized, gw2-gated MPToken (non-zero raw balance). + BEAST_EXPECT(sleBtc && sleBtc->getFieldU64(sfMPTAmount) == 0); + BEAST_EXPECT(sleEth && sleEth->getFieldU64(sfMPTAmount) > 0); + BEAST_EXPECT(amm.ammExists()); + } + void run() override { @@ -1965,6 +2214,9 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite testAMMClawbackAllSameIssuer(all); testAMMClawbackIssuesEachOther(all); testAssetFrozenOrLocked(all); + testClawbackCreatesMissingMPToken(all); + testClawbackAfterDeletingMPTokens(all); + testClawbackCrossIssuerPairedAssetAuth(all); testSingleDepositAndClawback(all); testLastHolderLPTokenBalance(all); testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding); diff --git a/src/test/app/AMMExtendedMPT_test.cpp b/src/test/app/AMMExtendedMPT_test.cpp index f04ea39f2b..5059128d4b 100644 --- a/src/test/app/AMMExtendedMPT_test.cpp +++ b/src/test/app/AMMExtendedMPT_test.cpp @@ -188,20 +188,28 @@ private: {features}); // tfPassive -- place the offer without crossing it. - testAMM( - [&](AMM& ammAlice, Env& env) { - // Carol creates a passive offer that could cross AMM. - // Carol's offer should stay in the ledger. - auto const& btc = MPT(ammAlice[1]); - env(offer(carol_, XRP(100), btc(100), tfPassive)); - env.close(); - BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens())); - BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), btc(100)}}})); - }, - {{XRP(10'100), gAmmmpt(10'000)}}, - 0, - std::nullopt, - {features}); + { + Env env{*this, features}; + fund(env, gw_, {alice_, carol_}, XRP(30'000'000)); + + MPTTester const btc( + {.env = env, + .issuer = gw_, + .holders = {alice_, carol_}, + .pay = 30'000'000, + .flags = kMptDexFlags}); + + AMM const ammAlice(env, alice_, XRP(10'100'000), btc(10'000'000)); + + // Scale the exact-quality fixture up so the visual relationship + // stays clear: the passive CLOB offer has the same 1:1 quality as + // the generated AMM offer, so it should not cross. + env(offer(carol_, XRP(100'000), btc(100'000), tfPassive)); + env.close(); + BEAST_EXPECT( + ammAlice.expectBalances(XRP(10'100'000), btc(10'000'000), ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), btc(100'000)}}})); + } // tfPassive -- cross only offers of better quality. testAMM( @@ -1084,9 +1092,9 @@ private: // AMM is consumed up to the first cam Offer quality BEAST_EXPECT(ammCarol.expectBalances( - aBux(3'093'541'659'651'604), bBux(3'200'215'509'984'418), ammCarol.tokens())); + aBux(3'093'541'659'651'603), bBux(3'200'215'509'984'419), ammCarol.tokens())); BEAST_EXPECT(expectOffers( - env, cam, 1, {{Amounts{bBux(200'215'509'984'418), aBux(200'215'509'984'419)}}})); + env, cam, 1, {{Amounts{bBux(200'215'509'984'419), aBux(200'215'509'984'419)}}})); } void @@ -1241,7 +1249,7 @@ private: BEAST_EXPECT(sa == XRP(100'000'000)); // Bob gets ~99.99e12ETH. This is the amount Bob // can get out of AMM for 100,000,000XRP. - BEAST_EXPECT(equal(da, eth(99'999'900'000'100))); + BEAST_EXPECT(equal(da, eth(99'999'900'000'099))); } // carol holds ETH, sells ETH for XRP @@ -1505,6 +1513,96 @@ private: } } + void + pathFindMPTAMMExecutableSourceAmount() + { + testcase("Path Find: MPT AMM source amount is executable"); + using namespace jtx; + + auto const checkQuote = [&](std::int64_t usdPool, + std::int64_t eurPool, + std::int64_t deliverAmount, + std::int64_t expectedSourceAmount) { + Env env = pathTestEnv(); + env.fund(XRP(30'000), gw_, alice_, bob_, carol_); + env.close(); + + MPTTester const usd( + {.env = env, + .issuer = gw_, + .holders = {alice_, bob_, carol_}, + .pay = usdPool, + .flags = kMptDexFlags}); + + MPTTester const eur( + {.env = env, + .issuer = gw_, + .holders = {alice_, bob_, carol_}, + .pay = eurPool, + .flags = kMptDexFlags}); + + AMM const ammCarol(env, carol_, usd(usdPool), eur(eurPool)); + env.close(); + + STPathSet st; + STAmount sa, da; + auto const deliver = eur(deliverAmount); + std::tie(st, sa, da) = findPaths( + env, + alice_, + bob_, + deliver, + std::nullopt, + usd.issuanceID(), + std::nullopt, + std::nullopt); + + // Each quote must execute when used as an exact-output SendMax. + BEAST_EXPECT(equal(da, deliver)); + BEAST_EXPECT(equal(sa, usd(expectedSourceAmount))); + BEAST_EXPECT(!st.empty()); + + auto const before = eur.getBalance(bob_); + env(pay(alice_, bob_, deliver), + Json(jss::Paths, st.getJson(JsonOptions::Values::None)), + Sendmax(sa), + Txflags(tfNoRippleDirect)); + BEAST_EXPECT(eur.getBalance(bob_) == before + deliverAmount); + }; + + struct TestCase + { + std::int64_t usdPool; + std::int64_t eurPool; + std::int64_t deliverAmount; + std::int64_t expectedSourceAmount; + }; + + // Cover the original 2:1 pool and the same pool scaled down by 1000. + // clang-format off + TestCase const testCases[] = { + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1, .expectedSourceAmount = 3}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 2, .expectedSourceAmount = 5}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 10, .expectedSourceAmount = 21}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 100, .expectedSourceAmount = 201}, + {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1'000, .expectedSourceAmount = 2'003}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 1, .expectedSourceAmount = 3}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 2, .expectedSourceAmount = 5}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 10, .expectedSourceAmount = 21}, + {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 100, .expectedSourceAmount = 223}, + }; + // clang-format on + + for (auto const& testCase : testCases) + { + checkQuote( + testCase.usdPool, + testCase.eurPool, + testCase.deliverAmount, + testCase.expectedSourceAmount); + } + } + void testFalseDry(FeatureBitset features) { @@ -3583,6 +3681,7 @@ private: pathFind01(); pathFind02(); pathFind06(); + pathFindMPTAMMExecutableSourceAmount(); } void diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index bb532b361a..83c848b7c4 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -267,20 +267,39 @@ private: {features}); // tfPassive -- place the offer without crossing it. - testAMM( - [&](AMM& ammAlice, Env& env) { - // Carol creates a passive offer that could cross AMM. - // Carol's offer should stay in the ledger. - env(offer(carol_, XRP(100), USD(100), tfPassive)); - env.close(); - BEAST_EXPECT( - ammAlice.expectBalances(XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens())); - BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}})); - }, - {{XRP(10'100), USD(10'000)}}, - 0, - std::nullopt, - {features}); + if (features[featureMPTokensV2]) + { + Env env{*this, features}; + fund(env, gw_, {alice_, carol_}, XRP(30'000'000), {USD(30'000'000)}); + + AMM const ammAlice(env, alice_, XRP(10'100'000), USD(10'000'000)); + + // Scale the exact-quality fixture up so the visual relationship + // stays clear: the passive CLOB offer has the same 1:1 quality as + // the generated AMM offer, so it should not cross. + env(offer(carol_, XRP(100'000), USD(100'000), tfPassive)); + env.close(); + BEAST_EXPECT( + ammAlice.expectBalances(XRP(10'100'000), USD(10'000'000), ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), USD(100'000)}}})); + } + else + { + testAMM( + [&](AMM& ammAlice, Env& env) { + // Carol creates a passive offer that could cross AMM. + // Carol's offer should stay in the ledger. + env(offer(carol_, XRP(100), USD(100), tfPassive)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens())); + BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}})); + }, + {{XRP(10'100), USD(10'000)}}, + 0, + std::nullopt, + {features}); + } // tfPassive -- cross only offers of better quality. testAMM( @@ -1359,6 +1378,7 @@ private: testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3); testEnforceNoRipple(all_); testFillModes(all_); + testFillModes(all_ - featureMPTokensV2); testOfferCrossWithXRP(all_); testOfferCrossWithLimitOverride(all_); testCurrencyConversionEntire(all_); diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp index 7078ea6769..90a267f56f 100644 --- a/src/test/app/AMMMPT_test.cpp +++ b/src/test/app/AMMMPT_test.cpp @@ -32,14 +32,17 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -3269,6 +3272,48 @@ private: ammAlice.expectBalances(MPT(ammAlice[1])(1), XRP(10'000), IOUAmount{100000})); }, {{XRP(10'000), gAmmmpt(10'000)}}); + + // MPT/MPT equal withdrawal after LP deletes both zero-balance MPTokens. + // AMMWithdraw must recreate both missing MPTokens; the invariant allows + // up to two MPToken creations per AMMWithdraw/AMMClawback (threshold > 2). + { + Env env{*this}; + env.fund(XRP(30'000), gw_, alice_); + env.close(); + MPTTester btc( + {.env = env, + .issuer = gw_, + .holders = {alice_}, + .pay = 10'000, + .flags = kMptDexFlags}); + MPTTester eth( + {.env = env, + .issuer = gw_, + .holders = {alice_}, + .pay = 10'000, + .flags = kMptDexFlags}); + + // Alice deposits everything into the MPT/MPT pool; her MPT + // balances drop to zero. + AMM ammAlice(env, alice_, btc(10'000), eth(10'000)); + BEAST_EXPECT(expectMPT(env, alice_, btc(0))); + BEAST_EXPECT(expectMPT(env, alice_, eth(0))); + + // Alice deletes both zero-balance MPTokens to reclaim reserve. + btc.authorize({.account = alice_, .flags = tfMPTUnauthorize}); + eth.authorize({.account = alice_, .flags = tfMPTUnauthorize}); + BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice_.id()))); + BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice_.id()))); + + // Equal withdrawal succeeds: both missing MPTokens are recreated + // (mptokensCreated_ == 2, which satisfies the > 2 invariant check). + ammAlice.withdrawAll(alice_); + BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice_.id()))); + BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice_.id()))); + BEAST_EXPECT(expectMPT(env, alice_, btc(10'000))); + BEAST_EXPECT(expectMPT(env, alice_, eth(10'000))); + BEAST_EXPECT(!ammAlice.ammExists()); + } } void @@ -4041,9 +4086,9 @@ private: { auto jtx = env.jt(tx, Seq(1), Fee(10)); env.app().config().features.erase(featureMPTokensV2); - PreflightContext const pfctx( + PreflightContext const ctx( env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); - auto pf = AMMBid::checkExtraFeatures(pfctx); + auto pf = AMMBid::checkExtraFeatures(ctx); BEAST_EXPECT(pf == false); env.app().config().features.insert(featureMPTokensV2); } @@ -4053,9 +4098,9 @@ private: jtx.jv["Asset2"]["currency"] = "XRP"; jtx.jv["Asset2"].removeMember("mpt_issuance_id"); jtx.stx = env.ust(jtx); - PreflightContext const pfctx( + PreflightContext const ctx( env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal); - auto pf = AMMBid::preflight(pfctx); + auto pf = AMMBid::preflight(ctx); BEAST_EXPECT(pf == temBAD_AMM_TOKENS); } } @@ -4901,7 +4946,7 @@ private: XRP(10'100), MPT(ammAlice[1])(10'000'000000000001), ammAlice.tokens())); env.require(Balance(carol_, MPT(ammAlice[1])(30'199'999999999999))); - // Initial 30,000 - 10000(AMM pool LP) - 100(AMMoffer) - + // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) - // - 100(offer) - 10(tx fee) - 10(tx fee of MPTTester init as // holder) - one reserve BEAST_EXPECT(expectLedgerEntryRoot( @@ -5010,12 +5055,12 @@ private: env.close(); BEAST_EXPECT( - amm.expectBalances(XRPAmount(909'090'909), btc(550'000000055001), amm.tokens())); - // Offer ~91XRP/49.99e12BTC + amm.expectBalances(XRPAmount(909'090'910), btc(549'999999450001), amm.tokens())); + // Offer ~91XRP/50e12BTC BEAST_EXPECT(expectOffers( - env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, btc(4'999999950000)}}})); - // Carol pays 0.1% fee on 50'000000055000BTC = 50'000000055BTC - env.require(Balance(carol_, btc(29'949'949'999'944'943))); + env, carol_, 1, {{Amounts{XRPAmount{9'090'910}, btc(5'000000500000)}}})); + // Carol pays 0.1% fee on 49'999999450001BTC. + env.require(Balance(carol_, btc(29'949'950'000'550'548))); } { @@ -5065,15 +5110,15 @@ private: env.close(); BEAST_EXPECT(ammAlice.expectBalances( - btc(1'060'6848287928033), eth(1'037'0658372213574), ammAlice.tokens())); + btc(1'060'6848287928025), eth(1'037'0658372213582), ammAlice.tokens())); // Consumed offer ~72.93e13ETH/72.93e13BTC BEAST_EXPECT(expectOffers( - env, carol_, 1, {Amounts{eth(27'0658372213574), btc(27'0658372213575)}})); + env, carol_, 1, {Amounts{eth(27'0658372213582), btc(27'0658372213582)}})); BEAST_EXPECT(expectOffers(env, bob_, 0)); BEAST_EXPECT(expectOffers(env, ed, 0)); - env.require(Balance(carol_, btc(19'116'439'640'089'955))); - env.require(Balance(carol_, eth(20'729'341'627'786'426))); + env.require(Balance(carol_, btc(19'116'439'640'089'965))); + env.require(Balance(carol_, eth(20'729'341'627'786'418))); env.require(Balance(bob_, btc(20'100'000'000'000'000))); env.require(Balance(ed, eth(19'875'000'000'000'000))); } @@ -5672,6 +5717,87 @@ private: }); } + void + testAMMOfferGenerationPolicy(FeatureBitset features) + { + testcase("AMM payment offer generation picks economically coarser integral side"); + + using namespace jtx; + + enum class GeneratedFirst { TakerPays, TakerGets }; + + auto const check = [&](std::uint64_t mptUnitsPerXRP, GeneratedFirst generatedFirst) { + TAmounts const pool{ + XRPAmount{1'000'000}, MPTAmount{1'000'000'125}}; + TAmounts const clobOffer{ + kDropsPerXrp, MPTAmount{static_cast(mptUnitsPerXRP)}}; + Quality const clobQuality{clobOffer}; + + auto const expectedAmounts = generatedFirst == GeneratedFirst::TakerGets + ? getAMMOfferStartWithTakerGets(pool, clobQuality, 0) + : getAMMOfferStartWithTakerPays(pool, clobQuality, 0); + auto const otherAmounts = generatedFirst == GeneratedFirst::TakerGets + ? getAMMOfferStartWithTakerPays(pool, clobQuality, 0) + : getAMMOfferStartWithTakerGets(pool, clobQuality, 0); + BEAST_EXPECT(expectedAmounts); + BEAST_EXPECT(otherAmounts); + if (!expectedAmounts || !otherAmounts) + return; + + // Make the tested branch observable: these cases are chosen so the + // payment consumes different AMM amounts depending on which side + // is generated first. + BEAST_EXPECT(*expectedAmounts != *otherAmounts); + + Env env(*this, features); + auto const gw = Account("gw"); + auto const lp = Account("lp"); + auto const maker = Account("maker"); + auto const taker = Account("taker"); + auto const dst = Account("dst"); + + env.fund(XRP(10'000), gw, lp, maker, taker, dst); + env.close(); + + MPTTester const token( + {.env = env, .issuer = gw, .holders = {lp, maker, dst}, .flags = kMptDexFlags}); + env(pay(gw, lp, token(pool.out.value()))); + env(pay(gw, maker, token(10'000'000))); + env.close(); + + AMM const amm(env, lp, drops(pool.in), token(pool.out.value())); + auto const makerOfferSeq = env.seq(maker); + env(offer(maker, XRP(1), token(mptUnitsPerXRP)), Txflags(tfPassive)); + env.close(); + + env(pay(taker, dst, token(expectedAmounts->out.value())), + Sendmax(drops(expectedAmounts->in))); + env.close(); + + BEAST_EXPECT(amm.expectBalances( + drops(pool.in + expectedAmounts->in), + token((pool.out - expectedAmounts->out).value()), + amm.tokens())); + env.require(Balance(dst, token(expectedAmounts->out.value()))); + BEAST_EXPECT(env.le(keylet::offer(maker.id(), SeqProxy::rawSequence(makerOfferSeq)))); + }; + + // CLOB price: 10'000'000 MPT per 1 XRP, so one raw MPT unit is worth + // 0.1 drops. One drop is the economically coarser unit and the AMM + // offer is generated from takerPays. + check(10 * kDropsPerXrp.drops(), GeneratedFirst::TakerPays); + + // CLOB price: 1'000'000 MPT per 1 XRP, so one raw MPT unit is worth + // one drop. Ties use takerGets to preserve the historical XRP-output + // behavior. + check(kDropsPerXrp.drops(), GeneratedFirst::TakerGets); + + // CLOB price: 100'000 MPT per 1 XRP, so one raw MPT unit is worth + // 10 drops. MPT is the economically coarser unit and the AMM offer is + // generated from takerGets. + check(kDropsPerXrp.drops() / 10, GeneratedFirst::TakerGets); + } + void testTradingFee(FeatureBitset features) { @@ -7242,7 +7368,7 @@ private: // overflow. Deposit has no such bound, which is why only the deposit // path was exposed. // - // These mirror the deposit repros: the same oversized two-asset + // These mirror the deposit tests: the same oversized two-asset // request is rejected cleanly. If the preclaim bound is ever weakened, // equalWithdrawLimit would be reached with a huge frac and // Number::operator rep() would escape as tefEXCEPTION, failing this. @@ -7318,6 +7444,7 @@ private: testAMMTokens(); testAmendment(); testAMMAndCLOB(all); + testAMMOfferGenerationPolicy(all); testTradingFee(all); testTradingFee(all - fixAMMv1_3); testAdjustedTokens(all); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index 8f8079c34a..e1732aaf0e 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -3778,6 +3778,21 @@ private: BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens())); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}})); } + else if (!features[featureMPTokensV2]) + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'909), + STAmount{USD, UINT64_C(550'000000055), -9}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + BEAST_EXPECT( + env.balance(carol_, USD) == + STAmount(USD, UINT64_C(29'949'94999999494), -11)); + } else { // Post-amendment the transfer fee is taken into account @@ -3788,19 +3803,19 @@ private: // quality. // AMM offer ~50USD/91XRP BEAST_EXPECT(amm.expectBalances( - XRPAmount(909'090'909), - STAmount{USD, UINT64_C(550'000000055), -9}, + XRPAmount(909'090'910), + STAmount{USD, UINT64_C(549'99999945), -8}, amm.tokens())); - // Offer ~91XRP/49.99USD + // Offer ~91XRP/50USD BEAST_EXPECT(expectOffers( env, carol_, 1, - {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}})); // Carol pays 0.1% fee on ~50USD =~ 0.05USD BEAST_EXPECT( env.balance(carol_, USD) == - STAmount(USD, UINT64_C(29'949'94999999494), -11)); + STAmount(USD, UINT64_C(29'949'95000060055), -11)); } }, {{XRP(1'000), USD(500)}}, @@ -6451,7 +6466,7 @@ private: BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}})); BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}})); } - else + else if (!features[featureMPTokensV2]) { BEAST_EXPECT(amm.expectBalances( XRPAmount(909'090'909), @@ -6464,6 +6479,19 @@ private: {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}})); } + else + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'910), + STAmount{USD, UINT64_C(549'99999945), -8}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}})); + BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}})); + } } // There is no blocking offer, the same AMM liquidity is consumed @@ -6475,10 +6503,30 @@ private: AMM const amm(env, alice_, XRP(1'000), USD(500)); env(offer(carol_, XRP(100), USD(55))); env.close(); - BEAST_EXPECT(amm.expectBalances( - XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens())); - BEAST_EXPECT(expectOffers( - env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + if (!features[featureMPTokensV2]) + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'909), + STAmount{USD, UINT64_C(550'000000055), -9}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}})); + } + else + { + BEAST_EXPECT(amm.expectBalances( + XRPAmount(909'090'910), + STAmount{USD, UINT64_C(549'99999945), -8}, + amm.tokens())); + BEAST_EXPECT(expectOffers( + env, + carol_, + 1, + {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}})); + } } } @@ -7400,6 +7448,7 @@ private: testFlags(); testRippling(); testAMMAndCLOB(all); + testAMMAndCLOB(all - featureMPTokensV2); testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3); testTradingFee(all); testTradingFee(all - fixAMMv1_3); @@ -7419,8 +7468,10 @@ private: testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3); testSwapRounding(); testFixChangeSpotPriceQuality(all); + testFixChangeSpotPriceQuality(all - featureMPTokensV2); testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3); testFixAMMOfferBlockedByLOB(all); + testFixAMMOfferBlockedByLOB(all - featureMPTokensV2); testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3); testLPTokenBalance(all); testLPTokenBalance(all - fixAMMv1_3); diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index 7e7509c3b7..72db63bd3f 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3749,6 +3749,186 @@ struct EscrowToken_test : public beast::unit_test::Suite BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); } + void + testMPTLargeLockedRate(FeatureBitset features) + { + testcase("MPT large locked rate"); + using namespace test::jtx; + using namespace std::literals; + + auto constexpr escrowAmount = 200'000'000'000'000'000LL; + auto constexpr noOverflowEscrowAmount = 186'000'000'000'000'000LL; + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + + for (auto const testFeatures : + {features - featureMPTokensV2 - fixCleanup3_4_0, + features - featureMPTokensV2, + (features | featureMPTokensV2) - fixCleanup3_4_0, + features | featureMPTokensV2}) + { + bool const mptV2 = testFeatures[featureMPTokensV2]; + bool const tokenEscrowV1 = testFeatures[fixTokenEscrowV1]; + // The transfer-fee split in EscrowFinish only overflows on the + // legacy divideRound(amount, lockedRate, ...) path, which runs when + // fixCleanup3_4_0 is disabled. With fixCleanup3_4_0 the split uses + // mulRatio (128-bit intermediate), which cannot overflow. Without + // it, this large amount overflows unless the MPTokensV2 Number path + // is active. So the finish succeeds when either amendment is enabled. + bool const cleanup340 = testFeatures[fixCleanup3_4_0]; + bool const noOverflow = cleanup340 || mptV2; + auto const expectedErr = noOverflow ? Ter(tesSUCCESS) : Ter(tefEXCEPTION); + + // Finish with a large MPT amount and non-zero transfer fee. When the + // computation overflows (legacy divideRound path, no MPTokensV2) the + // finish fails with tefEXCEPTION and the escrow is untouched; + // otherwise it unlocks the escrow. + { + Env env{*this, testFeatures}; + env.fund(XRP(1'000), alice, bob, gw); + auto const baseFee = env.current()->fees().base; + + MPTTester const mpt( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + env(pay(gw, alice, mpt(escrowAmount))); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(escrowAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + escrow::kCancelTime(env.now() + 500s), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount); + + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150), + expectedErr); + env.close(); + + if (noOverflow) + { + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount)); + auto const postBob = env.balance(bob, mpt); + BEAST_EXPECT(postBob.value() > preBob.value()); + BEAST_EXPECT(postBob.value() < (preBob + mpt(escrowAmount)).value()); + auto const xferFee = escrowAmount - (postBob.value() - preBob.value()); + auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee; + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow); + } + else + { + BEAST_EXPECT(env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount)); + BEAST_EXPECT(env.balance(bob, mpt) == preBob); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount); + } + } + + // Control: a still-large amount below the legacy overflow boundary + // finishes successfully in both feature modes. + { + Env env{*this, testFeatures}; + env.fund(XRP(1'000), alice, bob, gw); + auto const baseFee = env.current()->fees().base; + + MPTTester const mpt( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + env(pay(gw, alice, mpt(noOverflowEscrowAmount))); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(noOverflowEscrowAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + escrow::kCancelTime(env.now() + 500s), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == noOverflowEscrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == noOverflowEscrowAmount); + + env(escrow::finish(bob, alice, seq), + escrow::kCondition(escrow::kCb1), + escrow::kFulfillment(escrow::kFb1), + Fee(baseFee * 150), + Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(noOverflowEscrowAmount)); + auto const postBob = env.balance(bob, mpt); + BEAST_EXPECT(postBob.value() > preBob.value()); + BEAST_EXPECT(postBob.value() < (preBob + mpt(noOverflowEscrowAmount)).value()); + auto const xferFee = noOverflowEscrowAmount - (postBob.value() - preBob.value()); + auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee; + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow); + } + + // Cancel returns the escrow to the owner using parity rate, so it + // does not hit the transfer-rate division in either feature mode. + { + Env env{*this, testFeatures}; + env.fund(XRP(1'000), alice, bob, gw); + auto const baseFee = env.current()->fees().base; + + MPTTester const mpt( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .transferFee = 1'000, + .flags = tfMPTCanEscrow | tfMPTCanTransfer}); + env(pay(gw, alice, mpt(escrowAmount))); + env.close(); + + auto const preAlice = env.balance(alice, mpt); + auto const preBob = env.balance(bob, mpt); + auto const seq = env.seq(alice); + env(escrow::create(alice, bob, mpt(escrowAmount)), + escrow::kCondition(escrow::kCb1), + escrow::kFinishTime(env.now() + 1s), + escrow::kCancelTime(env.now() + 3s), + Fee(baseFee * 150)); + env.close(); + + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount); + + env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq)))); + BEAST_EXPECT(env.balance(alice, mpt) == preAlice); + BEAST_EXPECT(env.balance(bob, mpt) == preBob); + BEAST_EXPECT(env.balance(gw, mpt) == -mpt(escrowAmount)); + BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0); + BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0); + } + } + } + void testMPTRequireAuth(FeatureBitset features) { @@ -4047,6 +4227,7 @@ struct EscrowToken_test : public beast::unit_test::Suite testMPTMetaAndOwnership(features); testMPTGateway(features); testMPTLockedRate(features); + testMPTLargeLockedRate(features); testMPTRequireAuth(features); testMPTLock(features); testMPTCanTransfer(features); diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp index a94834eb28..49e3f9be94 100644 --- a/src/test/app/FlowMPT_test.cpp +++ b/src/test/app/FlowMPT_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -742,6 +743,164 @@ struct FlowMPT_test : public beast::unit_test::Suite return result; } + void + testOfferOwnerMPTCreation(FeatureBitset features) + { + using namespace jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const gw("gw"); + + { + testcase("Reserve-edge offer owner cannot create another object"); + + Env env(*this, features); + + auto const baseFee = env.current()->fees().base; + auto const ownerIncrement = reserve(env, 1) - reserve(env, 0); + auto const xrpOffer = ownerIncrement - drops(1); + auto const bobStart = reserve(env, 2) - drops(1) + baseFee; + + env.fund(XRP(10'000), alice, gw); + env.fund(bobStart, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .maxAmt = 10}); + + env(offer(bob, usd(1), xrpOffer)); + env.close(); + + env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1)); + + // This mirrors the full-crossing setup below. Bob has enough XRP + // for the resting offer, but not enough to pay a fee and add + // another owner-count object while the offer remains on ledger. + env(check::create(bob, alice, drops(1)), Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env.require(Owners(bob, 1)); + BEAST_EXPECT(offersOnAccount(env, bob).size() == 1); + } + + { + testcase("Reserve-edge offer owner creates MPToken during consume"); + + Env env(*this, features); + + auto const baseFee = env.current()->fees().base; + auto const ownerIncrement = reserve(env, 1) - reserve(env, 0); + auto const xrpOffer = ownerIncrement - drops(1); + auto const bobStart = reserve(env, 2) - drops(1) + baseFee; + + env.fund(XRP(10'000), alice, carol, gw); + env.fund(bobStart, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10}); + + env(pay(gw, alice, usd(1))); + env(offer(bob, usd(1), xrpOffer)); + env.close(); + + env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1)); + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + auto const carolXRP = env.balance(carol); + + // Bob has enough XRP for the resting offer but is close to + // reserve. The payment should not create Bob's USD MPToken until + // the offer is actually consumed, otherwise the temporary owner + // count increase can make the offer look underfunded during path + // execution. + env(pay(alice, carol, xrpOffer), + Path(~XRP), + Sendmax(usd(1)), + Txflags(tfNoRippleDirect)); + env.close(); + + env.require(Balance(carol, carolXRP + xrpOffer)); + env.require(Balance(bob, usd(1))); + env.require(Balance(bob, reserve(env, 1)), Owners(bob, 1)); + BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + BEAST_EXPECT(offersOnAccount(env, bob).empty()); + } + + { + testcase("Partial offer owner creates MPToken during consume"); + + Env env(*this, features); + + auto const baseFee = env.current()->fees().base; + auto const ownerIncrement = reserve(env, 1) - reserve(env, 0); + auto const bobStart = reserve(env, 3) + baseFee; + + env.fund(XRP(10'000), alice, carol, gw); + env.fund(bobStart, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10}); + + env(pay(gw, alice, usd(1))); + env(offer(bob, usd(2), drops(2 * ownerIncrement))); + env.close(); + + env.require(Balance(bob, reserve(env, 3)), Owners(bob, 1)); + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + auto const carolXRP = env.balance(carol); + + // Partial consumption leaves Bob's offer on the ledger, so he ends + // up owning both the remaining offer and a newly created MPToken. + // The MPToken is created regardless of reserve; this setup simply + // funds Bob enough that he still meets reserve(2) afterward (the + // under-reserved case is covered in OfferMPT_test's no-reserve-check + // testcase). + env(pay(alice, carol, drops(ownerIncrement)), + Path(~XRP), + Sendmax(usd(1)), + Txflags(tfNoRippleDirect)); + env.close(); + + env.require(Balance(carol, carolXRP + drops(ownerIncrement))); + env.require(Balance(bob, usd(1))); + env.require(Balance(bob, reserve(env, 2)), Owners(bob, 2)); + BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id()))); + BEAST_EXPECT(offersOnAccount(env, bob).size() == 1); + BEAST_EXPECT(isOffer(env, bob, usd(1), drops(ownerIncrement))); + } + + { + testcase("Issuer-owned offer does not create issuer MPToken"); + + Env env(*this, features); + + env.fund(XRP(10'000), alice, carol, gw); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10}); + + env(pay(gw, alice, usd(1))); + env(offer(gw, usd(1), drops(1'000))); + env.close(); + + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id()))); + auto const carolXRP = env.balance(carol); + + // The issuer can own an offer that receives its own MPT without an + // MPToken. Consuming that offer should keep the issuer side + // tokenless. + env(pay(alice, carol, drops(1'000)), + Path(~XRP), + Sendmax(usd(1)), + Txflags(tfNoRippleDirect)); + env.close(); + + env.require(Balance(alice, usd(0))); + env.require(Balance(carol, carolXRP + drops(1'000))); + BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id()))); + BEAST_EXPECT(offersOnAccount(env, gw).empty()); + } + } + void testSelfPayment1(FeatureBitset features) { @@ -2121,6 +2280,7 @@ struct FlowMPT_test : public beast::unit_test::Suite testFalseDry(features); testDirectStep(features); testBookStep(features); + testOfferOwnerMPTCreation(features); testTransferRate(features); testSelfPayment1(features); testSelfPayment2(features); diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index d03b1b8e93..e262954fdf 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -5,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +25,7 @@ #include #include +#include #include #include #include @@ -35,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -609,6 +615,267 @@ public: testHelper2TokensMix(test); } + void + testMPTIssuerOfferUsesRemainingCapacity(FeatureBitset features) + { + testcase("MPT issuer offer dust removal uses remaining issuance capacity"); + + using namespace jtx; + + Account const issuer{"issuer"}; + Account const carol{"carol"}; + Account const bob{"bob"}; + + Env env{*this, features}; + env.fund(XRP(10'000), issuer, carol, bob); + env.close(); + + MPTTester const musd( + {.env = env, .issuer = issuer, .holders = {carol, bob}, .maxAmt = 101}); + + // The issuer offer is fully fundable when placed. Later issuance leaves + // only one MPT of remaining capacity, so this issuer-owned MPT offer + // must be clipped by owner funds just like a holder-funded offer. + auto const issuerOfferSeq = env.seq(issuer); + env(offer(issuer, drops(1), musd(100))); + env.close(); + + env(pay(issuer, carol, musd(100))); + env.close(); + BEAST_EXPECT(env.balance(issuer, musd) == musd(-100)); + BEAST_EXPECT(env.balance(carol, musd) == musd(100)); + + // Carol's same-quality offer provides the legitimately funded side of + // the crossing. Without the issuer-cap dust-removal check, Bob would + // receive Carol's 100 MPT plus one free self-issued MPT from issuer's + // stale offer while paying only Carol's one drop. + auto const carolOfferSeq = env.seq(carol); + env(offer(carol, drops(1), musd(100))); + env.close(); + + auto const issuerOffer = keylet::offer(issuer.id(), SeqProxy::rawSequence(issuerOfferSeq)); + auto const carolOffer = keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq)); + BEAST_EXPECT(env.le(issuerOffer) != nullptr); + BEAST_EXPECT(env.le(carolOffer) != nullptr); + + env(offer(bob, musd(101), drops(2), tfImmediateOrCancel)); + env.close(); + + BEAST_EXPECT(env.le(issuerOffer) == nullptr); + BEAST_EXPECT(env.le(carolOffer) == nullptr); + env.require(offers(issuer, 0), offers(carol, 0), offers(bob, 0)); + BEAST_EXPECT(env.balance(issuer, musd) == musd(-100)); + BEAST_EXPECT(env.balance(carol, musd) == musd(0)); + BEAST_EXPECT(env.balance(bob, musd) == musd(100)); + } + + void + testPartiallyFundedMPTInputOfferZeroInput(FeatureBitset features) + { + using namespace jtx; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + + { + testcase("Partially funded MPT/XRP input offer cannot be consumed for free"); + + Env env{*this, features}; + auto const gw = Account{"gw"}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}}); + + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), drops(1'000'000))); + env.close(); + + auto const targetBalance = reserve(env, 2) + drops(999'999); + auto const drain = env.balance(alice).value().xrp() - targetBalance.value().xrp() - + env.current()->fees().base; + env(pay(alice, gw, drops(drain))); + env.close(); + + auto const aliceXRPBefore = env.balance(alice); + auto const bobXRPBefore = env.balance(bob); + + env(pay(gw, bob, drops(1'000'000)), + Sendmax(usd(1)), + Path(~XRP), + Txflags(tfNoRippleDirect | tfPartialPayment), + Ter(tecPATH_DRY)); + env.close(); + + // alice's offer sells 1,000,000 drops for usd(1) but she can fund + // only 999,999. Filling the clipped remainder would require a + // fractional usd (MPT) input that rounds down to zero, so without + // the fix the taker could take the funded drops for free. + // shouldRmSmallIncreasedQOffer() now treats the MPT input as + // integral (like XRP) and removes the degraded offer, so the + // payment goes dry. The removal happens only inside the crossing: + // tecPATH_DRY discards everything but the fee, so the offer itself + // stays in the ledger, unconsumed. + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr); + BEAST_EXPECT(env.balance(alice) == aliceXRPBefore); + BEAST_EXPECT(env.balance(bob) == bobXRPBefore); + } + + { + testcase("Partially funded MPT/IOU input offer cannot be consumed for free"); + + Env env{*this, features}; + auto const mptIssuer = Account{"mptIssuer"}; + auto const iouIssuer = Account{"iouIssuer"}; + + env.fund(XRP(10'000), mptIssuer, iouIssuer, alice, bob); + env.close(); + + auto const eur = iouIssuer["EUR"]; + env.trust(eur(100), alice, bob); + env(pay(iouIssuer, alice, eur(0.5))); + env.close(); + + MPTTester const usd({.env = env, .issuer = mptIssuer, .holders = {alice}}); + + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), eur(1))); + env.close(); + + auto const aliceEURBefore = env.balance(alice, eur); + auto const bobEURBefore = env.balance(bob, eur); + + env(pay(mptIssuer, bob, eur(1)), + Sendmax(usd(1)), + Path(~eur), + Txflags(tfNoRippleDirect | tfPartialPayment), + Ter(tecPATH_DRY)); + env.close(); + + // Same zero-input regression as the MPT/XRP case above, but with + // an IOU (eur) output leg: the fractional usd (MPT) input rounds + // to zero. The degraded offer is removed during crossing, the + // payment goes dry, and tecPATH_DRY leaves the offer in the ledger. + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr); + BEAST_EXPECT(env.balance(alice, eur) == aliceEURBefore); + BEAST_EXPECT(env.balance(bob, eur) == bobEURBefore); + } + + { + testcase("Partially funded MPT/MPT input offer cannot be consumed for free"); + + Env env{*this, features}; + auto const issuerA = Account{"issuerA"}; + auto const issuerB = Account{"issuerB"}; + + env.fund(XRP(10'000), issuerA, issuerB, alice, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = issuerA, .holders = {alice}}); + MPTTester const eur({.env = env, .issuer = issuerB, .holders = {alice, bob}}); + + env(pay(issuerB, alice, eur(999'999))); + env.close(); + + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), eur(1'000'000))); + env.close(); + + auto const aliceEURBefore = eur.getBalance(alice); + auto const bobEURBefore = eur.getBalance(bob); + + env(pay(issuerA, bob, eur(1'000'000)), + Sendmax(usd(1)), + Path(~eur), + Txflags(tfNoRippleDirect | tfPartialPayment), + Ter(tecPATH_DRY)); + env.close(); + + // Same zero-input regression as above, but with both legs MPT: the + // fractional usd (MPT) input rounds to zero. The degraded offer is + // removed during crossing, the payment goes dry, and tecPATH_DRY + // leaves the offer in the ledger. + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr); + BEAST_EXPECT(env.balance(alice, eur) == eur(aliceEURBefore)); + BEAST_EXPECT(env.balance(bob, eur) == eur(bobEURBefore)); + } + + { + // The dry cases above never observe the degraded offer actually + // being removed, because tecPATH_DRY rolls the removal back. Here a + // second, fully funded offer lets the crossing succeed, so the + // removal persists: alice's degraded offer is deleted from the + // book (not taken for free) while carol's good offer fills. + testcase( + "Partially funded MPT input offer is removed, not consumed, " + "when a funded offer crosses"); + + Env env{*this, features}; + auto const gw = Account{"gw"}; + auto const carol = Account{"carol"}; + + env.fund(XRP(10'000), gw, alice, carol, bob); + env.close(); + + MPTTester const usd({.env = env, .issuer = gw, .holders = {alice, carol, bob}}); + + // alice's offer sells 1,000,000 drops for usd(1) but, as in the + // dry cases above, she can fund only 999,999 drops, so filling the + // clipped remainder would require a fractional usd (MPT) input that + // rounds down to zero. + auto const aliceOfferSeq = env.seq(alice); + env(offer(alice, usd(1), drops(1'000'000))); + env.close(); + + auto const targetBalance = reserve(env, 2) + drops(999'999); + auto const drain = env.balance(alice).value().xrp() - targetBalance.value().xrp() - + env.current()->fees().base; + env(pay(alice, gw, drops(drain))); + env.close(); + + // carol's same-quality offer is fully funded and provides the + // legitimate side of the crossing. + auto const carolOfferSeq = env.seq(carol); + env(offer(carol, usd(1), drops(1'000'000))); + env.close(); + + // bob needs usd to buy drops. + env(pay(gw, bob, usd(2))); + env.close(); + + auto const aliceOffer = keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq)); + auto const carolOffer = keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq)); + BEAST_EXPECT(env.le(aliceOffer) != nullptr); + BEAST_EXPECT(env.le(carolOffer) != nullptr); + + auto const aliceXRPBefore = env.balance(alice); + auto const bobXRPBefore = env.balance(bob); + + // bob buys drops with usd, wanting more than carol alone supplies so + // the crossing also reaches alice's offer. carol's offer fills; + // alice's degraded offer is removed rather than taken for free, so + // bob receives only carol's 1,000,000 drops and pays only usd(1). + env(offer(bob, drops(2'000'000), usd(2), tfImmediateOrCancel)); + env.close(); + + BEAST_EXPECT(env.le(aliceOffer) == nullptr); + BEAST_EXPECT(env.le(carolOffer) == nullptr); + env.require(offers(alice, 0), offers(carol, 0), offers(bob, 0)); + + // alice's offer was removed, not consumed: her balances are + // unchanged and none of her funded 999'999 drops leaked to bob. + BEAST_EXPECT(env.balance(alice) == aliceXRPBefore); + BEAST_EXPECT(env.balance(alice, usd) == usd(0)); + BEAST_EXPECT(env.balance(carol, usd) == usd(1)); + BEAST_EXPECT(env.balance(bob, usd) == usd(1)); + BEAST_EXPECT( + env.balance(bob) == bobXRPBefore + drops(1'000'000) - env.current()->fees().base); + } + } + void testInsufficientReserve(FeatureBitset features) { @@ -947,6 +1214,161 @@ public: } } + void + testMPTAMMLimitQualityRounding(FeatureBitset features) + { + testcase("MPT AMM limitQuality checks rounded integral output"); + + using namespace jtx; + + Account const gw{"gateway"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + // IOC used to reject the AMM strand with tecKILLED. The continuous + // limitQuality target is about 32.88 MPT; rounding to nearest requested + // 33 MPT and made the realized AMM quality miss Bob's limit. The + // discrete fallback takes the largest satisfying integer output: 32. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 100'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, XRP(100), btc(1'000)); + + auto const bobBTCBefore = btc.getBalance(bob); + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, btc(100), drops(10'340'000)), Txflags(tfImmediateOrCancel)); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + BEAST_EXPECT(btc.getBalance(bob) == bobBTCBefore + 32); + BEAST_EXPECT(xrpAfter > xrpBefore); + BEAST_EXPECT(btcAfter < btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 0)); + } + + // A standard OfferCreate at the same limit used to bypass the AMM and + // rest unchanged on the book. It should now take the largest + // satisfying 32-MPT AMM fill first, then leave only the remainder on + // the book. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 100'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, XRP(100), btc(1'000)); + + auto const bobBTCBefore = btc.getBalance(bob); + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, btc(100), drops(10'340'000))); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + BEAST_EXPECT(btc.getBalance(bob) == bobBTCBefore + 32); + BEAST_EXPECT(xrpAfter > xrpBefore); + BEAST_EXPECT(btcAfter < btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 1)); + + auto const bobOffers = offersOnAccount(env, bob); + if (BEAST_EXPECT(bobOffers.size() == 1)) + { + BEAST_EXPECT((*bobOffers[0])[sfTakerPays] != btc(100)); + BEAST_EXPECT((*bobOffers[0])[sfTakerGets] != drops(10'340'000)); + } + } + + // Mirror the IOC case with the integral output flipped from MPT units + // to XRP drops. The same continuous target (~32.88) used to round up + // to 33 drops and miss limitQuality; the discrete fallback allows the + // largest satisfying 32-drop AMM fill. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 200'000'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, drops(1'000), btc(100'000'000)); + + auto const bobXRPBefore = env.balance(bob, XRP); + auto const baseFee = env.current()->fees().base; + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, drops(100), btc(10'340'000)), Txflags(tfImmediateOrCancel)); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + env.require(Balance(bob, bobXRPBefore + drops(32) - baseFee)); + BEAST_EXPECT(xrpAfter < xrpBefore); + BEAST_EXPECT(btcAfter > btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 0)); + } + + // Mirror the standard OfferCreate case as well. It should consume the + // largest satisfying 32-drop AMM fill before leaving only the remainder + // on the book. + { + Env env{*this, features}; + + env.fund(XRP(10'000), gw, alice, bob); + env.close(); + + MPTTester const btc( + {.env = env, + .issuer = gw, + .holders = {alice, bob}, + .pay = 200'000'000, + .flags = kMptDexFlags}); + AMM const amm(env, alice, drops(1'000), btc(100'000'000)); + + auto const bobXRPBefore = env.balance(bob, XRP); + auto const baseFee = env.current()->fees().base; + auto const [xrpBefore, btcBefore, lpBefore] = amm.balances(); + + env(offer(bob, drops(100), btc(10'340'000))); + env.close(); + + auto const [xrpAfter, btcAfter, lpAfter] = amm.balances(); + env.require(Balance(bob, bobXRPBefore + drops(32) - baseFee)); + BEAST_EXPECT(xrpAfter < xrpBefore); + BEAST_EXPECT(btcAfter > btcBefore); + BEAST_EXPECT(lpAfter == lpBefore); + BEAST_EXPECT(expectOffers(env, bob, 1)); + + auto const bobOffers = offersOnAccount(env, bob); + if (BEAST_EXPECT(bobOffers.size() == 1)) + { + BEAST_EXPECT((*bobOffers[0])[sfTakerPays] != drops(100)); + BEAST_EXPECT((*bobOffers[0])[sfTakerGets] != btc(10'340'000)); + } + } + } + void testMalformed(FeatureBitset features) { @@ -2727,6 +3149,50 @@ public: using namespace jtx; auto const gw1 = Account("gateway1"); + { + auto const issuer = Account("issuer"); + auto const sender = Account("sender"); + auto const receiver = Account("receiver"); + auto const seller = Account("seller"); + auto const buyer = Account("buyer"); + + Env env{*this, features}; + env.fund(XRP(10'000), issuer, sender, receiver, seller, buyer); + env.close(); + + MPTTester mpt{ + {.env = env, + .issuer = issuer, + .holders = {sender, receiver, seller, buyer}, + .transferFee = 100}}; + MPT const token = mpt; + + mpt.pay(issuer, sender, 2'000); + mpt.pay(issuer, seller, 2'000); + + // A direct holder-to-holder payment of 999 MPT at a 0.1% fee + // requires 1000 from the sender and burns one MPT. + env(pay(sender, receiver, token(999)), Ter(tecPATH_PARTIAL)); + env.close(); + env(pay(sender, receiver, token(999)), Sendmax(token(1'000))); + env.close(); + + BEAST_EXPECT(mpt.getBalance(sender) == 1'000); + BEAST_EXPECT(mpt.getBalance(receiver) == 999); + BEAST_EXPECT(mpt.getBalance(issuer) == 3'999); + + // CLOB crossing should apply the same fee quantum. The offer + // owner pays ceil(999 * 1.001) = 1000, not floor(...) = 999. + env(offer(seller, XRP(999), token(999))); + env.close(); + env(offer(buyer, token(999), XRP(999))); + env.close(); + + BEAST_EXPECT(mpt.getBalance(seller) == 1'000); + BEAST_EXPECT(mpt.getBalance(buyer) == 999); + BEAST_EXPECT(mpt.getBalance(issuer) == 3'998); + } + auto test = [&](auto&& issue1, auto&& issue2) { Env env{*this, features}; @@ -3102,6 +3568,247 @@ public: } } + void + testTransferRateOverflowOffer(FeatureBitset features) + { + testcase("Transfer Rate Overflow Offer"); + + using namespace jtx; + + auto const issuer = Account("issuer"); + auto const taker = Account("taker"); + + { + Env env{*this, features}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + auto constexpr takerFunds = 2'000'000'000'000'000'000LL; + MPTTester const token{ + {.env = env, + .issuer = issuer, + .holders = {taker}, + .transferFee = 50'000, + .pay = takerFunds, + .maxAmt = kMaxMpTokenAmount}}; + + // Covers OfferCreate::flowCross() sendMax calculation. A large + // non-issuer MPT offer with a transfer fee used to overflow in + // multiplyRound() before the offer could be placed. + auto constexpr offerAmount = 1'230'000'000'000'000'000LL; + auto const takerSeq = env.seq(taker); + env(offer(taker, XRP(1), token(offerAmount))); + env.close(); + + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + BEAST_EXPECT(env.balance(taker, token) == token(takerFunds)); + } + + // Each scenario below targets a BookStep/OfferStream overflow path. + // The expected behavior is the same in all cases: remove the unusable + // book tip offer and let the taker's crossing offer remain rather than + // returning tecINTERNAL with the poison offer still on-ledger. + { + Env env{*this, features}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + MPTTester const token{ + {.env = env, .issuer = issuer, .holders = {taker}, .transferFee = 10'000}}; + + // Covers BookStep::forEachOffer() offer preparation, where + // ownerGives = mulRatio(ofrAmt.out, transferRateOut) overflowed + // for an oversized MPT output with a transfer fee. + std::int64_t const poisonAmount = 8'500'000'000'000'000'000LL; + auto const poisonSeq = env.seq(issuer); + env(offer(issuer, XRP(1), token(poisonAmount))); + env.close(); + + auto const poisonKeylet = keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(100), XRP(100))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + } + + { + auto const gwA = Account("gatewayA"); + auto const gwB = Account("gatewayB"); + auto const alice = Account("alice"); + auto const mallory = Account("mallory"); + + Env env{*this, features}; + env.fund(XRP(10'000), gwA, gwB, alice, mallory); + env.close(); + + MPTTester const tokenA{ + {.env = env, .issuer = gwA, .holders = {alice, mallory}, .transferFee = 50'000}}; + + MPTTester const tokenB{{.env = env, .issuer = gwB, .holders = {alice, mallory}}}; + + env(pay(gwA, alice, tokenA(1'000))); + + // Covers BookStep::forEachOffer() offer preparation, where + // stpAmt.in = mulRatio(ofrAmt.in, transferRateIn) overflowed. + // The MPT/MPT amounts keep the offer quality reachable while + // applying tokenA's transfer rate overflows the input side. + std::int64_t const poisonPays = 6'148'914'691'236'517'205LL; + std::int64_t const poisonGets = 34'000'000'000'000'000LL; + env(pay(gwB, mallory, tokenB(poisonGets))); + + auto const poisonSeq = env.seq(mallory); + env(offer(mallory, tokenA(poisonPays), tokenB(poisonGets))); + env.close(); + + auto const poisonKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const aliceSeq = env.seq(alice); + env(offer(alice, tokenB(1), tokenA(100))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceSeq))) != nullptr); + } + + { + Env env{*this, features}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + MPTTester const token{ + {.env = env, .issuer = issuer, .holders = {taker}, .maxAmt = kMaxMpTokenAmount}}; + + // Give the taker exactly one MPT. If the old rounding overflow + // collapsed the required input to the minimum positive amount, the + // taker could afford the bad fill and the balance checks below + // would catch the economic gain. + env(pay(issuer, taker, token(1))); + env.close(); + + // Covers BookStep::revImp() output reduction. The issuer's offer + // is fully funded and has no transfer fee, so offer preparation + // succeeds. The taker asks for slightly less output, forcing + // limitStepOut() to reduce the offer; that strict reduction used + // to overflow and leave the poison offer on the book. + auto const funded = 1'844'674'407'370'955'162LL; + auto const offerOut = funded + 1; + + auto const poisonSeq = env.seq(issuer); + env(offer(issuer, XRP(1), token(offerOut))); + env.close(); + + auto const poisonKeylet = keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const issuerXRPBefore = env.balance(issuer, XRP); + auto const takerXRPBefore = env.balance(taker, XRP); + auto const takerMPTBefore = env.balance(taker, token); + auto const fee = env.current()->fees().base; + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(funded), XRP(1))); + env.close(); + + // The former overflow point must not turn into a near-free fill: + // the unusable offer is removed, the taker's offer remains, and no + // value changes hands beyond the taker's transaction fee. + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + BEAST_EXPECT(env.balance(issuer, XRP) == issuerXRPBefore); + BEAST_EXPECT(env.balance(taker, XRP) == takerXRPBefore - fee); + BEAST_EXPECT(env.balance(taker, token) == takerMPTBefore); + } + + { + auto const poisonMaker = Account("poisonMaker"); + + Env env{*this, features}; + env.fund(XRP(10'000), issuer, poisonMaker, taker); + env.close(); + + MPTTester const token{ + {.env = env, + .issuer = issuer, + .holders = {poisonMaker, taker}, + .maxAmt = kMaxMpTokenAmount}}; + + // Covers OfferStream::step() filtering. The offer is mostly + // funded, but reducing it to the actual owner funds inside + // shouldRmSmallIncreasedQOffer() used to overflow before BookStep + // saw the offer. + auto const funded = 1'844'674'407'370'955'162LL; + auto const offerOut = funded + 1; + env(pay(issuer, poisonMaker, token(funded))); + + auto const poisonSeq = env.seq(poisonMaker); + env(offer(poisonMaker, XRP(1), token(offerOut))); + env.close(); + + auto const poisonKeylet = + keylet::offer(poisonMaker.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(1), XRP(1))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + BEAST_EXPECT(env.balance(poisonMaker, token) == token(funded)); + BEAST_EXPECT(env.balance(taker, token) == token(0)); + } + + { + // Same overflow scenario as the ownerGives case above, but run with + // trace-level logging so BookStep::forEachOffer's removeOffer() + // emits its "Removing offer with overflowing amount calculation" + // trace line. This exercises the JLOG body inside removeOffer, + // which is skipped when logging is above trace severity. + std::string logs; + { + Env env{ + *this, + envconfig(), + features, + std::make_unique(&logs), + beast::Severity::Trace}; + env.fund(XRP(10'000), issuer, taker); + env.close(); + + MPTTester const token{ + {.env = env, .issuer = issuer, .holders = {taker}, .transferFee = 10'000}}; + + std::int64_t const poisonAmount = 8'500'000'000'000'000'000LL; + auto const poisonSeq = env.seq(issuer); + env(offer(issuer, XRP(1), token(poisonAmount))); + env.close(); + + auto const poisonKeylet = + keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq)); + BEAST_EXPECT(env.le(poisonKeylet) != nullptr); + + auto const takerSeq = env.seq(taker); + env(offer(taker, token(100), XRP(100))); + env.close(); + + BEAST_EXPECT(env.le(poisonKeylet) == nullptr); + BEAST_EXPECT( + env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr); + } + BEAST_EXPECT(logs.contains("Removing offer with overflowing amount calculation")); + } + } + void testSelfCrossOffer1(FeatureBitset features) { @@ -4920,6 +5627,7 @@ public: testSellOffer(features); testSellWithFillOrKill(features); testTransferRateOffer(features); + testTransferRateOverflowOffer(features); testSelfCrossOffer(features); testSelfIssueOffer(features); testDirectToDirectPath(features); @@ -4934,8 +5642,11 @@ public: testDeletedOfferIssuer(features); testTicketOffer(features); testTicketCancelOffer(features); + testMPTAMMLimitQualityRounding(features); testRmSmallIncreasedQOffersXRP(features); testRmSmallIncreasedQOffersMPT(features); + testMPTIssuerOfferUsesRemainingCapacity(features); + testPartiallyFundedMPTInputOfferZeroInput(features); testFillOrKill(features); testTickSize(features); testAutoCreateReserve(features); diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp index f6c5a94752..c3a681cf01 100644 --- a/src/test/protocol/STAmount_test.cpp +++ b/src/test/protocol/STAmount_test.cpp @@ -1,16 +1,21 @@ #include +#include #include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -24,6 +29,7 @@ #include #include #include +#include namespace xrpl { @@ -990,6 +996,84 @@ public: } } + void + testMPTRateRounding() + { + testcase("MPT transfer rate rounding uses Number arithmetic"); + + MPTIssue const asset{makeMptID(1, AccountID(0x4985601))}; + Rate const transferRate{1'500'000'000}; + STAmount const largeAmount{asset, UINT64_C(1'230'000'000'000'000'000)}; + STAmount const scaledAmount{asset, UINT64_C(1'845'000'000'000'000'000)}; + + auto rules = [](bool const mptV2) { + // Rules keeps a reference to the presets set, so use static + // storage here rather than a local temporary. + static std::unordered_set> const kNoFeatures; + static std::unordered_set> const kMptV2Features{ + featureMPTokensV2}; + return Rules{mptV2 ? kMptV2Features : kNoFeatures}; + }; + + auto throwsOverflow = [&](auto&& f, bool expected = true) { + bool threw = false; + try + { + f(); + } + catch (std::overflow_error const&) + { + threw = true; + } + BEAST_EXPECT(threw == expected); + }; + + { + CurrentTransactionRulesGuard const rg(rules(false)); + + throwsOverflow([&] { (void)multiplyRound(largeAmount, transferRate, asset, true); }); + throwsOverflow([&] { (void)divideRound(scaledAmount, transferRate, asset, true); }); + } + + { + CurrentTransactionRulesGuard const rg(rules(true)); + + throwsOverflow( + [&] { (void)multiplyRound(largeAmount, transferRate, asset, true); }, false); + throwsOverflow( + [&] { (void)divideRound(scaledAmount, transferRate, asset, true); }, false); + } + + { + CurrentTransactionRulesGuard const rg(rules(true)); + STAmount const one{asset, 1}; + STAmount const two{asset, 2}; + + BEAST_EXPECT(multiplyRound(one, transferRate, asset, true) == two); + BEAST_EXPECT(multiplyRound(one, transferRate, asset, false) == one); + BEAST_EXPECT(divideRound(two, transferRate, asset, true) == two); + BEAST_EXPECT(divideRound(two, transferRate, asset, false) == one); + + BEAST_EXPECT(multiplyRound(largeAmount, transferRate, asset, true) == scaledAmount); + BEAST_EXPECT(divideRound(scaledAmount, transferRate, asset, true) == largeAmount); + } + + { + // mulRound with an integral (XRP) operand whose mantissa is below + // kMinValue exercises the legacy value-scaling loop that normalizes + // the mantissa before multiply. The MPTokensV2 Number path is + // not taken here because the target asset is an IOU. + Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)}; + STAmount const iouVal{usd, 5}; + STAmount const xrpVal{XRPAmount{7}}; // integral, mantissa < kMinValue + + auto const up = mulRound(iouVal, xrpVal, usd, /*roundUp*/ true); + auto const down = mulRound(iouVal, xrpVal, usd, /*roundUp*/ false); + BEAST_EXPECT(down.signum() > 0); + BEAST_EXPECT(up >= down); + } + } + void testCanSubtractXRP() { @@ -1267,6 +1351,7 @@ public: testCanAddXRP(); testCanAddIOU(); testCanAddMPT(); + testMPTRateRounding(); testCanSubtractXRP(); testCanSubtractIOU(); testCanSubtractMPT();

    AaLi9S$CuuB?2~`qvfQM&JShVuX|H+j{%OMA}w z^re^ZqQ38aV=?ideEQBespaBT{ASlkartGh{PeeoMd!@*u>S(O=RC9o4YK#EU$Qv* z?~gs$!lDLw9dlj%bt@QDFJy%CcPd<|b@{r@t=Gmz*6reU@wYl}bL}eZn15^cuHHMA zf6e(d*MrNaxI_GY=YH3#`B!_V*BxDVY~6`X=zKziyUlAXsH*u zJ3re8sj*sZ8fiR=Em7ikx7e6fl(+)XAgQXe=t{ezPez}Qve7C0$bo^>c!+F5`JHMY zZ7sV)Pllc+x=`gAn!+O?Xsfy=mg1!<)Gj5ik)>&Zg-iQw*hXl)b+d%Gf}W(Bp&O|n zJlKglr`SeS?_R3Sx^cgI+|9V3V}1o9gNE%x9dJu4i0XR&)^$S2y6EOw@Q6X2om8~W3x0flNviNr$#@fh13{{ zR7;J@#{sW~#t28M1hCD%N5a`k^2``vYeSHb_RtvAy!}MgWrhY*W5D}TM5EjPTJ+s0 z)Ql0nMz|R<19bF=69(2C@F3hH)t`Vdv!YF>is%0JZnl_-*))Xu0`o!DCZmb#W_QpulV|>FWla%GZ)15`BV|xmu~9H_bj+zyYSR4%>&U~Wd7F0d541&DmGO1-+|M%FuSUJ z*55|${Ib@prOKn$MYmD7Jk?D;pVT(_9IJ%wxu~Vo6(bsEkW|D%`9iz_hd5$t4beVA z4KxO@ftWN&Z9~oM+EBwW$b6)ls3J@IYrw!E7ifV?TPnLIPy)_4$^)Y|6=)U7F)ss88BBiOEe?{@uhXzWI$dT4kZJz z%4Uf|JhLDlD0E>Z-h${XLjrn-D4~vpV^w+jZ5T%v` z+>n?y>PBe!ANNX7@7?Jrht{*g9QrnYX~DT8UaL`?Q|IQ})g~<)?8-G>vD9AdnQK2M zY;&1{9(&e^E&ScH7v0#s`U3U)b3a<6xaf`v`CQy}--X%y@;TpynP4Ph)wZo>>B zqWhZ5$JrUseHG?~s_v`NAdf|`kxf2kI2AY<_78iB>Px_}lm%k>nbxZ|y0jXvq|wxc z;5nU*ZrQO}1#>+$`db8s)-YI5XhtFZ97%olUm?BOcaF$Z0#r4Y71{yUI?$QOJ_TK& zj&P+7_JYlA^Mo~Z+K^eXL|lqn@w97-+ICCPRdBa^mP$)BUD|F}w|j}_a_Q^RgPL!6 z9`f$4`yP5+dQ|f*_qRNcdw(n)&>YkrbUp2U#`COqy6!iwkF_7WKJ_&0*5Eq2k8L>lZm^b3iVZq~T3 z^^gEO1!-WsCQDvZWy&Po*Hj?M)A|C^Olug;6n_4oT!+W&DysplNP5s@&4-9G=?1;c(w^eqa9?&WdLrIz{ zzyCC(nuv~U{X4eLTGSFUt+f^<#ai^jNjW2;CQYjERm_=G!6QELipnQeG`v{cjGy$y zllI@d;gocBd7R4I!oIm@lXHh0iJ-X&G`pA*&dmvoq0Mj9m~_!7XAbn7`U}gqWMz$n znDVD2k_fnq14Zz!D)_QDz!McsCZwLX>5+>nZZ7>)`Mp|f*r{4b*eS{yNMM#~s01Im zoQPvIR-&$%Psf9KqZ(aku(F$u>TA?;o~>|zTGV(A|6U~oY#S55U^__X$uv zaIsR0Hc)ENq@0E(g-xU^oKf)9!Vh#Vw<{AhwhRhlobKR6bO$HG09gQ4AZ5(Z&6;O{ z7r+f^tMZ&W4`(Gdk+fKqWWaq#ot1cr!zdnF9uJ@{ z#*#{~Tt;reWF(A8#^iPxn;L`K2`w|M9oIgoWwjqFjFp<#6s4bR69lD^Y_F?qspCV# z?F29@8x1OP24j=}&XmYrW9R9;#;j2$sy~HKC0h^;QZyga$8ynv*redB0WQs&76muz zGN%>&n&3dBd!TA;wAo%WepiI_cui1fcAOz8@D-_o#K>2}WEkv;E!wqw#|5Kzj(mS< zOQOkH?448Gt#PYe4hLLOoYxqyUa@KZc^9Yyxom_jj=iyE!&SGvKJ)NayQyLBgA1Dj z(I|H48aJ^Q4dz_Nt#jYs9Bv;t_mV@u8a>x#A=1NF%E$3$^di!sl*nm}oM z!y%LbCi~U)LN;@)>l*JhzMB#wnXmc8Ev}~{&m{iE`!Bw?BZ50FXA-euv>2b4$Ys{X zFOQF8#xuH?5%&0!zFyyN-T&g{A5Y+)M&5G19eFGMX5zz$;8Vl?gk%hQrBEFXdc{!K z6!h9dVdPiR_5MUDygbZ=!=k-D0V)zBiIN3*WKYhcdPY3FX9=~e&o32F2CJFL46{2k zojICeGigjWPhe`G!!)!M*O`o!jouYU6;(crk2GYa@byoH$QFv!xo2#l=#xp%<;O#P zoy4ucHhkcv2??R3VF(0ChGwY7S`$Qlk)+e-iY8)7CkaXO!6ojlZ^lt?xVZ*dKWj?7L`kRKH%lMimLP;qUlj zefimEn^#9|-gsX#{@cM{P5jF%jbjT6=ld_afAMWAoBNqB%w0PkOh=<_!E4y7NUnF^ zjo&?LT%^^0b9~_bUMrDD5fXAh9oMj@cgqh_{cVtX0-e?1msQJhHBBo=!b!-|k{Z=S zRz8=atx}DxJ+)RnwN|7?4Utwo3l=S)?H15bngukJX2CKVF0-um+_IWxx2)E|GO{<` zP1-Kgl5Uoz=;JB+c&e2~^^%HKnLO4?c5&#)bFDOPtd**jq-iTd?=z&2*0Z8{Na%ev z1}#^m^)FWXQ0l8OYHzDL(o|t;yg=1a_TX@5o4OYhVy`S(xk@E%va9g&Rhw6BUB#|i zEi7tuMbkR5Bh6Q&i!4zzhlWx|C5!DEIteM=z#8_ zcb&+KD_5-+U5$&(lsA|aYA#k%RINV}|T3SF$3zjJ)9;jkK9%v;33#o3cXy`Qa zcY3$Abzm9U!$vBX)f7|6d_wOoTQ)dwX7n1plF&7J!W0ml^N~`CSdc&joiy~W9C#k} zl;20)aAx7mmES+$0p`|Dbh zfkw!vy1He_z@nuzE(tW%{ozzHkegx+`@;*8fgZ@H^TVs+eG66wR&`0q);_hEOh`zK zE?T{Ye3)ojuhoeH$BT=48gnkEcF^hc$mU2Wr{ED~Qel)STu@D|$xJHJmTSc$t&^=x zE2(qztyvIRwk+7!-^YyiP4qFSPwr#-h!AgcIY zy2mR?$GP1XcfE&7)NEa%|9#XYRE^*$qSHEXO$)Az=uL)bI2O@|eAsBJGe*zUg2%`& z%TU)Y=(|)2{(q>#)etG_+Z9FUXEFb$tHm>PhFFu10%bE@~{`w%^~XR2~iprQWFJtXoGoEixl)Sp*mO4YYEl4gI*j8Yl2>L zC~Pq^43=DON*mnNGvwyTqwYG*h%_!8lh`uZAlxqvOYC}SS~?2KUgeiUC}WWb&X8vz zGpG8=tECOfNN7BCEX3yMPXw?;kYP6bD~Q(s19bZy00V&r%77@JseE2?jsExTcXsMm zsBX_=ZkTb-AS0Wk&=fPRS~WO@ zRHw0IF+q3z=t;z(DJG$MBn{=o8p?JxloxC2oQ_~BK=&JyCs05RtPku7aDiu-1ag!= zs?gf%@2o)YPbr&*=WJgp>l68< zVoc_j&AqrX(%$;%$(w`q(P&;7Cd`;ER;%m|dIIiDG7yi~X&FsGOKh<5a7#S9pb%*3LRqA<%5p>r z+7!$vL6a=!Fk{1*!3t7>R?Nj2lU9~p+E#>|WIy*Zm;UEEomKz@aJyYvT~0r)XHMwx zQT;JJGoqg+zeD15?sj39Cs@SAP$YvQ*(}P)AY~H~oA1xy@ytYq$qcs@r|=EH(>_Nf zXwaH~odK^pS04MAWKR+U(`f^4MD za__JxlHX1n8yh7*Q!<7tKc9w1(J1{j1et8hx9Cd2gr$h#^s4}P2~&}w)S{>(iMeR1 zB$`S>cFFBE*KAo8kR2pDG|iMJRznqlDO#zwh7b$78G;6;JO3Zy#mhffs+U4BeE0cR zFZl4oi|TR__x!oem^U%^CwHcAF4GgX>r6()W3M-3nZNtg=<8h;z24?$6otvmdu#5u zUkGK5+DHW3tD@J%;W z4Zbl+aeF+udaVr_q*Ezs>P^+dBlR8)ry0scf3RjK3amuY=wDD}X=r`p-ByPEXziL_S%_Yrp&3X;1xmlXRM%ABs!=zx7vKz$4UF*|0n=Yl9w7WU~ zJt77Ylo=Z9J3cgeeD>rJ`OS~ns_54@dpuP^n$tU@q%)#^ASH6({}|-+Szr$yCAPKH z|74t{)>+|@dwA+|4`{%!Ku_ z9-!A-{=IlJNFPNuQ1g7?wq(pkfb{>e_vUd{6xZT-RdwIneV4ws@4oNrtoP0eGYm`* z`|6Bl7|MPyvH-d zmAUghRecALnD@Tl-{<$|yE9dFx~jUmy1MGrsZ-~iqQbW%Mf|#~k2hF}L8J(hN~zRe zD)*;&bXrL;ih7-(*Jrggg4N`<)fn?~y#1oC62fj8`XN+K15mB2%1qVG=jZ93&}oy} zBC*(zHl}T9Z-K8cohqx=)_5zkleFW7X@<$Z+1h!+JaN8$o^hUcUUqibO6^i%wPA&K zg>O~G27ZHfgRnt=y*|~9m>ErKNHech;Zurre!r;G>HU6R zAQ%*p7W} z{-92;r?xg7K0ieGGXYVIW;6C|Hd6-3l&1KzWpG($v!>E%m=6#!==Gu~=tk|)?kj_D z)~8gJQ5;>%0wr0Jt<08nm+da&8p{@zwUo89i<4z%%fzzN;$L(#4ZcHO!!Z&@Ui_tE zpymzUGIGWzYDW!_>-tr*ou4+JIpvX0x#hug=h)`Q;0G7xr@Xu!m9T@!M%_KR2#8xQ z*-!bum9R6_(V-~Fb@SEj7InI6W-KAhENm(EblNG^2dNj%f){cFqEUH9Y#MSz^P4Dn z`<^QG&kTIpQYm$;UK*LV`WdSdwp5J`+KY0xrgOi@y%Eo?DKXk7jKZI~t40=M!=KV& zhgY(BJhlQN$46F{U>=j=fHOH-Gd+>4jNS6pac;@^@A1pNXDU5Q5q_lsUv$Z zQmOZTZdzWA6sX!0=m;WsfSYne5;#e*88EGtgpi(>6=$Vh;Xn64z0NV#AKU+sKcFUD z^ZQ>D$K%+gwY!wVx*|DGJ1&!H;MY%HIE3ER?PoF-`U35O%P(q_i8feWY5B*7V zJ*&A79aPHhw>~L6rGHB1H{wmgc6^(_j}xUd;v8wM&RrMcG900zK4C7)DV&Cz5@727 z^;O}35+DIfovaJ%h*=lX5#1F3(j|F`@fl}k$Q>{Y0Jx|TUXFd{gdyoo+L9)tr3Cq~ zy9C<>fSekDWW7{^Jp??f=qN!h9=tqlpJ$mv-v)BiBBKcPM^w9DA!U*2gkrVGsU$gr zMf~mD4Y`kUr*q%_!;7CCUc2M2HT^Gsv16@fRjw`fYv`dX@LjkLk9+;#lHHpdpLaiZoYNUO6Hfw@x%r$XBg&Sz4?3Xc5b@>prVoc7(xm+y9w6@>vJ~~o zSRUXS4(*cmN<yyd(GP_XcvG?CyBde(1?BkDdPoo_&0>)8@<4 zUKvA3vjS%7G^!*->5DUkwgg+zZ;xW@w&W|ZS4y}k@h3`%JLD=|7Uy7Dlt?Bfp?SEC zw8b~$&179@U3f$E`ovDWJ^Vn)Gx(Xrk>v9wLve>Td<(uKeoN}1_&)p?c|3lg3BmFGBus?Df_=>7o{LsjkcB3pMc}CEI3gh5et+cv{*GcUPX_{iVm6t8))TGF zM)o{rBU_o!ScM`7BBU|W6Csg9ay3?}+pFKI=Bg`2#@P^AJBaM#i&3Y8E%F^K(!n?w zjy+Y2j$#kG%u18o+{vaN%FxKDI}Dlp4#V(T2^LG%n&!?-5#4w7c;zz}Xg_O>7iKbb zX|!F|Y!y1UC1t@_C{Ys2RN%58c%{+e3KWZH!{t=}Juk_2QP+d5m=Z%L`;64A zzLkZXg)vHhg`%qW-D% zTLzN3&9}}ol6&xzH*ei*qy2Lm7bDLDW{{8S8c)`>C zfauY{(9e_r03JFmdFizDnGp*{myziG+h977=-kvVa9$IHeRhscPtQq`o;T4M!Rht7 zEc!M4Ph4w|1zWcH{P#rY{uwlWifh@_7T@IY)YsOQU9}|ldz`+Z80NXM)LrU4H$FF&N8AAvfba9` z{0h*UWIZwIaI(Iq#X>Nzvt^rQ?i#B~^A}C!oM8La>Xz{dUm@jn_zEeb!^dQhyk>83j?Cc_#X;f->KcWbaC zvbAWN_s*h6%=dX83EmfZpm0yolg`Jz&-kA$I_i7@YVbQ)ynaqyWe)d z{X~f{%Z8((sURq%^7e9kZgVii#k>WWzE&&|a0`Ogc&rg z@gYux!~XbS2LEu{oVpiYI(njl&b2;-xW91sppo1s`F>?g>fMz|z6AM0#Ti(kJy&9r z$wVtQ;g}~;T3S<4 z@<~BKs-g*6drK9r(rAQ)M-bvw342vl!stw;vK2;qMTHU2r(0)qRiqLg!^lk1t>=uD zLY3K%{UP`gnNs>0g2-yIP&P#=UxG_Yg28~^IDjV{YI9;|X<`7I`okXVq0^aBu2MX` zo|B%l9-c-}nKaLFQjIE*fS31Gm8J%;*pDi3#c}cyszJ46Mt|gu9qO{_9Od{n7j>LD z*PPc?Y95w3qbe3Mv-H5uV_?P|^X6RG`pJ}ENMX0NW=qEXv3#nTw(%*}&@jnmnjuDJ zm-3At10;xYovCiSNv_-S^2;=MS$tW5K!jMv|7&JKL&I!81I%7}%H8|?h_1#(HGT~g z-$Ur2Xiw+2)Juv_u6NT|xS*k8bD8QjRJA~$3$AJk)l$hP2n*6?TH4ve=9)yb z=`&MPsV5;Qr<0@v2@;E;%!*`~It#>E=zZ_>8JMVCpH&Sks^FYYs*#}qEZ|&6`P}`T6EN;v-X+-a2C-?`IbL?E<&6sxp;gS}pXNO018d%TgacP!Ne3 zoDI_^N0Yd^EM9iimQ%AQ*W{W?JT~RlyT_N57F@L(}Q!mKm9@kpmA-- z)~+`QUqKQhVsmYJ2;cly!a#%DIxt@Mc9dG%(xym+yR!NVD|EUR7uYcew#kfk^2rB< z@S4k}VP7r|^w%ifV<8_T#AlHBOh;KYtDT338K_(||1{NmHZY0>#31})uo(#y+D)|uqm zh8Y+gQyrRGNSnCMx2uxuOhEA4$^8rnFT&S0 zAO;Zs*5t+U8vN$?7Wogo&mh}9BQ_`?jpa8qXrNEF_{{?QCrz#*QGl$I+9l7(6Mv%= z>g%U`q*ZIvujd4R%n~Y07$^{W99Xbtf+7}dW;qKKW5yFZ4q}vtjle;k(?nEKpTxhJNP>s;S1QT zHlMgX+%44vogohd;LwAIInUBEWd$>*$Un_3YEHdXr){uxYfr*a(P*P6%PlV}HMd|! z&P11F*a~!eR|l8Q3#*yzE;>hV$RMWq&j>2ooJ2B>#_rvRyl^s|wM-@-@QgK35)}~= z%SrBm>T$Ct+NE~UC*0Jy6kC=M@$QGF7?Ba-g3fL3!7!^>1UDe;ibN_nsVSV5TJg$K z6mWKj`!2CwX{Do`KY~s}2Q4$coT7(;3Fg+|UJJ*_-`yebbc<-LfU2r-=-?hb3{WSN zqL1$(f4!44V>J#vNU@WBwMv5tUCUYjH@kxx4wdtWiuOq%0Ef(AU)ALVs!dbAnfK=; zV%llkG$S2}$XVcKsb|kKCfmTF#F1x9yW-~!R?Gat)!}|q^7#^*_cy#k}TZCGf@!Mx#)f;m7q=4UROqg z&eMBYsAyF!Cu};RV9qEm(SMQ-kgi2NTUb`U6}c03|k2>FVjhB~*#; zj|s{|2`@9JeC_hc7IxsC#b@(Ki&2kUlrd9|sg~p~SUf~i{at)nDwcDhlx2<~`BT6! z`H|8ZqbXWBzut+NK1OqT3S{zJi1^fmI66Muwexq690!F4tU0mAs20&JfH#b@Ns8Rx@{V0qX^| ztw0u)+d*Fw>3}5^x)T@{rA;I+if^j` zL6O~vf91d~Zk{m>)LA~pH(X-cr-teDvcjq~Kk;RctX{TC;PWoPyA09jv)>y*3mr{m zV&&~FkgUAt=@#&hbIfs4sYItii4Zrm9LyI$Ni09vMm-c&uQ0jXHYA@`ScY53E=4=# zXu)eclO9&b65*x_sJsM zCkJ^nB}zMQR~fCv&+U)EEmT!vE&s(RXojbH;+Ly-dpk!1{VCU*p8b)L6c%S^Y!ChE zSl8(ElfN@yBRY-MPp8V^=W3*_@0#)ZL7YBD2MsrwVEJFif3R|$e3p(qAK!;>rjMC_ zBH=I;_27;5nz$l>XlJ7_0_eyOp@AA%7`+la(=&%IkxXW<#!p4PVv~51|BM2t9`r># z1I@cFOU*MQT|EKhLGF%1R7@QR=oI#W76o-x+qxS{0aL{J3D>`Cf(X)cP-H$2En8qd zrgE&-icfuiR@>ML6g zh#gH&pG3ya8h{$gaeF;9uaEjOj_3R&gz9U^INKes$MJ??BsD~bq!P$t$j4>05Kh6D zmjq33DGQR*fM-Bs2>XjsxCM0Jbf#r9V97o|DGz1kO~8jv;w!?;J}+MkKzbQaRlFb<8x)NF}weYOIG%CrN)}R$`eVO9Z$HD;H7!3tG#v>AieJlxlDp zl1bZVD&;*Cm+bJ%J*S@qKEgvozh`XM;)nytXVXwp?9&y;NG(orU)R@Is(KO*?L{O}UQf`C zu}VjTcf!?p-3D`JUW{|)_g7YqOdmgM#rgb_s&$80cfvkX24~6Cp^IOFv=zP>Gg%0^ zEUCb%&|Sn!9At*ne1j@ZDPCGSr1+^)G6rwV>MnPdn!|`w$;NWcI&V8cTnX$*Qb3$> zzcKfG&uzwgP5)V;#e|P>D`W_J7N9^Nu^SZw4YBF3Q|*QBtzf?Vhbx4th7fgJt=PQ( z;8ngsmR$<|!gHj@|I2T?zn8?;5idXXRvqB_seVA3u%W(JRYf(UGChn?;92xBep8LT zeI9KzIj5&>*Y5`5U=&Us*_O0%cGuo)Q;&Zp87(n)rQ?2!dMxj?+{YtiQa4??Xhz&u z3K!yaEyjd=U;KBjBV?|J18(Mn$;4xwp9SlS@1#THOEpy#lx8}3Sd_`g7!#LH5Ozl#*a2O2;LWvQWmFz$zVp91cP^Qy^6DQ70y zguV!S>Bb!QdR_AGNjc<1Ns~n;m6Di@hEt*nQ;gSKYJ1kNlFpy>JIBT1EK$~k-fl`i zq?p4TV59~@sq!V^)yHiiT2uLuu_^)9Uq5negc5pCC9?1n* z$n1sZI^w7e|5k9%S4r2KTPe;Dbmp5AOV4j}RP2jPDu@xzCoQsGh2#`G@Mvo;E-5A? z5s(X~!jb~S1LbXrG+YUBB?rS;4#QG*{iZbov`lUj(AuI_1Bq`{c>aQN%o#eQ?53f1 zbuj}w57P$qcBP0m{saUPEg{Jeizj#3T8O^(x15Dpv>fi*XuBMLWw)z3`l`2hKErL? z$y^S5bWy*^Uxy4>yKXi=&puqV$X36_x4+mMsx7SVXl&F$e(3b81GY9ZHnDUMZ4SOY z9tzvHH{*M7IDPyszMkD)NFqL=^^SQ!R%xR=Q1RrFm40AO=Gu+e*Th^x@L+jSc`#Y_ zPl0+Q%ju}Adm^-ui8|t9BZad6dZoqv1PczBYJCpb#)tEYup!OvSTOMEs4&Q& zW7X5JuxVQjcIY|GH4;>s)_brRJ$VvFX3*Nil{XHv|3IlJ7?oyNDsf|NLJfks*ry^1 z#T2Pzq8pc@Ao=PRT~s23u%?YREIAG|)B`poao?7qyW~f&WTIeQ?JHYCBJ*|8C^DyS za#0;tC09;dIav8C+2lbT$Grk)5Zjb{RKKFD*6?lXY*X>-xKG`tUNm-B8cti(uKe*y z*=}8KtoakrnhEeCgo5K_BL=mQWXa&f65QrxNPmAiN2^3tH85LEX`ogPbI?7G3{0Bb zC8_Cziihvn!M2Wg7?2ujQQb?!k$PR>b1&PhbIm&<`ZnDh-7$3%#^ow;Aag`KteoQ6|CHH_IUg z#4r!-WJFHxo#(vrx1E)Dqsvufknu`nYQEomq9=3XHFI;u(ariNCDCn(viEH3jcrly z^fSNLA}7QrwV#%J!9}Cq8GTsDylP#zj+xIcB574*y{46$Z~L#HP@Qn0idy#B-#M|- zlSm=<4xPv~K^|KolV%M%0(r|tn`ROH$1gtm(r3eNnb7bVN)2c1{Kl3!jtK*(33c^& z_H>4lGWI|2X(&ZN7diL1E@8#6Ta7@WFm|$iV;~B8jg0vz&fX`;%8 zY<8B5i8_ygdp|$V0q7niLO=ju=F2!$4PHl+&hav$#{3po0bvR`dIp{DR^^d0}s`1-v~V6(UJ&PUk2V*iZ)_iJpTj}IIW5nrS(+h#JW1Y>tXSIMY~%qS%OQr0rnaa? zL4_nbD4@mulMf7)JMrgc&^I&uo6HOeyN9xP5D)mz}Orz5nmL7}C3~dh9M_B7+A)!5A(Fium z0e(u!N$eqV_TB6HDSSF%#0yYOnLExZX)fUCd;Xu#q^ zcTu>OqiwpJ6dSHGW(vcIgO;uL(s~$a`4(wDAu|fX!FO!h8`E@3x2Qx(M4!Oym0-8*fm5;s*0fl;RF+)q|2t^hqCdk$x)r?aWeaouSGSob z8jc?=_>&iIDliUDWb5a(CrZK+a4em0tT_0WfA2Adb&zzE7q5;+!<6yJ z28drm^!oo)IRcnD=ET^XV12>YO373b$EwsS8jh=vw~lccZJ*m>;@NnTTU#VdRZn*+ zJkSnQb!LYQv#}{^Gkgmd-!L5f&ArfMqgT&s#^O`rWxC^TNrvxvKfhErI7JmKgNfu^ ze!UHF2k+^T$)?*^wtL3?;w?6+BD^_%x%VvYE-8Hl)>*yiPq6G(QrQYRPS3>7igiu5 zI<|aEKYn>_ofPn4PaDpf7)uWqH_R&O%Vcg7Unn5RsFLU0j_RCr%Wt2Onp7_keG1H; znwsnk0=@{myDO(+Wea$G?QK)2nd2%&z;sJo!#;829J$N#zcsO9YA1R}>4EBTS7ER? z3O2EtHYi}lBQ=bJx-*JWnB1H1c1o~Luv1YeatTlD`lH%iIXBcERUATr;WT*QrzFw?!%KuZ-RDF9h5Cp`d3MQ zD+!J>PU$)Dty#*KO9>-OkfmTiOwvW2z5LZqzxA(eVe2l*k zGJ1RJCF*&=BTz)s(rm1R2L1mkSvr8aM(+VQ>2`g(%chSH7Bt}Dc!Fxx*r$lG>PTNw z*;G%%lVzufr@uK=uytg5yWvh}O8wx;hI-v)eqj(wT7D2o^{!cNt$1WKc-31rS}Hmy zp1Z8XBrO--nL&+nfQoWtS&v&V(HS(GL+t(Kn-~dpqR5d+GHFDvPw4)wq6!o6FfYC4 zyzboJ@z~M-Y{h!LyiK^7a7}YLeNF9!!Ykp?Knvl6%$5#kyI_OfQp!$+h^LbLB`{V; z8#G_cQ&jN{wgs$9mlH(n#gyDssvypfLavS|{ZqE^N~VLsoPe?q(Ym*c@LbF%^>P!b#kPfaGKscI0kt`>rYIv#{&9<(fSC{XbY|9C z*aD<<6lT#jd&OJc+hMhT6YP1%g{M}a?tb$}I+t6(Z|HW;VUv>eL?bV{1vbo&x*i%i?dtR#vg}4PJGxfiG=YC` zMu+qFX*}^{G%>oFnIXP**5$pcf}Vep8GJBry@_p9OfKHSiw6_$OTsTxxQVo?m0Fc_ zX+@lNDc9Y=3ghafL)8pS)rexWrp z*vJznR$rMmG`Hc!tuYyEtqBg%v~pHRqdS&3DTPJ~@RN9_m~NQ;*&wl~XHs|73}0I} zBX(+TTH-J|s^;Qc0E28)d^6M;J&EAgT}c`|HVRum2TMpq$`voU&COD1b~4Sm1w~940T4s53sN$)ZitMOO~-KSV)3Q{u0n8#0d`bL+>hkLurxvqFk^Bo>SC zCk>S@jH4;0?h6SjC;B#iD6xW8zSA-h4JXWlR~}(PE6JQ&-b<87KtB_xRzY1WUN;=b znnD7E^07~EKML5`{otthUgWrl3YO`U#B-^FUS7ytk6L?u_OVaAUw;lQN?ZT6X2Fx% zXkX)2t?@lr`v%xw4|b1%_E@WX6Qx&4RgF<&Dtdm2=KeqxB_=y=fyJnSH5bC>9{fq; z4Ri~3XxwF4Y}@>rOSS>yLU@|wFKy&2LoR`oh0-AQV(#VZT5tZ>Epgf4q>e}LHS?Hf z+^gSv#yn+NliR-I+)eeRuwa*$-$%Dk*rOhQ6tThxd zb;a<}jCfj*NrSbq*x;ZTxGY3Ql-g^)PyoCQdWg)jtCP#E_QWsLEyf;|j(le=Hk_W# z??#EJ=+m*#I=_0VWodW=ye@AQ#f_X6nsV=&PV^;7eAN-#7nmt02z96@t|`9S2;Dut z3yQMzn~3l(C}ywIFKaT_$*Cen!pzl6;_P=zriLg&X+!mG-+Y ze{(I~LH%c#AymfEao02D;7l|=2kKrSTcmheZ6Pl+FV`#$?PzClFCZWuOu|_5C^MOq zl6L`y?iR~Q(~vcZ2ZtAvSLI#f%(MRGcQ5Uy<>Nq*eS{jF-_5CPsWB6u%u{dj&>C>f zJz{Bm_A0yI3obO7aQgT^AlwyNNwBj6H?W4kIB%{)v!FYEmnJ(vJx0E@NLUcJHescq z!Phir^I>b@Zvx?_h3pp;Xfi5`r#(IN&vvz$nK%^5-D>5HN02e7&g7!XrH!m#B- zb%-=eCg=xoC~A|_zb0z8R%x&ccR2Ll)QmdyZbg}={2TvN&%J;jY9gjfO443V64)HP`BSvX3Z*LE6v#WswJ>O8F$QJ+M}~ z`O!GiNLo3_p5tQOS*N^Nak1bWkq0My&BUBI`S4;LAIh`y5gFan=BiG~HD`*%J zNkBj&cQ@4=bi}U+Mk&u*xOfrruNVG(twd2XVYavY3e#Rl2@=dVT$&8R0Yr zxINk*Q>&`iobg1_JC`DgH}s_=u*33E{Ri@YyHit-actw0?`kEHVC*A_t2UDxR^F0tz(Uezs`v=X_=SGlFH^Iu+$M+th)`+5B4*F}9ido|6(yJJ2 z$GfRZMSbdQgk7WrJw8?Oj2ngZQ(1X9Cpag`MTOlvlm)?MM|N{gIACVJuCv&qv|}m1;^` zFmFm@#UvWUBG!SKV)6ToP)580K~%@4HH#*M7FXkl!Q5pMrPM|Dr~+EGk3_Y2B`5sJ}QoMY*SYh*Vdm6g_DQ zdO+C02}AvTFCeSf|NO7S-J>4@_b-8T9Er+V+1zDEDTD4BmDV0;u5 z9t3cGmpVIPA#2_z0EfjV*X#-2mm%4w>N36m25&{-Yp`JyB@WnbQgw3Gw%`%YmmMHMEf;g9p(RpbdxHDxr4%K@CijbGeNvDzW>DYiV}u}Ha}w= z4kW@mGZJS316n!2U%MWEVBp|%401Oe|1GzuSGSkqvo<(d%ys5q1wzRiuwc9MfSr^mWQZQN%zXy8S z=ss{AGcV-wWUV`H{Hqw)Do1J@{}gs8y5C)%>Mf^6f*b&U*ww@awb~UichVu(s@=%j zG3!u&vDqI8QkmGJ-h>0dO}DF}f_XSB2$}dctbts%T^I&(J=TIxYM0o|0-v3P_MR(We<2?--!a1EZ{v zq8s)ycVP%e@8-i$E%XD5B5_D$+RQ%albe(@4wg{Wn&Y^u1dEQYJCDLeRfk-CPo7s& zcV6GGB6VJASbTddZk;cR!ci5EuiLC89coO31uOu4vaVF@Yafp|vbqbgze4X0H@o`o zHcXoV=2pk`5IJuN8ecg)P7xarh5!6?I3iy26F*sYFyMqag@N3Js`j9r4dV%$#kl8Z z?ko>+0PyVO{D}s?L=p(?g$5pez1j>-AaRZ$qV7g0>*)}w)3<cGdIS3Na3%ACEJGh~f?dq752m5y%MauJs&_ymo%)P6 zMXkZJWA8s4luUNyxQ`JdcOg}-o3L|yzJAx;zQNhhNC}d0Hb$$vt9zH+f>CC*5k0%( zBH;UwM>LDVN;YzASXBwBj8d{rVH4(p)e-$D)SQXDHI|Jk+y3I7EB1($`+zHsE8AV* zJ9Mi#M*cIJCoxzl$xfWgNr_mLseXuz2o&wWz;n+)$pS2cVfqKnmX(;R-9z*wkcU|V2W3bP7)FR8B}Qub<~4dn0WPeM21zShEa zBHF)UE7W>gWS?sz{=?4@|!Ue>Oq-2j{;WA%QFsP=w?l-I>< zF&TbUKVYGy`21wsT-XEnE_!j!IzP*^XS|+HHcK*V{)*c67|i9?+^jv+Vx6E2sHQ>x zeSq`=`!h6G-y=+Tv4a^M2%d0=lc%P6A}mfUo@6B1Kz< z;x8+Y*G4bR*PXPbPLAAy{1z(J)UYMA#fYkO4UwnJ!Eid9r|uZeE1T=<_KWU2mFxR% zcO=KlLB;|??SzQP%s6*}gsnL*Fl^3HEqUpbjg>&b#9{pWF^D5UE^1Zb-RY#4E0)fl zPU6~O4<2vQTkwbL^}_4lR-*R$?cyn!Nr$ED^GhPulgX0JF{)B$%LW>B)gb6y=(_aC z43^sQ6jtl0O@O$32ZRcKPpW!Ff z7hP`B30`5{4xR}Tk^}OZ@N_V@9!r^Hj+Lhz)va2ipHR0Q{xt)nM5Eg{kfNaM!xrnGKM zF&NnrCg?|1*=@b+u`refQSydXHllx8iaXjO}Bw9h)*!~LAmA0)E%vP&v{({ULf*EB~N4#*HQZ@;D2v@#e(u1d@s?O4Asa{ITOkdv>g z;d4f(quC+8r)zn8_xiWbLcq@WDDIYS++Ey#PJ)B7b+=^9jVQqGxlc8bZ$*?F4@QYh z*bs@qLwLA_RGY#jEbbR1`XpKcs*NhwB(RL+teDnA3Zn-87*}qSDYGiRjHwTowepuh zi1<^2rvj@+M4hjNm*HyRFynD!4jmL1ikIuk%6W&FQ*`&m6zSzuoY@O-H^1*h$jCX) zNr-;@l+Cb}=3ozRm0mwkt;hr-T8^7R3>PU6@oHi?q>L6K4e~Nn^IZ@%V4)C1lnnAB zj#pD2W##xCh!!?<&_iD+iUsSTprNq;@*z}7Es;d#LmM$LXVt{Lj_gh8!B7zmA6K`Z zOxi5PK_3ny+n-26RG=)KOr2chFe9|yX)=0JCO^QDzFzaaZVzAK{& zwO|t2D3^Zqs9Mt8ynePWZNh^jSu}2VR1Q@SRqjpxGiH`ZD7{8AVDcbXj;@$X+U!iC z%*Kv#?@l-;KvG~;Xi!SeXC0)MAST(-AK;(=Z2)}+witkmLj{{xSmsAat6qwXY8YL% zs2V&}B_`y;mzj+VG6bGgX6!{v(O-ahXvB=8t4nNZ@7Y4Sk;$EY)R<3qAQimCxT(Y# zC12@W3D>$-7agsX8D6rsO8&?qd%P)Zo^l*1AwGrkrkF?*QC-ih=pzZl@z_Mkii1ti zznK|T^n^B^Po&{Um>ibbPb}6#6b(kanio47{yT|<=rrXA<`{hK9+K4+-(wpi+k!SR zRHoi=mX;X|lL(zD*XVZ*XaS!i+qtn`8&MA?PRxac!jgn1;-FEQ~zpCk$>hP1unxla)zzJASfWWd-it zlMJVU+n-mw31pqXIiorc>NEQM{&oAxwbe{`zCygVOl~)OskuX_0$68Mi@Sv30Yr6==e=g zCRetV;H14BDr>lBOP`3UBJia%JiD)`-#2Da88K1)*`uMHv&o3Q z(Bo-U>47Byk@g33Ene zyCu-T&s7*nuINGFWH-cNvV4xw{j3Q)_pO#OP_mJ|NWO)h%ePH;z-Ot)=EtFjvDIDP z=?CH~ojLVAogibou>v>3BtjeomIvP%Y8gzWBj{WjPO5X7L&QTPk~OrS>Ly+-g@9lQ z1=LCnq2kWMXMB80{w!MgTqAI=uy7q7o(fMphGT z$9KpiN;0rgUS9Lfi3f?_Oh*~z6D6opA9s(9s$!~6%sM--8j7?RV2!gNgZWf?&=veS z{Em80al5<$#K@nQ&4*w@Z+{OdHvO}3i%h)qQn@0=Wi4kDTl8&#qs0jZBxvPZ^o5S4K1(}+u3XY|n?5d0Gkp-H= zjP+B(E;qdH(5dO&`5M%X?tUB8#kAyyJvFARKMl@(GJgb50{=DG8PnsgE-gwi%yUkpz_2p9JHW6qu=^WmtigI_0lop$ zB0>4b%G8~OakX!JZCZ~sQuX3S|Al8<4{yRTt`9ingcv*&0{|X)K=s`TOJj7ai=idp zS@7rz9cl5YoW?ydxV|vDFa0}ktuu}5lpICmXtr_UU+~XM6{J~!?N@Vt^KQp1$>OxZ zaGx^izeQcpJ4*E!1u=8J%01dB6M;>KwqBTOL)B`;7J+NHUE1KRMgniRNVq~u{LN8^ zX8uh=vhZWwm}Non$laWYT-^`eO`M*~FAa?R$F+<5C)h%YGJdpZATqe(S|LgNX1Mu{ zSq3+WzHhlNUdpR|?Q?GQ8#9nB`d{r=F$-IwcJ)xhkVT)uUC#=kFc zzSC@}El52XH7w`{C-vVt)x0pI zO>abZoTvwb^KX{&pihGz2)wzq7G6q^s!X}VS|%gBIH?VZUFy*aSFSEQtNa&fUJE~| zqR<*WJMXFKfVIJ9EJt@X+{Gn0FMuWY$R*XnDc4?BM{}A%0k%Fh9?}mq{x8}y+Xmzl z8{l6GDcjQ?dn76o%~Z|V&7m6o30~^w8w*G%U!knFhSlxiPs_=(oC8&=R6Vy0oH zr5^P%VMzv{q|7o|4qr}8=(N*xk!>!uo0(r^2xnLJTTgtwRRS7=VC);qS$iDkhLbH* zL$Ok+HQS4rwSMO;HJe?XFH755o{T$NTSg8zs?eNST)@ieF19y0TdTHJl@xgSY`ouQ z2xs=z8nC$eK4wrA%#lvjdEoDf${m*77Y)>XXeO-+ zcKB8&SM}@lZ!X}dQQg%6c*ED*e$3pwnXt`l@ChQ3U1Rx(&RJa__|(uT?df@P9Crjv zqU0f;?7l-h`jCq3dM{#0Cti9+yW!7xPjmuR3tPLFN!7>m|K&P}#-Rqc4Q%FnjpzF$ z7GpD49b5}xWCMLu+mZIkZ)3!+w~@}O*meHH0UG(s_)1{+$1a*F#52Sb{JcH(-uT~) zpXIMs7;FS-v39!3vKj6%?a%_d8yuxrnEs9SPW4xeARS0wpgVrZUW65<&CFgO41tU< zIi4^6HWz_cKAtYTeQ(5Vh!v(TR^M)a&6(?q@lfu~jvhTR8%O~U3@tQlh*@r+tzXiv z2vN1x0R?w|HHf0&y4sV`nY$6d9%=FiMUgJzQurkHFgC9SVk z{+L@?_Oj-M#91l_hHD0}1pXa`_rca8{`sD9vk}+jLgpCO_FoPYqYrLLsNPiZsSKHk z8CgbecUUAMJWWuk0bSw`37GBY;3^PaskvCTU%dDEDN~c-l)+*{*&2CJ*8JsB6R$Z+ z|0JH@5^yakJ-@7S$?}Nd0Y?gJ`-iD}9eKUAiL}|Y8B=+$OMIiidd0=k)zY)6V{3g2 z#GJkfs{<~}3KFXUd}!8aHVAZcp)Oie>dkF;Vt2jo3VcKmi#F($&Xu|?_?8(>Kv8Q# z$*UHu714niUsS0aEC%As3$S`g0oc3?N-pFPzz5!)&AE<&mC!x{B%6V)fx z1Y=gotml}!vHZCZ@OAO|xtlAn%K16gcruxrN~N>DmA-+##~x1)^>T`tWb4%dmSJ@G zu+srg506T2ne58V0b{)sVdC`VfndEHVG{h+fvYqx-RIDmt;`9!jBRF*GY4ycO~5m% z@q{{t^z5x%?uaH&|0Z1ow%>|6f92=#FM^mNVU0hO{YQ zhuLb5mJuW|am_=1S1ky~{@C$y;<|+OB*G6nO*o zO%!Q6d2*wFM{pZ@vj@~it!HKf4BHFl2SCpY{<+rXvzeP(Y1qf}34I9umJqH$*fZ3A zz3rP?_f&u@(vNlnVG75fxCN+(s3pR2I0kwFytv!MNngJ=&NST_K$}4Zg*S(?hkk|A zg%UX!kod1p(ml^KJb75Sc}F+}!$753e13Bp6#n1#bh?t>0HlXnJ)e4mSTS9pqF&*m z?+CG-NHH8FbElx~lhArfB+du8#gP|!Uy6ss2m+#R%!K2RA|!%x{(et7E}Up%pSU-k z^8?u&##q+BTlN4b#P#2O-p*k8zyWH0q3`FI_@hrt(gAvPwKfQ#y3b2nI3BpZ{%c7g zyjPLA(9CSN>|DX6E4~w<-@&DMrubYidk&CzqRwodAqPO1I zj8$*XI~26A7gWwWB87=;cW>=0%HZ~vYS*uTn`hM85It4h0F~R0 zo{oOGKrmWSdw~ZUekcJ;2JY{{F}ZsSNYZjv*;C3A*XU9NjDH932F8d*X4JVO^oxVI z=;x93sjzt%l>M;=Rr5SadihY9>PN&V#`&x0olp{UJgID`?^V}q-u92KR@WQI?=L5N zpU3}>a_^FQOa*#5Br+>$A5HFnWU%2s8CsFw0 zf{F}g?@#V0%NHO+>KEXjjq7%vcKitvy}X(puojTG)e5+}v#f2olUu95T36=*Er zU2yI2o9Z_PXE%!tW;%-{)pfIs;DSl)6^=ZUH6KRC8fU`~{NAe5Q&%9y4l`bj$C&jm zjHw(5oFozJhbN74d!Z=CjqAs`+hJ?h>D<`TryR9_5^DGt9}bvpLEobQ9cim7EOyq# z&Wmb8DrV#n-@uiG7uvZv7u^aP?L+v-ZC2~$uI%C z6J5L?)euz1(QX*15nCEYUqWts8n$HFQhMy~OLi!(TyBViNcVXQ+*7bOE}2qv6lUuR5o(pykz|l+_>=JL>LMTeZFGD;;cjJgs+#48-fB1nS6CKyFLT4zmSW zOt0%%c$0-}@b9)E&p!kHMUVn`8M!pT@-qbFU|9R%*&1%{mW*ud&bJ6{`G_4qi1~!A zPIiuGyCgO?H1IYuK0RaVa@X-QFM>U}hu#7SsYb6P?vh#27q-i6Rl1sc-R3E@@s0_M zlvxd29_dn!uGl%oHfZYVY10)9Vc&NizDKA@k{p!fjecDfZsFL8TJfqLikPOXrMg1buzdww4 z4kQ>ap|KGW(JUGwR2Bw(v7p*ezJZ;ydC#GZZHye99E|m?|D$XTETEwo=$P>7@c$zj zSs5AF{~vPZ|0U<*qE&LYGo}@>wQ&+Mb~JP_w{x;}_#akA-`bc~kpI7hQpw!f*ipvV zRl(L;-$qVRS_zs~#7f`P5ufFU_)!%QuyxaDcQiD%al&U~r-!Ef?|A<))C}}LW(4)^#Ei{N&7A&I{NWXyjICAh z*?x5XJAjC}l`+GQUB;jDR)FT=`G0hU<^Oa=%Gk!#$qb*Fm7exT!O8fiBUbuO#zMx1 zwnoN3O8@KfxMgW4O~hpKAqu~EflZ7V7LEkvIUTt33-&kEKWh+i?u^e{0!862JaO`T)-uGH>dNPuI*W9Tf zP@ilsJ@JlcHFP~wtu(sx_HoS}Hhpg}e|H2VQT4<~V{cq%^Z-_Kj!za0F?@zJFH4vt zmoJ*aQ`@IcEM671_5~VsVy6Hd**Ue2_ z{;Y@8ui0Kewr6=yu2qHfp35uU@J76i6Qg0SkF<5Eg}&+mQ+PJh0Ou`0s@}tj);qdJ z?iaC#{o_koV4-i##c}Vi1ztUyn6LJ0$?ts}tFN%T$Cg`&Rrg|zv;d^NMYd7}Bg zG2MdzdBsUx$FrL~lH=HP$j@KLRF5wj3&#i$`Mo1jL0|2|t8a4{?~xpScNlDtPhrDTS~f}luY#vD+Edn9-NnA+*hZ8~z(3wq z0CUb)0B+!$VJ+9eH;A(XCvO2>r}xt9r9JIuyUhGMeawS$L2BwYK^ylv%~$tMh!0=N z*C+T!J2}_?r<1Y%pF8k>I}@#M{{Q&@ zk^j()jEvAf5BvYN|6}8S_W#%B|F!<#cK$EM-T}ChXzTlqZQHgzF(%F=nbx94ie8>Cl{~hz&NAsU)<-bziS8n;viSkhNiq^la zOurMxm+3pV{I7Jw@n0AFKhg~o1LwEfzezdt|MXxaU}9n8VEFF|=)B!cM{(eugO~a7 zV#A|rGi|*iZKAW*#h87BjIf0e5=3w>#v2lez#m{c0RT`~j zVK#9^?Nscw($dR}Po!*Kk?%dbVcww2*eD5LVWBaL&#Qyo{&0LeWif^MFx|x>kj?sB z2&*33_UFm^G;_1<-xXAd9DE{w>5kRFe$6OK->(m$Xq%zZo_Xw#yYX6XM0u^y=(B?P zQ>SvZmj~%4;(Tj?Vw0ED&ddbv2+tpae>*`870Z2sapT%RA+mgJHd0}QlHj(^I}kjv zd(#%-?;dC{J+3|dPZIf?9fhU}{Hp9bc>FJYtD9?#g*HE|sVWd;2o2h-ZFI)uuDlJA zaso0~JZ$VYx9}^~9T9q`@#Z=Db)Ap%0Oe%*{C5rQ1acHhSk*E%bXZbomD1L?|Fqao zf8ygU^V--)!)w4bL2@EUe4?$b&Ph+ocu}bb0mYfmeoo8--khr@x%eJ2*29<0$oye7 z6E!>{yaTR>gUx+DLMRo%-_p)b2?j4e|0?*!;$zVaKGB_kuo853jL$zxdh9f;9-Q4a z^ZQ8Fg!+Z^L}ZGXvlTokk4F(ygN?QWE5)!&81=~YkK4=Q&#m1f-Mg_n>)E(sP!HU9 zq{SUatEO@|&2Ty) z2p^b>5UdWl9UMNgh44g(YN&z%#emAuDlZ^6x$Y=pti^bmL7l!Af?eGr&REK$_+pMn z3h-Q!4oiXd*Y?L%MELENCXn@al1FfN9Il8R!5+p+D(oqGn0v8g+MUWGLj+eqtf|qzAL(}4SwAmx{HQ-ZyPCI-zLbbIa zWn^9~&!Q`dANct}jda51)1vjz=RP&hSTAfZs9X`egFH$)#2;*wgk3v--WClWSR^(9 z^2l=a<^bkeQP#b!{2w{?p6JkQL6UX}-|TaVU28Kn#NnbJn}4k7UO2s7{*5>un%x9U z?eYUb@n-i&>n^_V|J8;$ctPNbgeyXM=Wz#q2gn5+uMzgR)AvO2!CVtfK0Ng#YuFX< z5#34uPAL!E;8zTg(5()Kk{)6tpw5=-rjxJ5PoJ}y-Id#=$1bx&eGTi3aoN|j%YAkF zL~QrNC8d3Ey~o23d%JT=zcXB$dX?Huh*$e4rCt9f^AGjiB#hyZkM)UnphtI@x;J?z z)E^uX*_^{{OE1wZ0L}5~RUQpL^exG2)F&!;=1%-B3AJH0CVnu^ahGNQ^aHCsk2k;% zv9~INq>%@iK(e-3k>HyMsOB87m37EVrg|u|7MP6={ve?vOpJ<4z{O7BwKu{S5Hn(i zNi&N9Pn^MtD?0yQm;I8q@LGOOJK)!VZ`5yMdIWH>l2&Begz&NWahvvO&8y9BEsPiB zYb05TC5DDqjiasEUEl^@vGnnmh=tJ9{$i+n>BCu1AN}TWsp6*w&b1ylDh4R1<5}T&n zYR1pO&s&^6z3L3u2#oPax<3}Wqr4mO0d7WlDnO#|p}wNx38vS-oPVNoXYWLh+B&=v ze*=C)euI9Kd;@F8-{#wm3JFOTXDlI+HXuupM2ps%6$2ZgvqNu5;t|XwA|H0rjrWrA z6xbk%(|`*XW?t-du$S~9;%+W$25&BE&a&g%^&ELme8n*7>%(|{2EgAdd!Bz|eZ#JW zm^Ll2sIcH`-%KpqH{TE1)4xIAw}!SOB&vtF^%wL1MF`8hkI;&0bnF6ev!>4>LFc1n z_r$#i(FxlnEc$Gl+o$=&1Q#RN3Ecj=7ZHgMB<;y%{yj zpR+#Ig2-!&Zy%;Rr)ysew9^Ipz!PVa&-@L?;j z9f!~RP!QXW1N3U>Q4X&K{@zxdQPwf;bKfgOi&VTenY!C+N0Jk6!q2u9%wgN91_-y` zDLSeKe?8Jk=yJ3FGM0S?;YB;040&zB0QQ6T1Fta#BXcq#Tl_}79Y`%M=!2q8AGE~Z zg#^6QHH>GnqW3X3Yq)dbJ=lR~$Y(#NL{@@c_- z_cvZ!z;zD<1=#r7+a~uL$do9aJ>WOOR+LScI%0U&S-{v`1hp?%mImfgq3bk|7b!Tu zS);AGz$A`Pk@sp9z_JxtN4D|87HF_{;#Lap*BaXPdh)CXrYUwgQk^aY2e=u<%Z#7) z@>x>{vAgMsi|V=K4dGth;&P9b#?|DZ<7aPO`Ds)RTzA+ z-GHe@<#b+xc?ad>h|7$GH#mr@&H6o8uR` z^yRGo2EW(-jkFh_SesjN>3c4*vT9zQp`YElx`N#qe!u0OlD+?Ht&AQI=pt@2c(aWu zcb`Ooh&wQiu;RXwAXW4NG{Iu@tY#~WnT)5x-0sL39%SweLc6q$@6eLJd(0kPriycM z1oI?zvhGuvS(x63#?r1cl&pZ1fnJD$hQo%zJjYZ1*BCJ3!9ClWDnbAzL$Ct6a5%&? z45-c4Sn#=Kz~o5FDTeIp0n@?dUmRpxMu06C+|LV4k8%Ripo%LNUyIY<;_rcwg3qH zaL|E+U}NQpUUti-teAtfgdjBmL#>;qF&RxR>cuY-)S*_Hbbh{ z8-VId#Kzj{ND4s(FIT^bwe~_+kkthUbaO`JncM^s|%6Pf+=^UCG|8c zdRAw9Yb4N!>_hTC$VpO!fBB1y{Na1E(rs@>a0?8TC{)VxJvL-afpT6sX<- zCkEQ>mun|*N+LBeDq9HL1#VgTa9EPzV-_L@`ua-0XmH+3n0T3p2*xn<_!r^@PE`=W zf!$ZO)qrAw;Au%&xHF0`{aTO1;q(pyr)=zOycYYeCo1BI~!D=;`b;we;OUz`1>trAD;AkZ$Pb^CWvjCUDM zq+_JgeFl0Iep&+g>IzVf#r!gt;=dPz_)wwCAEp`!H+UPuk%xR6!r(-;TORV4++Nln zt@<29WOi5N{^b_4EoHeXOj`=RbVcbdA^9fS#R!3)-j^pUa*-P*Y*JX zCYdj_FPF6LFRE7;06GWt`U6l~oBVIb4q`Osxj{YtxeJv8Q*1tu7b%Ok@#gJ3+n;`l zed3DR+HQX36opHs91eb!!-&B6oG0e6yJn`r1Nc~4%{|$xE)sxwzWOy6 z7Z(dLwGRt1P&%)DdRD*DFGSay-H*6T zOpV=kQ&oUwVghH*_?|!c&t(%x$W-p&G4UNYe^uncYVi0=*wsI?-#+<9I&^;UiUXhK z4`ttAU+9u}t2aQT5Tq(+$}-5)&^==Cz@ud;ZmTR#ru+4@N@Hm~xmk6I9--%WcIJp7 z_N>*FL;x!m3;8D%@S*f{f+=J<#3eZODfh*a6AptQzM*LGdB(U-Lx z>Fzhlj!_E-uCBXC{3y-hkp47-MXpgB)g7R3DM;pH$9b^Lx8=eg-u1|58u9D zF&Nw9t5Rg9O-MX25B2v53_US06%Un)T#;;`>=h40sxu%vAWwC7)Q&4XTTDp-$j`vY z!oZI*eD>t|3uY3&&0^yz1)d%iF+x&L>C#uC(5k>b3{&){K{I@v8NN7lHyGSiKnMbr z#R}kQP0jj8j2Z2HOG`l5>HFPZnv}R@9tL_HK?uM|2n#TOU5L6k;DbP1u*5SsP_EDKc|DLb+1k|s`6=<)bOo-N`BcaLx~d)r-eVB^D8~R9{fuW zoPnz!&`^%v253rD)OD6KZd+}$NoQGPxWAiM(OV4Li}vqmq)n6~$1~;VX=;D8?QI`F z1XfB4csHSfN!+9d>IKNii6Et!eE={eP9`ZZXq#PU%r=CY8M88yN*E_P&Q&&?S8Ew@ zo4nJ#l>PpKWoK+aem@A1@r-6ZNFU6M z|HOqvZ!3B6x}LtuDSh0Sb}m<9_UeSO>kadRT-XOVvbDgwBokFfp82zyb_A^TeB8N7 zH(fUqjM7c1AE;_x9xTpK#QdGpikZ=oc{wf+oxlAArlCkeQm54Vq!50N^ov*GFbm}h zyKffDx+T%=(j(1?J{h`nOKKrP+9J0w8o*$)07}#nsilx=Q;J3Os0ade zOQo4YKIp`zp`1ab`?*Y5q?k0S@Qc> z-sk8ukDHK4^&>n{d|kRJg$TMCTh|%lSk0YRqHCfIyjvy#{!E-I>^3_dt)TG{ebCyL zbeYUUE1^eZ8uqdtkelUnw6qD+Tak1&;FN+x{_-Nu@e1@ALIJ*VZXZS@9XN9-wh|&t zK2x^=Q42~9;u3d>wDki8%d9w7jbf#I1%B%|qD0rUczyqK_wJT~;8qq{|P?h?Y?Wy-iHF5@}y4+Dq8?!Y0H;nhmja3(Hxy~G9 zJzeZ_G?s{MCQ74qxy(}IZ*nQdk@#S78K-thzYIW&IS(j?Jf zle^U7jl+BOcC9Y;^NFaZ4zO1SgwpF^EzV(6dFKO)CXK9KpQ7ATX|0irD|sJ9P2@+T z3gHt;2?pejSlMy^aU=Eiez&?L=Zt5qHg@fXALb;Pb%>Nb^I8(-Rrvw%wBv<_AG+Mg zT$+|eF~uZ#!9VhnV~6Cwo6hBj18=dpwGRROBbtyg zuQe}LZ!W30R6J=4>2gvx;BcLXALJ-YhO=mqV9UW-TGxJRi2aQPm|HzuNm_0(B~>97 zkR#?q9!9?q2COssk4yEE6?{Mo$XxW$T<%le>T{XkOLTM423$5D^6Nnb3z- zEnkph94=whx!*xiCNb5d^IK6vgj9uH(VeJkM!c@(uNHi5DH4=bIgYP3GSFs-EC(|W zNoX4gB{X|9aqE4dX~9LcQ6@yb>6Swq*u@JqhY#4ri?)9Kap@LZRNiaWq*OfGX%EQG zpdVzEie6HbGB}!!xfuhi4veXt6%iE3u$c~N{7l-%e3>3HQvgWwgKY8|mWD~s*nw~# z7Xn+x4>B>OR+P<&aXu`%5&+eV!y!~g6QnF^La|&5h5BZVEmeT1IV}4g+ol?N*mC)z zMLM_f!pfo5YOUFdyZMGwLE`V%G$^(+UWSZS=9(v@irMU6U*F0&7A%5ABo7<#OsLjt z9QiI(R|zLJ(iPfl?Ct^!Tj1Z`nxIp7)1hy2z%6XxBPHq*Akw1YUrCg6fgSeRFJHql zltWIC8uBxX4(2nBPIrFFgcL<&*gA7$jhHc}l)E;JVArI=93m{1$!rzn4D*Qhfl3ei z%cg2EA|l6CX%&oE-DnYLv)3@P^(6a{u`&7p`dQ@OBnA%pHcFLq>IA6p^~)z%#==}k zafQ2+B~8NBtiU|{5}PGiE*#|L?i!NKa;bW)0|L%>5LF=%VvieTY!4A9V66}n;lm(X z+U936Ki!%_gnlYlA@~s&#XZ4=U?+ZA>%tl$*AjgHA=j@(v-&u)Ll?H87fyGhnFTRm ze+$+!)P#Y{OBnp~MGWFvJ81%tQZ6*8AOk9mNh593_h4@-l2+7wX5XaET5KEl#tjO8 ziqFc6?z4qxm-vPlD8>(CSz*|Adsbdcf5DZ-Ol$vF8;wj9bv*W+zl6(%d80R?`Ssy} zD`I1J@vCip^Drtc5a|&SVT5RfXs8CCsJl-KjUFR5l+4#(-FP@S(J03uZg8vz*}1!i z9`yb8o7?Y--XP(Kp&9X6C3SpLbxWfH&c>7C5yd)GXSq+uS<+U7-`8bP1&Wq{lm=0VUgTlw@P7sr?MO6^Wl zUMhkb)|+*44z89tCm+y@6CDZqIKV`o(k@{GS28e>o7+~WM*48lnjL$zM-oLbQ3(`Z zQc4lV34Vi0Rph~d+niTTrZml8)KtjHyj8h2Qk$Ux??x_QV!7`$DA41yHX1y0-3u2f zI$phUJB5Pou~{T*uvRf`>dJ%or4JraZcHZ7Q_#fPT-E$oY(Gg^vokN^`RQ5VZnfFX z20D+9bQC6?O|L-_OjZEZA%Ob_14jUl@~gJ3new=DY%mj_N-hLD)tS*;sVF1#ohFoo^1_ob%d`^5zjG!s%Otrdv@kzZ^R2v2wv6TbC z6v=R}S%XSN(@S~(2~?h{f9jWhB-ncWVz}KEcYfj3LFEv}&=GlwqTzj?WnI|>y9j#S z79;dRWssr3jcbl)LZxWEIBf)g6k~WAg_JcuqSRGn0lm0tZf@p^;wfcBDrm^vYTh>I znQHi0p1AFdgIJ@bdB4@Xe7CFlo|@94&GNkT7~M<%YEHS8zKiEq>xp!G-uPxiUE$QN zmD!e~#(jTo>H}xT-&{KV`kRY`x2G~inFGJ!G%%K^+^(OnAWDLsHn-P;RwZ^NW&(G% z7ug@SLw$7n4wb8##~4jPU1{W6S@3yApZG*GNOOT9A|$-N?*nS((%bLym7o?@G{Ou7 z&+fUKM4_HDMWIh3LLu_?+m=l~fjIjT@ zEQcV3rTC>EpWQ^na^s|~pmHvCdi z>$0oqhiLj%1~< zfek<*zZI-Y46^S0VY<4wmE}?IB$@l5>Tdkl8e~vWk}M(YyHjsTWtF3?CW5 zt0}s+0RF`FWvB@_`Or}kF0MImP}PBUImf18g`>05#x887;mAU8(V$7#5;`J z#oDCGHKa}T7L9^Hb2SUDo&-gH{<^ruL}bK7rIPJNi&(M@1eo{XX^h$z^DHe4 zos=0DlWMbE61|s?uSCj|qStiD)Y20iPn_{%D_p^uGftf%3TRzUj_fWTwc1pL~B*+MwDdv_O0*$R^LNeV|k7^&HnCWesd#+yXA7lolzSP zX3{lz_Yn;|)Bsu584dsVc-07GIT;Te`{t2>TXI@KUJG;ZL8B>0E9c-IuHm;fZlcH> z;QKm`a`@@BMmx+cM}Uj??>inUL<5&!Jt@6n1tmL`{?FGf1 zs){A2u=)cZ4QWKH*3;1dhnTgMtV-!S4f@^QfoyH6E5m&FqMpi0Orb@D^que_gZ>fg zB{7HNW41n1MIOM3fNK(unUF3~+n3f;?}ts`Ym5UnLg)>rC6LqQ&qS$~`RWVc7h5gw zdOI&)$%4(eoA(f?8zRq?_Tl%?S88(lEwg6T6&dNG#;o!kI?~nVSC&Upd@^{Ol{30l zCDR&J*4$aVMH{nNi7vurKr*%dlHcSFRVE|UYHwf^%&wBV1sBq3r`kYY!jni-=OzOM znVUDaP^5wa)grek4ZWGvKfKW$OO%v7ck#DmlSMw$$r2Sn2^i6TMp1g|ED~2a7d){* z2TS(QcJIx~J10_m6Z^z;RmfZQ8+L2{1~NDN;Wyt*)wzw#JN!lF=*wU z9lu4AM{3SF=WF_u=yMQT&ea-G8b{z4sZ^R*28X*ko7OuhvU^F`7o=K6-If!*UdUh@ z%R3^T$&X8}dOQ@noW<~3!GK9kMK#8$GV!|1sjx-R;;sh=S6M>!079R+BoQgqX&b^> zG>g2$#z`&#b6q$B&$EY`(%v4pg41`rIf@ruA*B1KEPFxaePqKVT{Ez9Rp|Kk^HnvDjuWaKD^^%S%M|A6Sp4&TO(?9R{*B@DT zT3XvSd;4Px(vvGHZS|b(dWXlxo%icbRm~^UeQtd3H|9?{@S(J5T--p%O@`P4z4dWM z$77@`}c4C zIV^fsENltToOc%zm2?^H`hKK{#NyI24_1f7)KsoH-xYQDIyKrlqlUn_IyE&jv(n!) zkw3-jD+?VG37wW?qYN_=b;LvIGBbLL^CfAbI`K$nP(tvriwA>BZCQV~S1ob^HV!DOffcnw;{Mm7tNPe>NCdl2(-1LXQJ<3Q%Tbb4TME~OG?L8BEK z-br>5tNTTF%;ya8^0}xz1z60P$=o&%?d*Kz46@(iirNJJPW(q*P^|l)P|EN_xUn&} zM`G)m#_jVfz@P2zngET#cvPvwEb;X9?3tLlRgZ*2d@tz?_90h{M`{(nu(_xGkoB5;igXtQ(Lpf*O9*B*5bK;$s3lGM<5 z(!LT<<}^=U0%=8sNuX6=1F2U|yKK|%ralJ-JB@3pJLeB*I!*lxp8B6d3E9q-{v72K z)UygaVb)4^iTy1F5lhdAPJ^PG$w0A2pN?YC3kgR; zgwDMK2O>{LH9dwz*Sh~xH(Ce5#%<@at$J1UqPA`HH)Z9^Rc;BeGS^8`$2kRCxU4~v zfU3DUGHb)&)F#Iy6Nz3Bxv^8g%h^m3fMFuL&gL2bg{2;We9?18z9 z(%o8eou{@5Y`8xTnw&0E_+u*)M$2c{Z2Ja^^Xt*8F zhZXB(Rx5XDBl)z(c6O2L2h3o8?(=k39&H2XsJ+Ms%kyd0p8}Wd0r-4^tfB%hmR8B< zBwj?UJOQ@COe93HVCi%-B3v!{Z)KjmsS0t-VK%>8RB{w`Bb3NFKmfyBbevRcwvgv} z6>3U0`crfgUgCMrvR}35=6na>>YCSb`d=gK{es~}YF2H-b`68Zw?m%-MOdhX|FR|u zdze_6s2;T-;JXGwPy3;f%=%5{HYZaC(HaZ1_Kc!JWszT?StDM8|COOZB#l84%UA<}Sh>hBs5ou2AS<%|Y6HSk)@!DzhEHKSDqd3kuW;n6GKVa{fQTrKu)?B3l$+je}Gl8bk;CyWcA|_FN?%yP*-}?ixc=ziEkQ zZ{H>&6`vkNA-%NcJrChX1kYBg!@6U`)gh4}Qlgj=?%$pLL_}54a}C2tOkU2d*iVBg z?OGtKTK<(nRVfuN97z)5BWpnJGpS@L3gkE{4VZXhmnwKl+(n)PZ-Be~9RlTj_p4$n zRox{DG>;WBy49;OGt}#+6M#LElN+c?5cBg7qUCE%w}E&2B$N&?2tgDg0|$kf@0YoP zo~tKyuU+YH#rj61$jq~~TGTJcNlK-S-?o9z%l>Ic1o5E!13lHlJ=cZT%e|Xg}^(zrlLGfT1 zt+c7Qt%NtGv(>~?s?T#EoXu>$LQ)n0Y#aC;-!c#Y9MBArb4zE9njH$Hts7HNT}>f3 z3LOAyOmBmV>NyEoiG1fDV3UW+N10#m(7F?V37pMf&sWCaruo$vl!r--9d_OYHGWo^ zqkd$Zs#)H_^=z)q1LZt7H8t3XT4~wHp5klH<-!bEeZf<#$8pEBlE=b1KGIj!59{re zppBg}Z43`?lzjh4e|#*#w@K!wkDZA&EF_oT>|ZoDjCC^c2z!MLb&_!|_lf z+@|;{EqBTyFa0ra-M=e{TenByifh~=ve#2KwXNQgcb7CD->KfWet35keU*GGP;Mr& zqYJNnFrgk^#=wi7gDd+hjiOYLw)Zy#f5GIG&avU}u_f`aYddydJ#4+95$%!bG4?Iu z;=c1^sc)N@u^}WvNZi3FK6L5ITZd*m*_37`O*~%WjMG;60~&){?!xBkS%m_mRAtz< zEkhwVr9iZ3m_lD;*s&p^g&~sfOhTPWnAfqqpB$| zqjq|FYXDZc*ur*3ey3}b+^t_fR*v98IXLdYo&m#T+K_x22zCRG_Q!E|FX{|i%CRk9lO{qu2 z$nH177C#!9mKc`R3HFYGC+%bjALdi{csi3-nRQ>bRlI5@ieW%1I+yBioVRna@zdRl zLUDZdSA6)Y(9Un#2gS(oTgZ}ldL4PLD0BRebQ>;WS@MwxlVG@?UqIB$uz3XsEO)1? zbOg!b-QUWmgJrjuF>Z80s9Vwb6`pLHNC-*gOTwYTdTAfWDnv*4rG5{ZE?^nUOTDPW z*oSjRC?`K+S~qSOKbPLCmQ_wQ$&+Za@|wC24u=5Z=-6>2t%qaZlc-GZ8}-!Wo{R^y z_`dneSKUx(=C1C_>-6N=VBz8wZRqHb3xE;#iuw_5q0@Tu9qn1hj(ET2jvXsfq4r0` z55xxzUlngld_q>jaWTA8#PTmFaH}(MlCovUjc-?&!m)R z8pZB{@w$EF4p)MHTs%uAD#UjTmPKf@M`AtSK9RVQ9&_9PrU8`5kKf9e!a33qj17ZR zoumvN+6=-${Dr^@u-(tjts#{aIXel%9;dqJ(bF*ai#~6DrCBO3yB}zY+#YqQy+>OP z8k2?HQ1NkxzBi;E=I-d^4~{H#srK4U?gb?CMDK5)$&fqHykE?Uu4qX~I>E}1l`>h{ z#(ZaTr20{OT@vlBJFAzW73B|9;h54(yu{-eMcZE3+)Ta$Ii?PA9OJU5)G?sH@5ga8 zaSIZob$Rbhix<_@Oz|_co-2d(r-f2L5y{Ry#@;5sFG+M#z#=G-zqLqCY&1$LIu^`` zV5R(>!jJX3JG+V4!TiT3o&h|oCUCc6T)ABTRLP}{{i4H=SLx+A$HKWEss%D0;;=8v z#GJ92b*qUAZHZ|sGU&3Jo+DQ33z67^e09bt^TX&CdFkl(e&Lx-kSUUdXKXO*vRgnY zjOi;wZ{bK&`F;N6k86Cc#}6;l(unVGKE$j2qYkMdO%3QRL`F_m9#LiPk%3dHkY(ov z236r%Pw!{AafM$s#Ro^`v2T14eJGt}|FA2ssP99i`o?V*he=s87az&a9l<+BTzYWX zO5ZbioCiS_`M$56322`z*LCUTIjIC7O@D$IG4sCuWs}?e7%}>DJr}i(^Rs!&xSR8a zq(dUIcgsxN?ndjxo;`;uRv34Si%w zwRhvTWL-7T&S!E!!jbFFM6a41yo-q$3Q}z=O?Zq}BPld8Ns?wp0H-2p8zc1XFd27B zfZwpxap|TSQl@=!40!K2dBV|4*IrO_>sxa0ikNTppGGm$knF@Os%t%16t_$ z#T16E?#V$ldGJ!FI^aR`(*!N><75cK!tkleST~e!QU^PNy}6E|A3GPuan=q7-B_1m zXnIVCy|0TCTH);#<4pI7-bgp}6B_K*q&;S=7r}atIr)Q)-?)6FS1yuKsL36z>j(7T zr*RInTD!qohH*mTyd2*dFQ~#NddB4Yu_tk~KxgEAkaFFp!ciKj-Q{NUrRL;D16>UC zzpO{tKZOmm?<9Ld2oM*td9PNd0ahTaAkM!E2I*rF)?@0tocla^2L1w}gf;omPNW84 zTSoYau+l5&vGoY}!1h!LG7k51vs+K(58~4*<^n+fJ->Jzfi`^n-zeA5!iN+3VQQ`D zHY{4bd#jPgU&4^Z9U#AfP<(1BCyjnC16iT&g?&gCe0J1MCB#r}YkasA*s)iRr{}kRN8|Nsy}8jt^7Fs6 zUYa~P3Aj95OoLQd(>l6TS_n&kaRTV1R(&DJH*n=Isbx}E_Gp+9jM}A>=K=rb$lujygcd3+ilc)_{bu3oC#gLG9>Cq@H%lv1#BHKP4F68-mqH8 z!L9%|paGBZX%pT_n3xcs7>sNF(%@CEy8>Nz@8Hg6{E1d0hO@F(S8FXab>OhmUD~x- z(Uru2x+uUWo#xi~Xstmzso7B4)g()UvKr*dR(ZJ!sLK9mjk%;kKzDM9i?u{EdA!_S zYrPw-l1f8=inT;#b+k-qwLVJ@SK-4}f#n^aNxP%7c>wz^B;-LofXYo!AZww;Q3NQ@ zUe~>K;FF;47S#U{vGc(C-eEpD2JQJdxN)zpEVz&ZXlwYH*mEx@Fg2JEU51a~3J6_F zk2Fx10B@nuB@})qHqZ%6ZBl7(Xvq`y-Xg7cL~WYI{%DE00@DGUo38xBAd9Ljuz_F} zz5(XmG8<)W4ZfigXQ|Wnoqy^%K`_saN&0-1M$ij9o9f957i*1+r1ET4Gz6N%{AB^& zvrw$NQoU}}3|lIkUY-+OuS!2>Z2E~W;Zg9+;V|KDYJYsI@dNsKvTy(5F!KP;PJhkz z;tGAg4d9V)aLOw@>l1_TlhN}-|LqNX5Pm$>OQcOB*rPKaUq`?CUOVPV!DUiBQ5xao z6+K@StP64~XnGp`fIBQH2E};wYQTdR{}l)HMQ88B#r4;jmjbhl=_0Q8mpt#fZYf@= zYeM3j{^AsLsXfekkF?XLYG)p*4r_E_T93a*XhZL} zo6}en1CO(vNfsoB@eJ_%R6H?ZS0Gv>I|YRL8&ij-tNQF73^hsc(vMgplpD@2a~~@V zCBb7lI|u@1fH#1FL&hij$=Yrz2Sdo!iiN)GjKe9s&8&W<$hz8If4Doscb_Ztzy}-T z5sZVg-U_Az?!v>y)P{%2HY(!d5pR>d<=q*#6I`MLdcCkacCaaIla#n^m@bmWEj4^v zl5UCjMB}%pEc&ZPS@wF{H5%46jT3F-U$6q%SfRXveR-&MqJs#)jAVQ$V|Oox-NWE< zd{~3(jQvMG;6GS^k6@gC;^R2HP^e!1syLXaEiNO^SC@n4Th~2HcL) z@5!}$>5@I_zVnJ}3*AUD)W(gVremJ2e@==wE%usuV|odDsY$Ukyu;56pUiIRuZ)e) zNii(c(I*$eqNOf!5xpIgMrp89OF1^Bu)(5|&59}SG2M^U*@jGl4MA&`5d*NX_wb1HZ}b%5F0zoi?dNjc{-?y~L|I~G2blz`)sT?6(A&A;`wa2;RqssgBl8XkKU2d*Cnqki8EXF>PGaIr;r3z`N*`{1H7`~X({?40R#9e((I z!b)-7LBUV$12w8jx+*Fp65Z}u|EI31CwNgHgT)ngRNrKxn`o|T$_P5#wd=qQ>@_B7 znt%&wWjoFLt>@RK_Lu?`&&jK#Rwxd}eFv_DE<^{U>0|hdOp@SF`BzV6*OkZ30ylR) zxB>{?12@vU*vkT)Q1JdD3Cx{1LZx8=dSRfCMTw-B3mA_HC-uU!ZJxkCun4!_1`nFB znk!fw=h*&wN?>fSiAJF(aMfru?qLBdIX|{0XpLjg15A31Hpge@J27*BI|pZz+#P%h zI?eF=@o!0QWnQU(%PVsM#xgGhCIuvOeSkY{jsZ7&M~$y;e|Gn6>B+}bOD>VzL6`X} z2_TjFL*>W~qrUly=*nZDa0Q6w$Wx%$fmRVfH%yrsS6n4`tl zRWoDg8m6|+1u$ZWIkmNL+B>HbfJNton#rUsIw=>>=~B-`lG%iJ7}iAKEMU+)0R9Sld9NPML46xW)ZGTt$ z6ZIp4@5)}66+5;NlZ-h)!m>R3?n!GXiKjGtW6}$hf()(NJ`;Ajz zWMyLG_%8;*myr3N_Wv7BLFC`!6lnhmN%$|S0{eeZ5B`Z&VBuu{Csu*+TfNN4z{K)D zu?k1zF@sbi#R zUGHbw&JB77)wCfj$h2POq8ZSIrG`Dw`A{px3=?rYm>ayq-o-v)rJI(AS9OIjKriEW zIuT*XTNVFZJ<^nDQ1E zE%c!}O!&!jr}};!?)!#(hhFxHm-1$Rhj>Ka(tQhcBP&GvOs?8|b#HWia&7q*9$(UC z@0q?07{Iv$;s)GhJsycx*82#1l2qRM?^Yxu(|=v0{}=Ya|9t`eTLb|U$NxYO{7Ys3 zt*Zat|5o6C@Bi!l_x4+z|84(94t)E3V+Fo!|A`>@wtu4m{-x;uhwp!=^S|xi%Kh&c z-|gS1_5X3p{ZF<2KNjr&TEpMGknbDz>&E<-TA%ToiSqxe)@Nq_LKXb?RlWA5)<>CN z=JT9P$yk1!JbUkCZmUbOU`tykNgE?0oAt2yDh;dSf(W8F~2`niJ3k+*Rxa>DWut|x4cii39qpPaUX?f z#|m57X7@fup(c9tLh*x_quuUmTSl!D&vS|wbA`eEC<`(arvh2D&+b3e`t3Rm%eXR0 ztGoBlz&+6v0mODJXtHx8<_ZqSFH((w?|ctK8v(+OjW4ha+5!xV`vJeFh@bnW}z$Rd=KO*7?uvbhNx4RU7_kP`EQVOerT58yVZSzhwA@hd z@BZ}ufbT2DkBu*EdP9_-cEi%pf?d-QlfG&J`r!+gv#A4bWz-r2y1&&Fdc;*9RpMiw z9%&Wi#_d}*@QNY%{Hr-lsUF8tl>CbIi2}0Dx(@%VbO!dCHeBT&ZrA?4WvMBVjIVAEI)_haEA(vF$0~CE<@kIajf7A;M@nv)#L`UnK{J#MpL8t zOS!H5GU8AM`rd?|hWq+4`Wbo))~gry;Wx<*%mMb+^0jci8MGSiVk>$K9Y&vE7VEJI zJFtYCa4X!|wfHFh9SIVdEMhKZ_OW-CHQ<;aDW16t$SDMeX%f^GH7fXLB8_TJ3Q+a3k!Sb)mW9ljdfZ_|N8>nk7$iuB@ zC%O-P4d(nH`cL!=pu2a}&q?$#tiflP18VZZ6-BU&6SxlU?tHumUyZlpd-0?A`}lYG zBtAnBF_8#-YDo*3NiHNekq^lk#=ywTWab)XH}g~GpPG$qBYd7=pJPAfPHJPiV`uh^ z|F(ommzH*w_LQ4}7CE3iJMdf%O#zyl39GyU4Z&v^Z9rGU9BqObe-J2VAKH&zK(B-J zegi(gM!!+7mww)dl{kgQQ3(@RPZsOoQ@QpmtV}mh%Tl}&*0%Dw7T<(#$M?f$5B?AQ zFwE~y@K5p2@H_Yq_!GD%MABq3nFe>%Pc9%u_$(pI$&KU=au7a0CU25oli!nnG6G{| zL?*%XFqbg5G259D<{+DPH580D! znY)*Jgd64F(;Bob+J5bP?d{s9v@d9Xqb=(aKu>*e^~rO@+`ae$O@{2mWikr){av!2 z`4RaFe&!33a64SV3bcfbGT$LzyKX1*d*&PDCWJIo)x(oOiXB7WM#tEnX`JkP=r!U) zAAvml3bTxSmpni`xP|G^+@d)KQfDJv=hw+QL`(KTk3Yj|EI|vf7yY+pA^Ho<{~PRf znA2YJTl@_9KA8!W{5E=&ynr4+d(leV3RkxRJ%|2@?#7212_FKw+l-E*57CLwuU3;8 zpF#?phiu^5VTBIkdF9tgvixV@`rqMO(65<)0_`uvb8rSdj{X3v{tKMLqNb$rqo07} z3!yzgwSPhffCqo5i2?_Gf(|n|v{-Wj);u%*gVI!XJ##ZYO(w%~x>dG%$GptJ%V1-;_IG295$G{$aVcd}2j2ifnk|H(C=Yk+I_ zz$*S8odT^P;pH$FAE3Vjtxo~&sRRC*3|BE7bl7ENfcXyU#y&I%GOz~Z_7s?d#juiB z!YDVPJAt1b10C{X^f4Cjh3LEJZQvX?@cDB1eI1N53oU?EzY;wT((z_|09sa{5UPcF z`X{#FHnJXmkILCyAZL%lHT?#?3zDO(URNFNz*AxMmZQH@UWQ+2LH+ny(8-5T9yH}t z<{0`Tih_2X0zCRCoVOHc!-9e+&;9`uR9Bi)ZX=g6-@z`>h8Ccz`JmY+;UTyR9`0it zIq_W7T$%=>Jp;0=pM4CpZwfSnlQ=c!vkTz5e+8QC$7r~`2!B-z9H2~DFkdNjP3r7u z&$qQU=b9QDvYGmHU8*)&lZeNnawIH?pB+M z+`Cj7i7y?|#O3MJ)3h!xgN9}2G%OvFpsDwZeMY3EYB%YNJr(G=>R);)m7dDko>-7N zQD?eN>XD_9|C}mIqj>SWMNt0oRCz!e8B@!BYI&zxHbXfah7+V7&+4ht2wp1njP!0; zy}f7YR2c4AqoG^wUTH|zp=S+7C>o(O;+6-W#qKVw7Kpp2{aJ!^X1J&kpFFi^#4Asw zS1`iFdX}vi>7Tc#XR1FO9!S@X;O^z}#Un_bGQy|SE~s1mnGvpgM63RcbSb?Xbcgh8 z-O=rLjtc1Fr76=2dBw5|7mYB>2Iy~CQ}DY}N8FoEdOokh;I{5X+s-}eXSVlvE|q9? z`}S?p$liI2&OI8Y>A(Pt0q2rf@6zqP@Y8p~B+fcdf?p%I3@jSKx4=(I^iJqqRqk)4 z+(TQIu8~Ic@)UXX_BBgkX?)v9(3dub_xpUx;qnRO>yft4UnGY|3VwND+0?+Z4z&GC z8xMFD$@|44>AGhHYh_}eweZz~$$V~M<=JCuN$pNcv%Yk8W-+}Yc?J-~h_qaSD_JDN zJ+;wfWgFVQybZd+-vEZQMpnRzTsorfUb~2 z@Cl+tN+)MY4USKgM^dSg+FD8?+HP1=xW+EE-kh%6FiIYh2L%Z-m_F1GbGvMyJp_;}a)*DU_s#*hiPg9rsFk28&&v3=dCw~Nw(}M*IxK*#dfWU(`w1r9 zOQ#Gx8--(w4oe_()mC!0mDVI$1Ir31eLvBuhx~^Xgtn+hHEM%eUp|VF+NPUm!)W;^ zsk8}d8~jm5@1DQt+$B``VjvA9L2v*xdVocMF|}yQL4sfAw4;npu_IRVGJ_0S&C3{h zbsYOLVZMzgBRweIg~*c3VPyE+MCT;Mf;1-68!}KotgyQK+2^9*Rg-!uJwtOC2ZFyTB zl5Ljy)HcoauOMjj2xC|{T1>&;Hlg$SSGEbS;8dz1+r8*8sPGXbWp75jz_+4dX)wG+ z+7iAS?c{e#JHrRj!LV5)X~MOdgfU{T^>M;z`GWoSX2>2_?6zhNMuGzi!cM$5Fd~cu zbO?S04;51bz!A?0I)}diT~8=_o2P(u7JC5!4^(Tsqri`r|8xNQK>n-!7Iy*DOH83u zY5-$vE)mx@hn*JUw944(a5XhK!_6%%&DKObo@mA;@-;a-gpV%l2uIGWUE3p-#KA?u z)RZoE_L=9&v`wjY5{nt-xl2FWt-18fBUgPX7Q>4#W4;q@iI5mz$eHDn%=664$PDW{ zU-7)=!wD0%ov*XR%@}Fjajjlw3@Vz@@=?Xo=q+fJcq+$hu*N4t)Bah_YBsHs71ZoN zeo~I+O0*`GJ1QTS36d3gsZ5KkB0nS(nYW2HVpEPAv5}qt1FeAmjvEm$|I5e!z&*`C5s&5R)xO0qtVI4OrQ*l^`k(d~gLWFbJop!=; zaeu%U@CF!;H`@|$m%$Kr>1`q8(FPOPYO*A7h_QsQ-QW(P0P9X5I=H$7wW-?LRO-g= zMM^^r&VyM$L)d6y2f59r&BCB}3%A3xL)hYdoqS(3Y}O8%2l>sO9ojACE&L9T4i`~z zXaHycH@X0KX*oGUnsYXHgyWnJx69Sk*xJ(CV$H=9aa`K;)5}+Gdh2H=-+!ZNhTCGC zUY`yn%#L`>$Gm*Qd)r^T^$}e2@(*xoTHhajxVAWLrZ+Na2@XHKIp_pgfaaD@YI=Y_ zLnw_mDf0d1C(Vb=&$~1>TdNL*gb;CyX}!+#h!}cR=Cz`hXh-o!2k|4E2!#ut*QK_a zOgf`DikB!}_r`GCp@pFk(g`}Et3@8cLp-&3V*PnM7n2c;u`g3uf0`ZWn3SWr;((Rw z&m7N?!OY$a$%t_rR|I;%Nlz7UR#1d~;kcj?y!CB2s(f4=N>x^sF6!w@ZLCP8Q(^2B zEUYjld@^eBr$6^ffIZx zfly^JVJ&Z@E17|HvJXZEYezCiGu)tMvu%TWvpkr*Hhrsmd-{I!Zr7f=$6e3V9jUWy z;cvGR1S?piYkqPM7%lI($duEa#IG5 z%gy$<9MRIXce&}>Ge6~Y>9*2U)7JGKSUrFBbE~^ocj!&oDcfeQjd^03T-sf;XbwC3 zvtyS#!XSA1zOt}u?@iy?{g+L-$=J8n6%5pl-+GrreDL9CpN`wNSEzs~0v|h(gqxK` zoNbn)=ve1i?Of^E=+MRtPk=@H1M5%7PnfsOZ#)0Sd}=mqcH)R)cjgu{tC)3>tC`J_ zo0(fJADG{B>T7jn7uMD%;DtSDcQ>n%T zso4CQkNuePu^bp zRBp8PNY0)QqQxYL@hG05IMHIhMQkB0Pc;OCp~X^Altc{7dyUc?T8w;xkNBQ)CR03M zMI+A#i~(M=f!~na!#`$x&iD$?rINf+BeTs7jNI(h&&5mdI=mV0z$`u=#f3PDk77Zw z*nAy|(VXkxbs|UsXgVl1)O*`UasJsw>PqyTq)eMS4cvONs4jrjoi7eiy_?S?;ltvo zu_6|LgDYjV_$)^ujUq8dh9|LPJhjGnDZk0Mk-s&0OX@!U8^#xmKQ#W3H&emckcAW% zv0Vn{iKrs8@^LyeaztGcPAglRs32`=&Veed2k{CFr;65=R^}yRGWdtBtFCqimCVy0 zo%f~E-;XK73$voH-4=`0eRlWYElsPp9)9HfkDi;-mD%R^h0N^QQs>ieTs|!=XX?ZA zuUftO)~7%5MIALsg5Lharg_=L^Cn-g@e;y z5j8QIkarXxQoLB>a4^oB3FahY)~Fa6q#~KadS&=1dt=;9^ z=y2E-lcQj#QH|c(D^GevSZ3098~NKUt)RPUV75 zV1kA$9zw7nO->n51C=>$PMx`Cs|@&|6_5V*w|rcf+#Z^DXy96_VbcxIPSK2)o?brw z;=D|7x$EfiNs(RnAM(H}8|l5y040C%rD(k& zNAoTF#b&h{3{{$WLCuwf!<7?Kc$aZ!iWt3SYmN_s!byrjAtWR@>~y)^C<5AwYN?3( z`(lt5WH~A(p`Zk1LnE(*It#oKXf5!Uu#85Fl3XZg;ETv`1TR4vyyW?v+T+?2S_Tx{ z5ygm-yjye=cdZ<$=q#EY$mVit?w8bDaoAkBqY*q9!6+g`NaRG!pkBz|^O({Kqzk{3TeuiTG2f0zJT(pg3Rn|c^LZecqMcqJV%tj?edM-3?Ax>Du^scJ#UB%HPi4y#oF{1~QI zg2Q6r$p#Cy7&cjP*edWEmmIcQhz7f5J`$|~s>S#uAP$~^88;a8I^Km{NAOKZ#`TIU z8NAu}V0=q_Z~S=tM4XEWaVf6E`=MoLd?c>jaXIkYkWf73_4SR96_KZ6TogKel=a3t ztzcfbsi6UK&z3K$mWJ+n;T6a|v`~B{1(OQ@9;DhGm9%(nMGKUp?d+to4n$oFhqVh=$|XOI1*U%!{hj- zty5BYzn}x)zkH9T{j(?kBPp`6m`e!R?D{Ex!#^vffm-H)U3Wg{q$my^My~Rf1A0R) zFj~pEYHo&H8GwSxr}wwm`+T>$?(psK-yYDdwXU^ov~INBZheA#()^hFHTRGF2F``z z-LA=jEv{SKxB9mRp4Yq(GGyYb#jCju<_-Q^?MHZRtHo-Io>gcL1&ZOPtTqd~CdgRU zIQ4i*#)_@JLBNi-*cFFyBcgH-kei-2hz4Tl^LkJ9yAL8ovbLKeE!3# zwn0<`&N^@7vyD0c15sCiGn?Y>m`<iF?Az=f+4Da zR8zoO(Lr=^Y^ri`Qh*Spa6U>E4sBx3Pt<*U|K^`J6fS(_!7aboF#Pw&ep%Z0{EzU! zOFJIA&?{xMpk8W6U%6+)?!$*lzj$Eq_N%U5`z_o%`VzkIXje4TM7ig}^52->f{blI zon)rsPQ3Tr()%1f68r0R_wcNF|YuLTZxeq_q z{vtlnWzf0$yViFw({;pYbCM4FE3dfof9b%DO*&xmR9#J~E|#iGb~ZiJ^g$+d+ z^hTXuH>hJAHXY+J$Hf$km|T_Vm`)mZqhcmQJW3MGOONPT;#uc;$g|J$qDT9V=R?mK z599GD79r;$V89spIXq15+^V1k}17Kf=0K26b2!In>D;5b#iV_>k1pE@a2@X)Z0Cf15WA}{T5L6scA!}n>dq6e_jE0W|6eeeIt2VxzLz(3I&Z|PCA5OQo}i1j_=35AOEHBf$$GOn-pTTZ9+@!HsgKr zebJ|kUzbOX2W2B`V$Hgw(=^RE%fu;0#YAk4BHBYljKL-?UtFK*fP+N z%ls|n5xsl-qR&T-7U;6mhkc`XtrGI?asAC^W8*2UEflvIZ53lhu{m@20_p)faZvBz z7SMuX&^yS2ii%QAAtRrw)FKvIZwK2(1Rdq$b2yWmn_H4wm)o4%m*a9aokRzrDOpg# z*$SLoDMftAnh8s)2GgRV2VD_B)?G5(Qa-yMW;Bz1zo38 zhQt+vD%@LzJgBf7Iz3E{?1@vu63mc#D|#3sd=Z>a@8kdsq2@4(n!`Xe-+y+j)WFHq z5R3pgSE=#9BnPaJ2!2SY4@)t-Djf$vGSeRlg+hEGG+O@s0RXsa4n1g2U0!NOwclaH zmY-8>(3cHCM>Yh#SjR*c0mM6SPt~%ZH=ytt#b7ODKtDhN_d>hTPL*Etmt$$TG=Qkb z4^(ovRWDzNrQH@<`9ktosX=~cPwEVJt zF`kzx{}jq6H4V%YD1z@fjG{mUkI!oEj~%l4xY`Og)P^{v zXIhR^CN*ie)bJiuLBK|(Wd1t+i^bdsoIW=k$ zj8R8b1r~OW6;vrq%~pWYDb=Y^98OW6Ed_C^UPsm9{OdYLz)wGpTfQ_pRs6 zy|y;dg|D|K{n21eTcV43Vm!LGSsT5+f5x(#9>G_x$?0O_H?C+8*?n{IDO!Wox0ZUD zFjQ;q!S-X0r#l)O-x+Jn+8N@bAB3C^U>BD2 zA>-196Ad3XFb#A)%#NB|gWK)nof#(qbA?gdY%(b&a*3%4Ch>!C3bDt*PWEOS;Mjri z_ZCpc@Tp?osqx}TP{nh4R!)64HH;><0r~_0gkuz+gYH#vx-Gv=SD#8*CQv#ppvuAj z4W)z9)^$?rsF*@U7E_BY>{Q*jcDu^uYE_HLIX08K;^qSs|KFvbPMutj|JqQ~xcBmo zhAy11Z=YKFWMxCo>hms{mTR1Zu};T({536c^4!;^TVgR1@x%v9_hA3-j#wRu#n?&D zj?XHc>72i$yM4CO9XA?-wYw-@tc65oFGEHYK)+U8p)FQ-fwv+X3W!$0CImQlRJ5sP zv^gqTRWsTX6$3BekC2Qwm~m^aCAW{moPw|^z}c(@Jza+Yv{ulQ!X!;5m8Q(K9=8I+ zyD9&)H`DBZB##rIh>H9u`L$w= z#G_(Zju@h%MV13mQ5q$`Qk-%kDyG2Jl_Z|G7`&^f%fdg3mmD}wAqgnEg`Q@-5s(~b z5WxtpbvhkXCOSZ}@(wIH-f$2H6}1i!vkod|9ZGWxBp@RlRKhx_z;#f8>!7mML1nGr zzz!;2c`-ebCYkhLnxsL-rmHem&0&yqRo14fvNl~6vgxXjRp*2UA)5~HNTei}NW|5> z0Jy-9<2VyP3f40duS(f?Rm!Se^dMg2-nyz#RV5>JUD0Kpkf0Pte=bicA*g;WDpLXB zj8(RFC@-tW8NoswEhC+f~5WsD1Dx4ezj%?JlyBk8L~|+8;k8nE zy>ZU8m9z4Xl)iEqi0!C0=3S2WUfCJGwq(4t4H`Jfbr}&3F_DEY;~pf(c0YJrQ$W+*86pe-T?{Kzl(i9anz9I_nQEZ-p$8Px{D{wB-Cb{mm?8V*MyAn0^bcoee$NKvg& zOKN}X9%YE)GDTx>Qx$BGHktbA4sQLV!`Q3tI~Uc3rAsM{j}?XS3B4)M1s?CTNy?SZ;G`#>K|Vhv z7F@N3s9&W*FE!;)6{9Ji(wXuB52!|5*X0`@ocKjNjzMY0J%Z&;{%_Lq_F- zjF@>R!(>3OVh`nytk<9Ogpv^j^2p9)=2!2ear#Y!lGQDlTGMeJ^KTvpqvcJr*_Jd> zo`!);s)$cE<|NZ`69JRdlr?QJ?KJH*amXZ?ma4_$rjJcplUK@QGo(KA!|)Nj0&_qB zsUc9*6g%~u>=aH8og4ycP)qL$r&FiC2LggB@2c$Yq}%zRx~V*%EKj{;G_R(rtCc#; z=!RP>EL<0%F72k~MDBBcYjdu(>d54rZfBDdzvGY=j{maI?6~bV{Ii4CUOjVCZW5<4 z3GQHmY-f7LufE6=W1>;)&(0>dU)+<~dGx}z^puvcK49gY20q)o@9K-`TFoh+WR`&L zkkMb2%Rk|8RF4PrkB43*ugY)Z5Ag4a)}X_6q}Fl1xJrMCxIw?cFdW)#|Aze=j!|;N zaVT^|el_$)%!;tnju=beI6486;5a^k359C}hwV;}*YmLzTmS5d8@1u-8Y2&Dk;2uT zGIcA~ueauSjQ8RZIK#Iu_Lmrm@c}VF0*%^gKbjw^N#%}fv3A0K)Ow@UowQUjRo!=` zd#a#mY)I%-y_UtH{E&*7)QyB9#j?XgG1WUj=-i&;*sgAmRW>$T7)6}&>be(B zth)B?yPxT4>*&*SZnv0?^G40x@qbq67m{k9nFJvopDYkr< z5>Mp1sLt$CoSs~^3)8~~Y@S@#D5FsTrPA$DN3eCb$rQ{GMgi5vj4}bmgi)$tcShS2 z5AxI1ZBAQ(=86HXR+^5dPxrKgxv6e@o7-`Fqi0x4@UW=G8M+z_rzGp9P^%mguv({J zeu_ASOqm`QXrrJu3YZr}fe51vt2pAhEDS|*n9qqhl2gL*R2}_25nAh%nq;nyy07@U zb#*)HnEtxsb)@dE$4rI9sHzW4;`4C{p_z+vAOBIncZG% z$VIhwfBARp#~`iED1@I;npO*|?e`nrvc2Vf+xK?ht>B+*daXwraubiq?Q;hbLc*SK z)c6dcEqX)FO%rF;xz3;CWT&}~I@?!J?>p^(}?Y(S-%ij#dF zb+UKjkPr*T9dUh(jeC6_AE7ZfF_5s! zN;D8BimZZ`gr!8>Dj*gJ$|_WhG+VPk;*1+l#rN%gy>xHsp4a~a@4;}(}U=~v~n1xOnwZy6cIcnF2O4o*J*M@Vt(*1umRHtIIQIP(HC><$Y z=LvaZsdzA+h}C(L2^`UawM}(m)!#x31)Sa``(i!S))n!fJF)`b_h$hmx6Svh%0>Cx#q z4_N16_fqE9)RMe*x|F%;k6Z8h^R*G*qnq;cKm6~3j(aFZNCFvrM4#S6+m&=!X$=$% zQjkO^GZciDHS>6RIIv@IAPlW%ym}x zq}BN%AB}gUJEA^|(bb(#lUHe(H@ds?u~^J$iFj5q*R9Sas^v3Vp$vDrxcn#l1kNEN ziX22UI3qKP?TS&-XZ1wyHLbgyN}4l88l?wv;#@NxjPPX__w+1VhI4Ax)1#s*Tlsfd zI&%$#Qx5FVf0`sSJePW(@{D?3_rC9aKdH@ou(r;PVyFepZCujW-?SF#_(q{gY3gqp zY}(SavuSVBNR$30{6^y+(BDwGk-buXrFVVJE&7|id(jil5%e(%P@2CvuS^XZJp5B&T`4Gtm@9ads>+k#f9CLENIfCUpD6hvPr zC^pn)gAIx%sX@lk^40?$qX7)jYn4lrI<4fZ#L?-}$r?v8Sz|&*!AOk8w8!o6c-(q} zUT3KBc%kg&wAz|vEp)1Nn~VmHP~-E`M>IU#f?8agtWDO?Cpb(X?~M(T2m*{44LYs9 z32;4{Y{1_^5=xRzq#zF|Kv@9rN8JG4#->qn$${{W%RQsG?iT7WOZmKg<37*0&pYl> zeP%R-Qia`}opeXrmLJYr-FYZ(>#J9NWbC%emUg9Jp?lk(mjF7bN+iq`C}j4uOanIjqOHDha)I346LNaefB!F8Xq(H< z2GAcf?fC3rX3d#hn)9CM<$%Kz!T7b~R}ivu!(6c8ut6Jkx;Kp9KzbHW2_!QlhN?cK zHlWlx{0{XYeZ1e8uU9RkSYz?E3lX7?v+c9IXd!-# z>`~ED@f#(mvpu4Zie}Ys6sCTo<#J6_tdnKzhf|Sfl|~y0hbLRiot;b^hNIl8a z4^lkT)m&fo64h6|L-kd!P<_=aRA2QLsW)r^9aN7h#z$>U;;eHhfisDtiQ@?-QFRU_ zs?MQAHD)MLaT0}d32*vyCsD;qbdHk~dB7Ll$ZoTd04?Lj6OQfAtTkZpY#P8M=(X8W{+ii!yuG6oR~t3;KZ|UB(s)H!(TZtcD-w|%@DH&IIBRbVZ zw3a!fx`+t6zWhh#%YZc_s2*)qY+AqDPj;KJ{#!o0*MS`zKEimBjHez-8qrG_*Nx(- zic5{-qlpyX64%6Uo!D%ds+h%KCRhlb2x@|#W&v=>&s99N-^T4IgMN;iRA0@|7@&l) zBIuEEumn(H{M|xPmEY7`BFDkp&i(r~a>XD7X4IZ5<=;&b@1^0@(^_Hyd#by!ZT7L} zU;5GN`&JZe-4|Wd4PSg@-4)-vYT+$61YI5>($~^3W$xq+`w!o|}j0*wQ&Q_?&vRX|Bk2cW%UpcNlR+W?w7X_S!~lOvYBQ?XVrSov^XC zYP1{852fpKR@JFZN%jj~?Fltpp{k*?J3^lm&4JHd*Rwy{aMoeXynZotSi{}VMu@u# zFl8^9qlnW8w%J6*5Ng%&cGQV_MLV!UFNa%Ny;0FIO5QpcNk_#RC@7A| z>(g*3tsY9hn0_P8DCr$(l71iYtzs)_t(`JiHOzyN+|tP>CX>CBM(a)7N=pAAuKR#j(o;`@X#VXPXem8Xk9|38-&Dol-L z8vH?{ndP$aK)ivi4`EIlH2Ol=WX^DnV7{0_m4IQPvl41S!z|E@`5P6RDCzW)E|g$J zeK>)nuuh9rhc&7I+UU~hC#Dn56gA~c%Jesl>|A!PZjOGg@#u86jm+idn*PaYs5fh9 zxDsSOoo=-U)rmeJI1Aip`O^a+y3`y58sPMg&*oNhr5$p$&KoOrzIt4!o(DOtKWqHl zGE!kTjaskzzW@J49QCqQ<46A8EB=GNo98Uv6z;#bf7zAkL{}-8_uCw)U}{m?>YiK* zB+|SiMFl>e)s%^i~9#|-&MNtvK&Z0HsN1}@40?zxKJn=R{CO;3*?3` z;d?eKQKvYoWLRF{zTiSvk+$Za6ffdA%9u;z)f1^H3qWvZ)06JE9B;Ybb^qP1-)LCx zysrLs<{k%oyWxK3e#5=aCz&S=oaE?nQrx(aVGWGIK$@tI_=@I1{iB+1=^uBnCXBT6 zOr{^{f?7!mdLoh3yoQF~)df@BJdA(D2Dz{lOh#nPA(PgOoPv`$T`7mt#kjR@*8y9- zr=cc^>rEz4l6Z8Sme_UrFHji3E5zM?d6lqN6Zr_fVV0>8$lrXkPU0Hp#k@R zr$6+(=|t$`kTyi4jm^y=DnjA`F;{TSbuHu=twkqlHE!(odqj6d31U{{T!K?{jh1g$ z*6=7G`2&GoUU2ZdfDyWgw?N4fuwbO&1PPFWK%-;mV=F}PyLrFBTQD1NyIfk(bOB`a z|6SOaFbUy_%QY zY5doW)l>J?Yn#(~+*sGvR(d@^esQzx)5l^~S17is^f1oc)FLKGEXK8L9gk3-NveEO z;|D%=qXeF(@U~u^3QVxaYmJKTif1IgmC=UcMx$jI7R%^HNQmHwqV?I570*ag-M{Ua zlN%kiPlY5gR$)iba(qcipR|IWTYqcp8Pa+)^jti{dt@(myM5wqsgsQtk z_!rMeKBKxu)UL@&*JQP8@?6hI#WB(;oTN^&id&?rZ5lzenb}x0?)L?Jgww~RSWFX1 z;GoGHLS~C-fU?ZR6WC`Cg-{rj%NL^jYOB5y`FeIxKPYXE?$SNMKCXLSqq|9Wi=J%O zY&L8bH^+9dyQ7@yLK^@C`?>QYp3uq@ap%^`lV}q$-+14KJC{Dabknh$XK%>wiD(U} zCcKq1%|LBo3kw|HIL1+2mB;9{%C3GX1QZV`!`O!)!$Ch zinV-_-49$_N6tTJLso#LpgakjCgTx&hDjz&Ao@-wR}+bDf-0l ziSe}Mv${BE;EbF_Bd}IYNU86yUs|unnAmJqyB!$>qiFDiyrM^wz=>3in5e`zXuyn<4N<@0CHYnQ!4MQ~gx{%$55oa|@_|MX35|L5~F(rv1`U(aOHq<5J9=Acy)iDGa z>!}2*s~BFbZ50Dc3Res)kJp#0SLlOOa8G1kJ+24HmAFb0M{)X)veY_gCDs>k8jODZ z#FKXV&HA#Q$?6q-zn;;1)Agfdp(3jS_z#{lUiF(>ypdcfC zuKESeZ2iULcE3MLsjX?tiqdyq@k9Usdo1oQ%y&GgdJE2R4FK)UM2Iy3?Mdhxhf#!T zAYbK4rVhcDlOjr_KXNpp$wGnP|I%vDkRbL*K_L>+OF=#&i~r^Goe2d+t*-`2MBsI3 zP>qnTRU$fGFM@RJ@(La-dHOv&J&Z>ZL@bH@;%0HDs1c9gTI3<$IuNFtNy2H6Cp1!$ zs>f4Rdw0BZ0%J~qOm*_Ak5f?SSb>%2c%@a-LbkG|s5ED4e9208cYAugz2b+y`1Y>z z-Enqy>F&+z!nV&o_}mq(akbCeh1UU!D8E+f#pCQ+WJC?DsF%CF6CN2!hug<_v&QQ$6h3fdY-1D-4u6&$Qh( z`J&+!)9NOt(qFOuNVct~ zrN8Y7^g3c=ffjrDvKrQ@T+wAOUW#3HTj>fFHHAAe0VI*3< z;)eyHqod-7Rnd9{TB|ws<=JSiCtR+3C#z^2`iqzsCGdBPWg*_|J-1Ip+F=L;lG{?^LtZ+9L zVlgb{GR6y-#g{ZCRGA}`f5+}u-vtspjAZ(JgD;vpF5@Q6ZYL3B+~&r4_oeQq+@o%d z%k6f0yk7dE$sqEYvDs+}noT-m&=mGc!7u==JCzo3Rb=dA8p zR6xl@fDFbJwGo$_+5I!yYtJjC!==pp{9Ll#wMcRc^*D@8S(hZHrm?eQrtZeWpPkaQ zygU_5Cl?>IHLM*kl7Y)-`a<<4YmCOrq(BE)U_Jc6I1PA#a`*jiY+LEyKsIEb@H|s@ zBy^qm2AyEiC?SxdEL9A_adi5pGH2-TBqycu|PDM zut-6DB*IHUM?}uXLJYtODcewsVxedNA-y9|AB*W7(R!!TL2B!DI=v1_0^r-(0uf`=|T57aanK0^CI73!bg-8>0_wP)MV`@Iehg zQ8{ShEiP5=fk7%EU==eel@7};536;@B5`+&i`T_umz2Q~nxwS#865URX(B_LoMvk? zU`Bl&9XR2|E2=vNifvQ&{WOW!rLy_RK;5l%Z)!Puh`Pwss1+?2{`U|;j;(|R#4Bx1@e;8t67l+M zR>}5f2eW&#Y*tr!vq)Dm*d|tXNo|Fl7)Qf#e*=s~Xe^Oa@;&&I6g+AwGNx85en3+C|d zSggKYI&XaZFF)U(QC@$F+<#R*g6Uc~f&DH68`nl^6{8I?F6MpaGe%<`Ex)Je_J8j27f* zEO(XZzX{3cGJ44>*%q4KGX2T)caWV#WAYP+NmI2GYD~moHxM7Wo@^)25tf)SYjT>d zGTmZ2Vq#55JRDnb0~S?N!G9RpKz`?~hJsFSwOQd7OlI2(^Lq1E^H0ZPuGFB8-C&hG52w^d^UqSTNIT)Q>W8#cVVnn_vSg8yh$qZK*N$B60{} zgM)4tAP06(%N2yV&!{`3*BdUtjGY6JCBcHOr)}Guwrz8|r)}G|yQgj2wr$(CZJV#> zuJ_&db|X$iR=Q4PR-L~x>-^ui;$S{?K_{}1&3+8vpff-Mf~x2ix`5u>gmMW&B!qzaz!C;8!hk3>uuCD?=(lLJ^_>xZmj>2%-4iv0<9v1nZtFiE+ zMPs;!kcnnVfoP0-v{{e-P!CEP%kXh?`+I=Y#%{hyVF@W?9SwzkSX!oy9>A-q)phy1MH&JkZ=|yvuf3 zHE5Y;N=AFaO3jQg_fYfPL=WI@j<~3FQK5^nUc9aW^L|p^oVxIV+(?lun0tP$sMSu$ z*^5E-iXXzpk1BB~Q;2yhua3}Bf6EK3Batv6G^pkoEkz>kJr!U)aH4@t&E0ChlGVHT zTfJ#hQYpdeO|Smp2CT`8bC=*L3XjlvtLsgQ!|!ewYn-)*#U>BBRjMYFq{vgKYzUM7 z9A-Ny+7kJR$C@Ls`st@Mbv&GKlq=nq|A_?B_sjI{NSwUCnOsd5Fp?9L(l^&5nGWal zQuEP_SU0p{oCXxOmY+-JgvDfXqIjS*vPV{e#X@gw2844!@iUMr*Ad@%>lvNPH@i($ z7V(+)(JIn;yyx}`vOdkB+8PNyAB3H_iyYAsE*007tljNzyBcIl7J`>zBJ!c4{iNw* zw2AihYm^1GLFq-=$lJphJRP!F*Re^khT!5%T{f^BE+_q3)|4~a9zyToZsG7C!T@MJ zyMV`+YL3IzB(<4zQ+503*iX}4)1HB)Jk&g|{Bc-sPSbCiYn7Yn-o6@~FK%Qt9zN6A z>#8h_obL!B2vWqIAjW7$0%3M){0)#0%L4;`#E@8;LX6O;V%8G00i0C)tnlyRkgJqxd4UN@ie!n#HY@y))1inHf^@!HI+iZCt+RsY5R53du8i2 zW$$$~KAy5MVR1_5nJAfbvCyRDw&8*~pkCG4zZ09bw!A`(K8ZB zhxK_dR{wZ`c%rgQ7L5a97Pb`t*w88H8>-t;8^<;!ntvt2d6;k|U$A1&g+Tt51Bofi zBj?Q4d#f2>*3c^PT5tZ zWv{5BiN#@A-5KOxOnLp9<%uo}6bozcYWXblO3lF*D9h?@`1dAatt1Z8$0P7i3EqPr zCi^4cZ6tQm`!K9+vz|e3eEtblcA}6Qmtqkw6}HoOBGyy1YWE&68#0VacP0~4=KGkb zRnNAWjnjl2Ns-g)B?mMI-{4h>GdbSE%_5%iQ8a&|DRBFqAj&N1gBYP0?U zzop|ENas8H@YF6_A=3cdRdug;rD#l3G*v0g0>zb4l>kK&08b#>W(R#4b( zk0U)L&lPR`lt{Mi(qqIEl55}3b#BSe0c%A~4A^wj7j9{+O3_C|vdhI!tvWB@(si{y@vwb9e0t@?@HN{Y-BeiF|ckv4VwQTL^jEznJD9a+j z);q=eu9>@WIwLFy6*)oua58}>dfOQLs|9UYJ`rGjshA1nqj7GxoUAThaX6kWpD=|@ zB`1KbygAExnQO8ZJB_qSh(BL1X(CShy!H?UxefiYNkdN7G=vakf@Ulx^f3N_4p~Jv z0Oe;Wvo!Ts&EY|O0`p!Zs5A2~adG<^C@d6ZEiW(OfKoN8MWeI{D`yUS`wgq8g#t2m zcGzA6*U9wTTVLPa!`xiex?d8;Q;E4Mmxe!=N#oLU1wM88<~3V`z1?~Dbu-LCK`pyQl0u)(R5XVfIC~-l4_FO+0Vzm%Y0N(YidtAfb0| ztRP!hASYXAPtfuO^y_Db^Okc=F|la-Zxy!o$zko|q3ON&Hg7%dlCJFnS_^jgxG6yt z)80Z}qbk(;j{1(4v24C{-)eW-9#m{NY!0foUA@TdRc)@jtPR$6wNnU|5gB{a2l04d zJ!}Y&PF5b+jIA75ajyNw<5{C#kinZQ_OdH#*Zd@~jm@O00`mqBkHa0U!HI=4-Tm0< zwPs_2YVdJQUrGBp!H=^(97h-=Lo%3^td`o=IpqOsa<2HvmWjp^GhLRp={9n=2$hDq zh@x<~pdY4~5)_e`^?9m)uWY;!m7!R$H6w4_pg#3_Z2zu%@qW{q*v{*9(pP5PHM#Me z+A&X)x%1h52U9l5JC)H@Wa7TU0InYNg7vd^O3Ae`LxcTe+4&cZXF$c+G4>doSc~Lv z&_0vu7?0GVuT>l_!NN8tv4Gmo%uld>AQiASa}{=zR3^WvxWt$r&x+HEeMKld=J-*n zNikBxM?FqF?^Oa(cP%&$cmdMWui&bdD`JOvnm=6OKV7peqdmpk_SfMo@JR23CnE%u zgPMhJ?+g&mmk6ZM9f0tGug9MBD#2AHkG?S=I?`iXHB@yR6=fA=mWqwMY>gb2ElL68 zU>Z|x-e>HotjS*!zF$;aO7lugY-ei&Rf*>`FL&qaV+{77qf1I2%5n8=l`KbR!JHp zRbp=i(!Zx1{R$TM$!*nNCAA1^4g2>oR4rCaZyCtzxy)`+W6e_-Tq#owUqVLKhV90x zes43b~jf!M=q6&uHz*` z$$560cvGGvQJ*f45LzgYTMVvN*5gT0Qzb{zBvI@S=|~|ZUYW%m zk&@8Uv=x6bMSUI>fY1&mICqWMzYt6wVLo;-xzc?3Z4b2XC#7=T9xBIH!NVB)a@4|S z$IvkF?Pk-*5(@f2ulx=SF!-rPw9S4Qz(r1FmSfse!wcSuTU&8G67P>!;hf4PI(-T;)>a+T4S5q&C&<*hIV)S!Y$qWcAbRs1#k zH-8o(M1R&-CQ+CAI@D+26UdfS?VouKrou`L`=0@YXwe8Lmq9Q~5z%J^d)%L*Mn~Tm zh$77;rX><8=N>!oyYiTZV|f9E1oIbGLYD5RC?ojT4WmT_dInra0Aj2FPmy8fTD9<~ zCh5gvxl=2X~m3R%<~wk)g8pP;1CGUDI%a`r$sx z1zY0@Ohor7HQ_UNMB?s4!}7VQZ3B=}dIM&9iWVu8jxAm=9Yl}rRkH!dA9HtHgi4ko z_wCkh1sY!WB9G3G<}bp`+eGw(u?R1dRs6VH=;pk{ZT-XYyJF4nViCWdwB#0=lUL$H zB4jb%$<+D)&2yVLiXVb(B z1I-!|d-yFq?auf}Dk`Fi_J}z5J{mMfE^g`@NjfFevsO1b=TkAyd9Sct)1>xBhXcD{(`k!SR_Fl>18LLL@hFn_IQ*IE4H?&$E!?lHqNSr91XRnO!nlec3O`>Dfr- zW!Da~-s