From a12ab0496cb450fa6ea235a612cd15d2dc4937bc Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 16 Jul 2026 11:18:26 -0400 Subject: [PATCH 01/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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/86] 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 95e1ffea6e7a3cb82f49a8371a1d7154b7ba011c Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Thu, 23 Jul 2026 17:49:44 +0100 Subject: [PATCH 22/86] ci: Add llvm-tools-preview to rust toolchain (#7853) --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3206cf59c0..a82b4734d8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] channel = "1.95" -components = ["rustfmt", "clippy", "rust-analyzer"] +components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools-preview"] profile = "minimal" From 38c54c3f36be1878c2bfbb7a86ebe4aee7c22141 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 23 Jul 2026 14:50:59 -0400 Subject: [PATCH 23/86] feat: Add fixCleanup3_4_0 amendment (no functionality yet) (#7854) --- include/xrpl/protocol/detail/features.macro | 1 + 1 file changed, 1 insertion(+) diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index bfe03a6303..4f1fac82da 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) 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 24/86] 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 40cdf49d155322595764ff9b7edc33f1f553ab83 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 23 Jul 2026 20:05:24 +0100 Subject: [PATCH 25/86] build: Use custom libc in a devshell by default (#7852) --- docs/build/nix.md | 30 ++++++++--- nix/ci-env.nix | 104 +++---------------------------------- nix/compilers.nix | 117 ++++++++++++++++++++++++++++++++++++++++++ nix/devshell.nix | 91 +++++++++++++++++++++++++------- nix/docker/Dockerfile | 1 + nix/packages.nix | 12 +++++ 6 files changed, 234 insertions(+), 121 deletions(-) create mode 100644 nix/compilers.nix diff --git a/docs/build/nix.md b/docs/build/nix.md index d6e53a254a..b95b82fc42 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -38,8 +38,10 @@ The first time you run this command, it will take a few minutes to download and ### Platform notes -- **Linux**: `nix develop` gives you a shell with all the tooling necessary to - develop xrpld and with GCC 15.2 (also provided by Nix). There are no caveats. +- **Linux**: `nix develop` gives you a shell with all the tooling necessary to develop xrpld + and with the same GCC/glibc toolchain that Nix builds for CI. + See [Choosing a different compiler](#choosing-a-different-compiler) + for the custom-vs-plain toolchain trade-off. - **macOS**: `nix develop` gives you a full environment too, with Clang (and every other tool, including Conan) provided by Nix. To use your system-wide Apple Clang instead, enter `nix develop .#apple-clang`. Conan has no binary in @@ -63,8 +65,16 @@ The first time you run this command, it will take a few minutes to download and ### Choosing a different compiler A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#clang`. -The `.#gcc` and `.#clang` shells provide the same GCC and Clang versions used in CI -(pinned in [`nix/packages.nix`](../../nix/packages.nix)). + +On Linux, `.#gcc` and `.#clang` provide the exact toolchain CI uses: +the compiler (pinned in [`nix/packages.nix`](../../nix/packages.nix)) +rebuilt against the pinned custom glibc (see [`nix/compilers.nix`](../../nix/compilers.nix)). +Building that toolchain the first time is slow unless it is fetched from a Nix binary cache. +If you don't need the custom glibc, the Linux-only `.#gcc-plain` and `.#clang-plain` +give you the stock nixpkgs compilers of the same versions. +On macOS there is no custom glibc, so `.#gcc` and `.#clang` are already the plain nixpkgs toolchain, +and the `-plain` variants do not exist. + Use `nix flake show` to see all the available development shells. Use `nix develop .#no-compiler` to use the compiler from your system. @@ -72,14 +82,18 @@ Use `nix develop .#no-compiler` to use the compiler from your system. ### Example Usage ```bash -# Use GCC (same version as CI) +# Use GCC — same toolchain as CI (custom glibc on Linux) nix develop .#gcc -# Use Clang (same version as CI) +# Use Clang — same toolchain as CI (custom glibc on Linux) nix develop .#clang # Use default for your platform nix develop + +# Stock nixpkgs GCC/Clang, Linux only — skips the custom-glibc build, but does not match CI +nix develop .#gcc-plain +nix develop .#clang-plain ``` ### Using a different shell @@ -110,6 +124,10 @@ 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.). +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. + ## 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/ci-env.nix b/nix/ci-env.nix index 63bddb46d8..787b94406e 100644 --- a/nix/ci-env.nix +++ b/nix/ci-env.nix @@ -6,108 +6,18 @@ let inherit (import ./packages.nix { inherit pkgs; }) commonPackages - gccPackage gccVersion - llvmPackages llvmVersion mkVersionedToolLinks ; - # Underlying compiler toolchains to wrap (versions pinned in packages.nix). - customGccPackage = gccPackage; - customLlvmPackages = llvmPackages; - - # binutils wrapped to emit binaries that reference the custom glibc - # (dynamic linker path, library search path, RPATH). - customBinutils = pkgs.wrapBintoolsWith { - bintools = pkgs.binutils-unwrapped; - libc = customGlibc; - }; - - # Rebuild gcc (specifically libstdc++ / libgcc_s) against the custom - # glibc. The override swaps gcc.cc's bootstrap stdenv for one that uses - # the existing gcc binary but links against the custom glibc, so the - # resulting compiler ships runtime libraries that only reference symbols - # available in that glibc. - customGccCc = customGccPackage.cc.override { - stdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv ( - pkgs.wrapCCWith { - cc = customGccPackage.cc; - libc = customGlibc; - bintools = customBinutils; - } - ); - }; - - # cc-wrapper around the rebuilt compiler, pointing at the custom glibc - # headers and libraries. This is what we actually expose to users. - customGcc = pkgs.wrapCCWith { - cc = customGccCc; - libc = customGlibc; - bintools = customBinutils; - }; - - # gcov ships in gcc's `cc` output, but the cc-wrapper doesn't expose it. - # Surface the gcov from our rebuilt gcc (linked against the custom glibc, so - # it runs under the loader installed in the image) and matching the exact - # compiler version, so gcovr can produce coverage reports in the CI env. - customGcov = pkgs.runCommand "gcov-custom-for-ci-env" { } '' - mkdir -p "$out/bin" - ln -s "${customGccCc}/bin/gcov" "$out/bin/gcov" - ''; - - # stdenv built around the rebuilt gcc / custom glibc. Used to rebuild - # compiler-rt below so its sanitizer runtimes see the custom glibc - # headers. - customStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customGcc; - - # Rebuild compiler-rt against the custom glibc so the sanitizer runtimes - # don't use glibc symbols (or sysconf constants like _SC_SIGSTKSZ) that - # only exist in newer glibc versions. scudo is dropped because its CMake - # includes CheckAtomic with -nostdinc++ in CMAKE_REQUIRED_FLAGS, which - # makes std::atomic unfindable in our stdenv; we don't use scudo (only - # asan/ubsan/tsan etc.). - customCompilerRt = - (customLlvmPackages.compiler-rt.override { - stdenv = customStdenv; - }).overrideAttrs - (old: { - postPatch = (old.postPatch or "") + '' - substituteInPlace lib/CMakeLists.txt \ - --replace-quiet 'add_subdirectory(scudo/standalone)' \ - '# scudo/standalone disabled in xrpld ci-env' - ''; - }); - - # cc-wrapper around clang, pointing at the custom glibc headers and - # libraries. Reuses the rebuilt gcc for libstdc++ / libgcc_s so that - # C++ binaries produced by clang also only reference symbols available - # in the custom glibc. compiler-rt is wired into a resource-root so - # sanitizer runtimes (libclang_rt.*.a) are found at link time; this - # mirrors what nixpkgs does internally when building llvmPackages.clang. - customClang = pkgs.wrapCCWith { - cc = customLlvmPackages.clang-unwrapped; - libc = customGlibc; - bintools = customBinutils; - gccForLibs = customGccCc; - extraPackages = [ customCompilerRt ]; - extraBuildCommands = '' - rsrc="$out/resource-root" - mkdir "$rsrc" - ln -s "${customLlvmPackages.clang-unwrapped.lib}/lib/clang/${toString llvmVersion}/include" "$rsrc/include" - ln -s "${customCompilerRt.out}/lib" "$rsrc/lib" - ln -s "${customCompilerRt.out}/share" "$rsrc/share" || true - echo "-resource-dir=$rsrc" >> $out/nix-support/cc-cflags - # compiler-rt ships the sanitizer/profile/xray interface headers (e.g. - # ) in its `dev` output. In a normal Nix - # build these reach the include path because compiler-rt is propagated - # via depsTargetTargetPropagated and stdenv's setup hooks add its - # dev/include. The CI image runs clang outside a Nix stdenv (binaries - # on PATH, no setup hooks), so that never happens; add the headers - # explicitly. gcc ships its own copy, which is why this is clang-only. - echo "-isystem ${customCompilerRt.dev}/include" >> $out/nix-support/cc-cflags - ''; - }; + # Custom-glibc toolchain, shared with the Linux dev shell (see compilers.nix). + inherit (import ./compilers.nix { inherit pkgs customGlibc; }) + customGcc + customClang + customBinutils + customGcov + ; # Strip the generic cc/c++/cpp symlinks from the clang wrapper so it can # coexist with the gcc wrapper in buildEnv. gcc remains the default diff --git a/nix/compilers.nix b/nix/compilers.nix new file mode 100644 index 0000000000..90856afacc --- /dev/null +++ b/nix/compilers.nix @@ -0,0 +1,117 @@ +# Custom-glibc compiler toolchain shared by the CI environment (ci-env.nix) and +# the Linux dev shell (devshell.nix): gcc / clang / binutils rebuilt to target +# the pinned custom glibc. Linux only — the pinned glibc snapshot does not build +# on darwin, so callers must not evaluate this on macOS. +{ + pkgs, + customGlibc, +}: +let + inherit (import ./packages.nix { inherit pkgs; }) + gccPackage + llvmPackages + llvmVersion + mkGcov + ; + + # binutils wrapped to emit binaries that reference the custom glibc + # (dynamic linker path, library search path, RPATH). + customBinutils = pkgs.wrapBintoolsWith { + bintools = pkgs.binutils-unwrapped; + libc = customGlibc; + }; + + # Rebuild gcc (specifically libstdc++ / libgcc_s) against the custom + # glibc. The override swaps gcc.cc's bootstrap stdenv for one that uses + # the existing gcc binary but links against the custom glibc, so the + # resulting compiler ships runtime libraries that only reference symbols + # available in that glibc. + customGccCc = gccPackage.cc.override { + stdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv ( + pkgs.wrapCCWith { + cc = gccPackage.cc; + libc = customGlibc; + bintools = customBinutils; + } + ); + }; + + # cc-wrapper around the rebuilt compiler, pointing at the custom glibc + # headers and libraries. This is what we actually expose to users. + customGcc = pkgs.wrapCCWith { + cc = customGccCc; + libc = customGlibc; + bintools = customBinutils; + }; + + # gcov matching the rebuilt gcc (linked against the custom glibc), so gcovr + # can produce coverage reports both in CI and in the dev shell. + customGcov = mkGcov { + name = "custom"; + cc = customGccCc; + }; + + # stdenv built around the rebuilt gcc / custom glibc. Exported as the dev + # shell's gcc stdenv, and used below to rebuild compiler-rt so its sanitizer + # runtimes see the custom glibc headers. + customStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customGcc; + + # Rebuild compiler-rt against the custom glibc so the sanitizer runtimes + # don't use glibc symbols (or sysconf constants like _SC_SIGSTKSZ) that + # only exist in newer glibc versions. scudo is dropped because its CMake + # includes CheckAtomic with -nostdinc++ in CMAKE_REQUIRED_FLAGS, which + # makes std::atomic unfindable in our stdenv; we don't use scudo (only + # asan/ubsan/tsan etc.). + customCompilerRt = + (llvmPackages.compiler-rt.override { + stdenv = customStdenv; + }).overrideAttrs + (old: { + postPatch = (old.postPatch or "") + '' + substituteInPlace lib/CMakeLists.txt \ + --replace-quiet 'add_subdirectory(scudo/standalone)' \ + '# scudo/standalone disabled in xrpld ci-env' + ''; + }); + + # cc-wrapper around clang, pointing at the custom glibc headers and + # libraries. Reuses the rebuilt gcc for libstdc++ / libgcc_s so that + # C++ binaries produced by clang also only reference symbols available + # in the custom glibc. compiler-rt is wired into a resource-root so + # sanitizer runtimes (libclang_rt.*.a) are found at link time; this + # mirrors what nixpkgs does internally when building llvmPackages.clang. + customClang = pkgs.wrapCCWith { + cc = llvmPackages.clang-unwrapped; + libc = customGlibc; + bintools = customBinutils; + gccForLibs = customGccCc; + extraPackages = [ customCompilerRt ]; + extraBuildCommands = '' + rsrc="$out/resource-root" + mkdir "$rsrc" + ln -s "${llvmPackages.clang-unwrapped.lib}/lib/clang/${toString llvmVersion}/include" "$rsrc/include" + ln -s "${customCompilerRt.out}/lib" "$rsrc/lib" + ln -s "${customCompilerRt.out}/share" "$rsrc/share" || true + echo "-resource-dir=$rsrc" >> $out/nix-support/cc-cflags + # compiler-rt ships the sanitizer/profile/xray interface headers (e.g. + # ) in its `dev` output. In a normal Nix + # build these reach the include path because compiler-rt is propagated + # via depsTargetTargetPropagated and stdenv's setup hooks add its + # dev/include. The CI image runs clang outside a Nix stdenv (binaries + # on PATH, no setup hooks), so that never happens; add the headers + # explicitly. gcc ships its own copy, which is why this is clang-only. + echo "-isystem ${customCompilerRt.dev}/include" >> $out/nix-support/cc-cflags + ''; + }; +in +{ + inherit + customGcc + customClang + customBinutils + customStdenv + customGcov + ; + + customClangStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customClang; +} diff --git a/nix/devshell.nix b/nix/devshell.nix index 1316fe4234..9b453ddef8 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -1,16 +1,51 @@ -{ pkgs, ... }: +{ pkgs, customGlibc, ... }: let inherit (import ./packages.nix { inherit pkgs; }) commonPackages + gccPackage gccVersion llvmVersion llvmPackages mkVersionedToolLinks + mkGcov ; - # Plain nixpkgs stdenvs — no custom glibc, unlike ci-env.nix. - gccStdenv = pkgs."gcc${toString gccVersion}Stdenv"; - clangStdenv = llvmPackages.stdenv; + # Plain nixpkgs stdenvs — no custom glibc. + plainGccStdenv = pkgs."gcc${toString gccVersion}Stdenv"; + plainClangStdenv = llvmPackages.stdenv; + + # Custom-glibc stdenvs, matching the CI environment (see compilers.nix). The + # pinned glibc snapshot only builds on Linux, so on darwin these fall back to + # the plain stdenvs; the `if isLinux` guard keeps `customGlibc` from being + # forced (and erroring) on macOS. + customCompilers = import ./compilers.nix { inherit pkgs customGlibc; }; + customGccStdenv = if pkgs.stdenv.isLinux then customCompilers.customStdenv else plainGccStdenv; + customClangStdenv = + if pkgs.stdenv.isLinux then customCompilers.customClangStdenv else plainClangStdenv; + + # gcov matching each gcc shell, so `-Dcoverage=ON` builds work in the shell. + plainGcov = mkGcov { + name = "plain"; + cc = gccPackage.cc; + }; + customGccGcov = if pkgs.stdenv.isLinux then customCompilers.customGcov else plainGcov; + + # 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." + ''; + + # Tools to expose under version-suffixed names (see mkVersionedToolLinks). + gccVersionedTools = [ + "gcc" + "g++" + "cpp" + ]; + clangVersionedTools = [ + "clang" + "clang++" + ]; # compilerName is the command used to print the version, or null for none. makeShell = @@ -19,9 +54,11 @@ let compilerName, version ? null, versionedTools ? [ ], + extraPackages ? [ ], + warningHook ? "", }: let - compilerVersion = + compilerVersionHook = if compilerName == null then ''echo "No compiler specified - using system compiler"'' else @@ -37,10 +74,11 @@ let }); in (pkgs.mkShell.override { inherit stdenv; }) { - packages = commonPackages ++ versionedLinks; + packages = commonPackages ++ versionedLinks ++ extraPackages; shellHook = '' echo "Welcome to xrpld development shell"; - ${compilerVersion} + ${compilerVersionHook} + ${warningHook} ''; }; in @@ -48,25 +86,21 @@ rec { # macOS: Nix Clang. Linux: Nix GCC. default = if pkgs.stdenv.isDarwin then clang else gcc; + # gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no + # custom glibc, so they fall back to the plain nixpkgs toolchain. gcc = makeShell { - stdenv = gccStdenv; + stdenv = customGccStdenv; compilerName = "gcc"; version = gccVersion; - versionedTools = [ - "gcc" - "g++" - "cpp" - ]; + versionedTools = gccVersionedTools; + extraPackages = [ customGccGcov ]; }; clang = makeShell { - stdenv = clangStdenv; + stdenv = customClangStdenv; compilerName = "clang"; version = llvmVersion; - versionedTools = [ - "clang" - "clang++" - ]; + versionedTools = clangVersionedTools; }; # Nix provides no compiler; use the one from your system (e.g. Apple Clang). @@ -76,3 +110,24 @@ rec { }; apple-clang = no-compiler; } +# The *-plain shells (stock nixpkgs toolchain) exist only on Linux: on darwin +# gcc/clang are already plain, so these would be redundant and are omitted, which +# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc. +// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { + gcc-plain = makeShell { + stdenv = plainGccStdenv; + compilerName = "gcc"; + version = gccVersion; + versionedTools = gccVersionedTools; + extraPackages = [ plainGcov ]; + warningHook = plainWarningHook; + }; + + clang-plain = makeShell { + stdenv = plainClangStdenv; + compilerName = "clang"; + version = llvmVersion; + versionedTools = clangVersionedTools; + warningHook = plainWarningHook; + }; +} diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index 8b851fd9e0..53b646ac7c 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -8,6 +8,7 @@ RUN mkdir -p ~/.config/nix && \ # Copy our source and setup our working dir. COPY nix/ci-env.nix /tmp/build/nix/ci-env.nix +COPY nix/compilers.nix /tmp/build/nix/compilers.nix COPY nix/packages.nix /tmp/build/nix/packages.nix COPY nix/utils.nix /tmp/build/nix/utils.nix COPY flake.nix /tmp/build/ diff --git a/nix/packages.nix b/nix/packages.nix index 1d9b6cd2f8..3cf0f57c3e 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -48,6 +48,17 @@ let }) tools ); + # The cc-wrapper doesn't re-export gcov, but coverage tooling (gcovr) needs a + # gcov that exactly matches the compiler. Surface it from a gcc `cc` output. + mkGcov = + { name, cc }: + pkgs.linkFarm "gcov-${name}" [ + { + name = "bin/gcov"; + path = "${cc}/bin/gcov"; + } + ]; + clangToolLinks = mkVersionedToolLinks { name = "clang-tools"; package = clangTools; @@ -72,6 +83,7 @@ in gccPackage llvmPackages mkVersionedToolLinks + mkGcov ; commonPackages = with pkgs; [ 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 26/86] 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 27/86] 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 b89d75a2d504c58f71813dc5b3d2437567193113 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Jul 2026 16:57:00 -0400 Subject: [PATCH 28/86] test: Add an RAII class to manage the env.parseFailureExpected flag (#7669) --- src/test/app/Batch_test.cpp | 3 +-- src/test/app/Vault_test.cpp | 3 +-- src/test/jtx/Env.h | 43 +++++++++++++++++++++++++++++++++++++ src/test/jtx/impl/Env.cpp | 2 +- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 5085ad6172..ffaf26b5a7 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -498,7 +498,7 @@ class Batch_test : public beast::unit_test::Suite auto const batchFee = batch::calcBatchFee(env, 0, 2); auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1); tx1[jss::Fee] = "1.5"; - env.setParseFailureExpected(true); + auto const g = env.getParseFailureGuard(true); try { env(batch::outer(alice, seq, batchFee, tfAllOrNothing), @@ -510,7 +510,6 @@ class Batch_test : public beast::unit_test::Suite { BEAST_EXPECT(true); } - env.setParseFailureExpected(false); } // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 617820c89c..12ad7e6782 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5524,9 +5524,9 @@ class Vault_test : public beast::unit_test::Suite env.close(); // 2. Mantissa larger than uint64 max - env.setParseFailureExpected(true); try { + auto const g = env.getParseFailureGuard(true); tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1 env(tx); BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max"); @@ -5537,7 +5537,6 @@ class Vault_test : public beast::unit_test::Suite BEAST_EXPECT( e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s); } - env.setParseFailureExpected(false); } } diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 7e22cdd571..0df62c7e9b 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -514,6 +514,49 @@ public: parseFailureExpected_ = b; } + /** + * RAII class to set and restore the parse failure flag (setParseFailureExpected). + * + * Can be created directly, or through the `getParseFailureGuard(bool)` function. + */ + class ParseFailureGuard final + { + Env& self_; + bool const oldExpected_; + + public: + ParseFailureGuard(Env& self, bool b) + : self_(self), oldExpected_(self_.parseFailureExpected_) + { + self_.setParseFailureExpected(b); + } + + ~ParseFailureGuard() + { + self_.setParseFailureExpected(oldExpected_); + } + + // No copy, no move + ParseFailureGuard(ParseFailureGuard const&) = delete; + ParseFailureGuard& + operator=(ParseFailureGuard const&) = delete; + ParseFailureGuard(ParseFailureGuard&& other) = delete; + ParseFailureGuard& + operator=(ParseFailureGuard&&) = delete; + }; + + /** + * Gets an RAII guard to set and restore the parse failure flag + * + * Usage: + * auto const guard = env.getParseFailureGuard(true/false); + */ + [[nodiscard]] ParseFailureGuard + getParseFailureGuard(bool b) + { + return ParseFailureGuard{*this, b}; + } + /** * Turn off signature checks. */ diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 3f6aca9fcb..4da2e2b521 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -629,7 +629,7 @@ Env::autofill(JTx& jt) catch (ParseError const&) { if (!parseFailureExpected_) - test.log << "parse failed:\n" << pretty(jv) << std::endl; + test.log << "parse failure:\n" << pretty(jv) << std::endl; rethrow(); } } From 4acccfeda8a097dd5355b16095f3950153563f12 Mon Sep 17 00:00:00 2001 From: Marek Foss Date: Thu, 23 Jul 2026 22:00:06 +0100 Subject: [PATCH 29/86] test: Modularize Peerfinder component and migrate Peerfinder tests from Beast to GTest and GMock (#7054) Co-authored-by: Alex Kremer --- .../scripts/levelization/results/loops.txt | 3 - .../scripts/levelization/results/ordering.txt | 20 +- cmake/XrplCore.cmake | 7 + .../detail/aged_unordered_container.h | 1 + include/xrpl/peerfinder/Config.h | 163 +++ include/xrpl/peerfinder/PeerfinderManager.h | 179 +++ {src/xrpld => include/xrpl}/peerfinder/Slot.h | 0 include/xrpl/peerfinder/Types.h | 46 + .../xrpl}/peerfinder/detail/Bootcache.h | 6 +- .../xrpl}/peerfinder/detail/Checker.h | 0 .../xrpl}/peerfinder/detail/Counts.h | 7 +- .../xrpl}/peerfinder/detail/Fixed.h | 4 +- .../xrpl}/peerfinder/detail/Handouts.h | 7 +- .../xrpl}/peerfinder/detail/Livecache.h | 18 +- .../xrpl}/peerfinder/detail/Logic.h | 95 +- .../xrpl}/peerfinder/detail/SlotImp.h | 7 +- .../xrpl}/peerfinder/detail/Source.h | 3 +- .../xrpl}/peerfinder/detail/SourceStrings.h | 2 +- .../xrpl}/peerfinder/detail/Store.h | 0 .../xrpl}/peerfinder/detail/Tuning.h | 0 include/xrpl/peerfinder/make_Manager.h | 36 + .../peerfinder}/Bootcache.cpp | 38 +- src/libxrpl/peerfinder/Config.cpp | 135 ++ .../peerfinder}/Endpoint.cpp | 4 +- .../peerfinder}/PeerfinderManager.cpp | 32 +- .../detail => libxrpl/peerfinder}/SlotImp.cpp | 10 +- .../peerfinder}/SourceStrings.cpp | 5 +- src/test/overlay/TMGetObjectByHash_test.cpp | 2 +- src/test/overlay/tx_reduce_relay_test.cpp | 2 +- src/test/peerfinder/Livecache_test.cpp | 212 --- src/test/peerfinder/PeerFinder_test.cpp | 789 ---------- src/tests/libxrpl/CMakeLists.txt | 3 +- src/tests/libxrpl/main.cpp | 3 +- src/tests/libxrpl/peerfinder/Livecache.cpp | 294 ++++ src/tests/libxrpl/peerfinder/PeerFinder.cpp | 1270 +++++++++++++++++ src/xrpld/app/rdb/PeerFinder.h | 3 +- src/xrpld/app/rdb/detail/PeerFinder.cpp | 3 +- src/xrpld/overlay/detail/ConnectAttempt.cpp | 4 +- src/xrpld/overlay/detail/ConnectAttempt.h | 2 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 11 +- src/xrpld/overlay/detail/OverlayImpl.h | 6 +- src/xrpld/overlay/detail/PeerImp.cpp | 4 +- src/xrpld/overlay/detail/PeerImp.h | 4 +- src/xrpld/peerfinder/PeerfinderManager.h | 365 +---- .../peerfinder/detail/PeerfinderConfig.cpp | 113 +- src/xrpld/peerfinder/detail/StoreSqdb.h | 2 +- src/xrpld/peerfinder/detail/iosformat.h | 201 --- src/xrpld/peerfinder/make_Manager.h | 26 - 48 files changed, 2308 insertions(+), 1839 deletions(-) create mode 100644 include/xrpl/peerfinder/Config.h create mode 100644 include/xrpl/peerfinder/PeerfinderManager.h rename {src/xrpld => include/xrpl}/peerfinder/Slot.h (100%) create mode 100644 include/xrpl/peerfinder/Types.h rename {src/xrpld => include/xrpl}/peerfinder/detail/Bootcache.h (97%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Checker.h (100%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Counts.h (98%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Fixed.h (92%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Handouts.h (98%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Livecache.h (95%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Logic.h (91%) rename {src/xrpld => include/xrpl}/peerfinder/detail/SlotImp.h (96%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Source.h (95%) rename {src/xrpld => include/xrpl}/peerfinder/detail/SourceStrings.h (90%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Store.h (100%) rename {src/xrpld => include/xrpl}/peerfinder/detail/Tuning.h (100%) create mode 100644 include/xrpl/peerfinder/make_Manager.h rename src/{xrpld/peerfinder/detail => libxrpl/peerfinder}/Bootcache.cpp (80%) create mode 100644 src/libxrpl/peerfinder/Config.cpp rename src/{xrpld/peerfinder/detail => libxrpl/peerfinder}/Endpoint.cpp (76%) rename src/{xrpld/peerfinder/detail => libxrpl/peerfinder}/PeerfinderManager.cpp (92%) rename src/{xrpld/peerfinder/detail => libxrpl/peerfinder}/SlotImp.cpp (93%) rename src/{xrpld/peerfinder/detail => libxrpl/peerfinder}/SourceStrings.cpp (93%) delete mode 100644 src/test/peerfinder/Livecache_test.cpp delete mode 100644 src/test/peerfinder/PeerFinder_test.cpp create mode 100644 src/tests/libxrpl/peerfinder/Livecache.cpp create mode 100644 src/tests/libxrpl/peerfinder/PeerFinder.cpp delete mode 100644 src/xrpld/peerfinder/detail/iosformat.h delete mode 100644 src/xrpld/peerfinder/make_Manager.h diff --git a/.github/scripts/levelization/results/loops.txt b/.github/scripts/levelization/results/loops.txt index cf70468e32..ea7b8a372a 100644 --- a/.github/scripts/levelization/results/loops.txt +++ b/.github/scripts/levelization/results/loops.txt @@ -1,9 +1,6 @@ Loop: xrpld.app xrpld.overlay xrpld.app > xrpld.overlay -Loop: xrpld.app xrpld.peerfinder - xrpld.peerfinder ~= xrpld.app - Loop: xrpld.app xrpld.rpc xrpld.rpc > xrpld.app diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 3c9c514516..fdd134dc8a 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -25,6 +25,9 @@ libxrpl.nodestore > xrpl.config libxrpl.nodestore > xrpl.json libxrpl.nodestore > xrpl.nodestore libxrpl.nodestore > xrpl.protocol +libxrpl.peerfinder > xrpl.basics +libxrpl.peerfinder > xrpl.peerfinder +libxrpl.peerfinder > xrpl.protocol libxrpl.protocol > xrpl.basics libxrpl.protocol > xrpl.json libxrpl.protocol > xrpl.protocol @@ -146,19 +149,13 @@ test.overlay > xrpl.config test.overlay > xrpld.app test.overlay > xrpld.core test.overlay > xrpld.overlay -test.overlay > xrpld.peerfinder test.overlay > xrpl.json test.overlay > xrpl.nodestore +test.overlay > xrpl.peerfinder test.overlay > xrpl.protocol test.overlay > xrpl.resource test.overlay > xrpl.server test.overlay > xrpl.shamap -test.peerfinder > test.beast -test.peerfinder > test.unit_test -test.peerfinder > xrpl.basics -test.peerfinder > xrpld.core -test.peerfinder > xrpld.peerfinder -test.peerfinder > xrpl.protocol test.protocol > test.jtx test.protocol > test.unit_test test.protocol > xrpl.basics @@ -197,6 +194,7 @@ tests.libxrpl > xrpl.json tests.libxrpl > xrpl.ledger tests.libxrpl > xrpl.net tests.libxrpl > xrpl.nodestore +tests.libxrpl > xrpl.peerfinder tests.libxrpl > xrpl.protocol tests.libxrpl > xrpl.protocol_autogen tests.libxrpl > xrpl.resource @@ -220,6 +218,8 @@ xrpl.nodestore > xrpl.basics xrpl.nodestore > xrpl.config xrpl.nodestore > xrpl.json xrpl.nodestore > xrpl.protocol +xrpl.peerfinder > xrpl.basics +xrpl.peerfinder > xrpl.protocol xrpl.protocol > xrpl.basics xrpl.protocol > xrpl.json xrpl.protocol_autogen > xrpl.json @@ -253,6 +253,7 @@ xrpld.app > xrpl.json xrpld.app > xrpl.ledger xrpld.app > xrpl.net xrpld.app > xrpl.nodestore +xrpld.app > xrpl.peerfinder xrpld.app > xrpl.protocol xrpld.app > xrpl.rdb xrpld.app > xrpl.resource @@ -277,15 +278,16 @@ xrpld.overlay > xrpld.core xrpld.overlay > xrpld.peerfinder xrpld.overlay > xrpl.json xrpld.overlay > xrpl.ledger +xrpld.overlay > xrpl.peerfinder xrpld.overlay > xrpl.protocol xrpld.overlay > xrpl.resource xrpld.overlay > xrpl.server xrpld.overlay > xrpl.shamap xrpld.overlay > xrpl.tx xrpld.peerfinder > xrpl.basics -xrpld.peerfinder > xrpl.config +xrpld.peerfinder > xrpld.app xrpld.peerfinder > xrpld.core -xrpld.peerfinder > xrpl.protocol +xrpld.peerfinder > xrpl.peerfinder xrpld.peerfinder > xrpl.rdb xrpld.perflog > xrpl.basics xrpld.perflog > xrpl.config diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index 3e49267715..62a8fe143b 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -133,6 +133,12 @@ target_link_libraries( add_module(xrpl resource) target_link_libraries(xrpl.libxrpl.resource PUBLIC xrpl.libxrpl.protocol) +add_module(xrpl peerfinder) +target_link_libraries( + xrpl.libxrpl.peerfinder + PUBLIC xrpl.libxrpl.basics xrpl.libxrpl.protocol +) + # Level 08 add_module(xrpl net) target_link_libraries( @@ -227,6 +233,7 @@ target_link_modules( ledger net nodestore + peerfinder protocol protocol_autogen rdb diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index db10e8cc23..c4287b1ca1 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include diff --git a/include/xrpl/peerfinder/Config.h b/include/xrpl/peerfinder/Config.h new file mode 100644 index 0000000000..9ff0d342c3 --- /dev/null +++ b/include/xrpl/peerfinder/Config.h @@ -0,0 +1,163 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { + +struct PeerLimitConfig +{ + std::optional maxPeers; + std::optional inPeers; + std::optional outPeers; +}; + +/** + * PeerFinder configuration settings. + */ +struct Config +{ + /** + * The largest number of public peer slots to allow. + * This includes both inbound and outbound, but does not include + * fixed peers. + */ + std::size_t maxPeers{Tuning::kDefaultMaxPeers}; + + /** + * The number of automatic outbound connections to maintain. + * Outbound connections are only maintained if autoConnect + * is `true`. + */ + std::size_t outPeers = calcOutPeers(); // Note: relies on `maxPeers` being initialized + + /** + * The number of automatic inbound connections to maintain. + * Inbound connections are only maintained if wantIncoming + * is `true`. + */ + std::size_t inPeers{0}; + + /** + * `true` if we want our IP address kept private. + */ + bool peerPrivate = true; + + /** + * `true` if we want to accept incoming connections. + */ + bool wantIncoming{true}; + + /** + * `true` if we want to establish connections automatically + */ + bool autoConnect{true}; + + /** + * The listening port number. + */ + std::uint16_t listeningPort{0}; + + /** + * The set of features we advertise. + */ + std::string features; + + /** + * Limit how many incoming connections we allow per IP + */ + int ipLimit{0}; + + /** + * `true` if we want to verify endpoints in TMEndpoints messages + */ + bool verifyEndpoints = true; + + //-------------------------------------------------------------------------- + + /** + * Returns a suitable value for outPeers according to the rules. + */ + [[nodiscard]] std::size_t + calcOutPeers() const; + + /** + * Adjusts the values so they follow the business rules. + */ + void + applyTuning(); + + /** + * Write the configuration into a property stream + */ + void + onWrite(beast::PropertyStream::Map& map) const; + + /** + * Make PeerFinder::Config from peer limit and server mode parameters. + */ + static Config + makeConfig( + bool peerPrivate, + bool standalone, + PeerLimitConfig const& limits, + std::uint16_t port, + bool validationPublicKey, + int ipLimit, + bool verifyEndpoints); + + /** + * Compares two configurations for equality field by field. + */ + friend bool + operator==(Config const& lhs, Config const& rhs) = default; +}; + +//------------------------------------------------------------------------------ + +/** + * Possible results from activating a slot. + */ +enum class Result { InboundDisabled, DuplicatePeer, IpLimitExceeded, Full, Success }; + +/** + * @brief Converts a `Result` enum value to its string representation. + * + * This function provides a human-readable string for a given `Result` enum, + * which is useful for logging, debugging, or displaying status messages. + * + * @param result The `Result` enum value to convert. + * @return A `std::string_view` representing the enum value. Returns "unknown" + * if the enum value is not explicitly handled. + * + * @note This function returns a `std::string_view` for performance. + * A `std::string` would need to allocate memory on the heap and copy the + * string literal into it every time the function is called. + */ +inline std::string_view +to_string(Result result) noexcept +{ + switch (result) + { + case Result::InboundDisabled: + return "inbound disabled"; + case Result::DuplicatePeer: + return "peer already connected"; + case Result::IpLimitExceeded: + return "ip limit exceeded"; + case Result::Full: + return "slots full"; + case Result::Success: + return "success"; + } + + return "unknown"; +} + +} // namespace xrpl::PeerFinder diff --git a/include/xrpl/peerfinder/PeerfinderManager.h b/include/xrpl/peerfinder/PeerfinderManager.h new file mode 100644 index 0000000000..ed683520c1 --- /dev/null +++ b/include/xrpl/peerfinder/PeerfinderManager.h @@ -0,0 +1,179 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { + +/** + * Maintains a set of IP addresses used for getting into the network. + */ +class Manager : public beast::PropertyStream::Source +{ +protected: + Manager() noexcept; + +public: + /** + * Destroy the object. + * Any pending source fetch operations are aborted. + * There may be some listener calls made before the + * destructor returns. + */ + ~Manager() override = default; + + /** + * Set the configuration for the manager. + * The new settings will be applied asynchronously. + * Thread safety: + * Can be called from any threads at any time. + */ + virtual void + setConfig(Config const& config) = 0; + + /** + * Transition to the started state, synchronously. + */ + virtual void + start() = 0; + + /** + * Transition to the stopped state, synchronously. + */ + virtual void + stop() = 0; + + /** + * Returns the configuration for the manager. + */ + virtual Config + config() = 0; + + /** + * Add a peer that should always be connected. + * This is useful for maintaining a private cluster of peers. + * The string is the name as specified in the configuration + * file, along with the set of corresponding IP addresses. + */ + virtual void + addFixedPeer(std::string_view name, std::vector const& addresses) = 0; + + /** + * Add a set of strings as fallback IP::Endpoint sources. + * @param name A label used for diagnostics. + */ + virtual void + addFallbackStrings(std::string const& name, std::vector const& strings) = 0; + + /** + * Add a URL as a fallback location to obtain IP::Endpoint sources. + * @param name A label used for diagnostics. + */ + /* VFALCO NOTE Unimplemented + virtual void addFallbackURL (std::string const& name, + std::string const& url) = 0; + */ + + //-------------------------------------------------------------------------- + + /** + * Create a new inbound slot with the specified remote endpoint. + * If nullptr is returned, then the slot could not be assigned. + * Usually this is because of a detected self-connection. + */ + virtual std::pair, Result> + newInboundSlot( + beast::IP::Endpoint const& localEndpoint, + beast::IP::Endpoint const& remoteEndpoint) = 0; + + /** + * Create a new outbound slot with the specified remote endpoint. + * If nullptr is returned, then the slot could not be assigned. + * Usually this is because of a duplicate connection. + */ + virtual std::pair, Result> + newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) = 0; + + /** + * Called when mtENDPOINTS is received. + */ + virtual void + onEndpoints(std::shared_ptr const& slot, Endpoints const& endpoints) = 0; + + /** + * Called when the slot is closed. + * This always happens when the socket is closed, unless the socket + * was canceled. + */ + virtual void + onClosed(std::shared_ptr const& slot) = 0; + + /** + * Called when an outbound connection is deemed to have failed + */ + virtual void + onFailure(std::shared_ptr const& slot) = 0; + + /** + * Called when we received redirect IPs from a busy peer. + */ + virtual void + onRedirects( + boost::asio::ip::tcp::endpoint const& remoteAddress, + std::vector const& eps) = 0; + + //-------------------------------------------------------------------------- + + /** + * Called when an outbound connection attempt succeeds. + * The local endpoint must be valid. If the caller receives an error + * when retrieving the local endpoint from the socket, it should + * proceed as if the connection attempt failed by calling on_closed + * instead of on_connected. + * @return `true` if the connection should be kept + */ + virtual bool + onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) = 0; + + /** + * Request an active slot type. + */ + virtual Result + activate(std::shared_ptr const& slot, PublicKey const& key, bool reserved) = 0; + + /** + * Returns a set of endpoints suitable for redirection. + */ + virtual std::vector + redirect(std::shared_ptr const& slot) = 0; + + /** + * Return a set of addresses we should connect to. + */ + virtual std::vector + autoconnect() = 0; + + virtual std::vector, std::vector>> + buildEndpointsForPeers() = 0; + + /** + * Perform periodic activity. + * This should be called once per second. + */ + virtual void + oncePerSecond() = 0; +}; + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/Slot.h b/include/xrpl/peerfinder/Slot.h similarity index 100% rename from src/xrpld/peerfinder/Slot.h rename to include/xrpl/peerfinder/Slot.h diff --git a/include/xrpl/peerfinder/Types.h b/include/xrpl/peerfinder/Types.h new file mode 100644 index 0000000000..9e82d9d65c --- /dev/null +++ b/include/xrpl/peerfinder/Types.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace xrpl::PeerFinder { + +using clock_type = beast::AbstractClock; + +/** + * Represents a set of addresses. + */ +using IPAddresses = std::vector; + +//------------------------------------------------------------------------------ + +/** + * Describes a connectable peer address along with some metadata. + */ +struct Endpoint +{ + Endpoint() = default; + + Endpoint(beast::IP::Endpoint ep, std::uint32_t hops); + + std::uint32_t hops = 0; + beast::IP::Endpoint address; +}; + +inline bool +operator<(Endpoint const& lhs, Endpoint const& rhs) +{ + return lhs.address < rhs.address; +} + +/** + * A set of Endpoint used for connecting. + */ +using Endpoints = std::vector; + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/Bootcache.h b/include/xrpl/peerfinder/detail/Bootcache.h similarity index 97% rename from src/xrpld/peerfinder/detail/Bootcache.h rename to include/xrpl/peerfinder/detail/Bootcache.h index c84fed42c7..2141a374fa 100644 --- a/src/xrpld/peerfinder/detail/Bootcache.h +++ b/include/xrpl/peerfinder/detail/Bootcache.h @@ -1,11 +1,11 @@ #pragma once -#include -#include - #include #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Checker.h b/include/xrpl/peerfinder/detail/Checker.h similarity index 100% rename from src/xrpld/peerfinder/detail/Checker.h rename to include/xrpl/peerfinder/detail/Checker.h diff --git a/src/xrpld/peerfinder/detail/Counts.h b/include/xrpl/peerfinder/detail/Counts.h similarity index 98% rename from src/xrpld/peerfinder/detail/Counts.h rename to include/xrpl/peerfinder/detail/Counts.h index c90598c1a1..ce78eadce4 100644 --- a/src/xrpld/peerfinder/detail/Counts.h +++ b/include/xrpl/peerfinder/detail/Counts.h @@ -1,11 +1,10 @@ #pragma once -#include -#include -#include - #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Fixed.h b/include/xrpl/peerfinder/detail/Fixed.h similarity index 92% rename from src/xrpld/peerfinder/detail/Fixed.h rename to include/xrpl/peerfinder/detail/Fixed.h index 24d54775ef..6754ec6dbd 100644 --- a/src/xrpld/peerfinder/detail/Fixed.h +++ b/include/xrpl/peerfinder/detail/Fixed.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Handouts.h b/include/xrpl/peerfinder/detail/Handouts.h similarity index 98% rename from src/xrpld/peerfinder/detail/Handouts.h rename to include/xrpl/peerfinder/detail/Handouts.h index 757f1a8e1b..cb5fd7f850 100644 --- a/src/xrpld/peerfinder/detail/Handouts.h +++ b/include/xrpl/peerfinder/detail/Handouts.h @@ -1,12 +1,11 @@ #pragma once -#include -#include -#include - #include #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Livecache.h b/include/xrpl/peerfinder/detail/Livecache.h similarity index 95% rename from src/xrpld/peerfinder/detail/Livecache.h rename to include/xrpl/peerfinder/detail/Livecache.h index 2015098847..cac284d1cc 100644 --- a/src/xrpld/peerfinder/detail/Livecache.h +++ b/include/xrpl/peerfinder/detail/Livecache.h @@ -1,9 +1,5 @@ #pragma once -#include -#include -#include - #include #include #include @@ -12,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -22,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -411,7 +411,7 @@ Livecache::expire() } if (n > 0) { - JLOG(journal_.debug()) << beast::Leftw(18) << "Livecache expired " << n + JLOG(journal_.debug()) << std::left << std::setw(18) << "Livecache expired " << n << ((n > 1) ? " entries" : " entry"); } } @@ -434,7 +434,7 @@ Livecache::insert(Endpoint const& ep) if (result.second) { hops.insert(e); - JLOG(journal_.debug()) << beast::Leftw(18) << "Livecache insert " << ep.address + JLOG(journal_.debug()) << std::left << std::setw(18) << "Livecache insert " << ep.address << " at hops " << ep.hops; return; } @@ -442,7 +442,7 @@ Livecache::insert(Endpoint const& ep) { // Drop duplicates at higher hops std::size_t const excess(ep.hops - e.endpoint.hops); - JLOG(journal_.trace()) << beast::Leftw(18) << "Livecache drop " << ep.address + JLOG(journal_.trace()) << std::left << std::setw(18) << "Livecache drop " << ep.address << " at hops +" << excess; return; } @@ -453,12 +453,12 @@ Livecache::insert(Endpoint const& ep) if (ep.hops < e.endpoint.hops) { hops.reinsert(e, ep.hops); - JLOG(journal_.debug()) << beast::Leftw(18) << "Livecache update " << ep.address + JLOG(journal_.debug()) << std::left << std::setw(18) << "Livecache update " << ep.address << " at hops " << ep.hops; } else { - JLOG(journal_.trace()) << beast::Leftw(18) << "Livecache refresh " << ep.address + JLOG(journal_.trace()) << std::left << std::setw(18) << "Livecache refresh " << ep.address << " at hops " << ep.hops; } } diff --git a/src/xrpld/peerfinder/detail/Logic.h b/include/xrpl/peerfinder/detail/Logic.h similarity index 91% rename from src/xrpld/peerfinder/detail/Logic.h rename to include/xrpl/peerfinder/detail/Logic.h index a7dbbf850d..c623263884 100644 --- a/src/xrpld/peerfinder/detail/Logic.h +++ b/include/xrpl/peerfinder/detail/Logic.h @@ -1,17 +1,5 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include #include #include @@ -22,18 +10,34 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include + #include #include #include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -197,8 +201,8 @@ public: if (result.second) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic add fixed '" << name << "' at " << remoteAddress; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic add fixed '" << name + << "' at " << remoteAddress; return; } } @@ -221,7 +225,7 @@ public: if (iter == slots.end()) { // The slot disconnected before we finished the check - JLOG(journal.debug()) << beast::Leftw(18) << "Logic tested " << checkedAddress + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic tested " << checkedAddress << " but the connection was closed"; return; } @@ -255,7 +259,7 @@ public: beast::IP::Endpoint const& localEndpoint, beast::IP::Endpoint const& remoteEndpoint) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic accept" << remoteEndpoint + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic accept" << remoteEndpoint << " on local " << localEndpoint; std::scoped_lock const _(lock); @@ -266,7 +270,7 @@ public: auto const count = connectedAddresses.count(remoteEndpoint.address()); if (count + 1 > config_.ipLimit) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic dropping inbound " + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic dropping inbound " << remoteEndpoint << " because of ip limits."; return {SlotImp::ptr(), Result::IpLimitExceeded}; } @@ -275,8 +279,8 @@ public: // Check for duplicate connection if (slots.contains(remoteEndpoint)) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic dropping " << remoteEndpoint - << " as duplicate incoming"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic dropping " + << remoteEndpoint << " as duplicate incoming"; return {SlotImp::ptr(), Result::DuplicatePeer}; } @@ -304,15 +308,15 @@ public: std::pair newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic connect " << remoteEndpoint; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " << remoteEndpoint; std::scoped_lock const _(lock); // Check for duplicate connection if (slots.contains(remoteEndpoint)) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic dropping " << remoteEndpoint - << " as duplicate connect"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic dropping " + << remoteEndpoint << " as duplicate connect"; return {SlotImp::ptr(), Result::DuplicatePeer}; } @@ -506,15 +510,15 @@ public: if (!h.list().empty()) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic connect " << h.list().size() << " fixed"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " + << h.list().size() << " fixed"; return h.list(); } if (counts_.attempts() > 0) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic waiting on " << counts_.attempts() << " attempts"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic waiting on " + << counts_.attempts() << " attempts"; return none; } } @@ -535,14 +539,14 @@ public: if (!h.list().empty()) { JLOG(journal.debug()) - << beast::Leftw(18) << "Logic connect " << h.list().size() << " live " + << std::left << std::setw(18) << "Logic connect " << h.list().size() << " live " << ((h.list().size() > 1) ? "endpoints" : "endpoint"); return h.list(); } if (counts_.attempts() > 0) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Logic waiting on " << counts_.attempts() << " attempts"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic waiting on " + << counts_.attempts() << " attempts"; return none; } } @@ -568,8 +572,9 @@ public: if (!h.list().empty()) { - JLOG(journal.debug()) << beast::Leftw(18) << "Logic connect " << h.list().size() - << " boot " << ((h.list().size() > 1) ? "addresses" : "address"); + JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " + << h.list().size() << " boot " + << ((h.list().size() > 1) ? "addresses" : "address"); return h.list(); } @@ -689,8 +694,8 @@ public: // Enforce hop limit if (ep.hops > Tuning::kMaxHops) { - JLOG(journal.debug()) << beast::Leftw(18) << "Endpoints drop " << ep.address - << " for excess hops " << ep.hops; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " for excess hops " << ep.hops; iter = list.erase(iter); continue; } @@ -706,18 +711,18 @@ public: } else { - JLOG(journal.debug()) - << beast::Leftw(18) << "Endpoints drop " << ep.address << " for extra self"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " for extra self"; iter = list.erase(iter); continue; } } // Discard invalid addresses - if (config_.verifyEndpoints && !isValidAddress(ep.address)) + if (!isValidAddress(ep.address)) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Endpoints drop " << ep.address << " as invalid"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " as invalid"; iter = list.erase(iter); continue; } @@ -727,8 +732,8 @@ public: return ep.address == other.address; })) { - JLOG(journal.debug()) - << beast::Leftw(18) << "Endpoints drop " << ep.address << " as duplicate"; + JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " + << ep.address << " as duplicate"; iter = list.erase(iter); continue; } @@ -1074,13 +1079,13 @@ public: if (!results.error) { int const count(addBootcacheAddresses(results.addresses)); - JLOG(journal.info()) << beast::Leftw(18) << "Logic added " << count << " new " + JLOG(journal.info()) << std::left << std::setw(18) << "Logic added " << count << " new " << ((count == 1) ? "address" : "addresses") << " from " << source->name(); } else { - JLOG(journal.error()) << beast::Leftw(18) << "Logic failed " + JLOG(journal.error()) << std::left << std::setw(18) << "Logic failed " << "'" << source->name() << "' fetch, " << results.error.message(); } @@ -1098,8 +1103,6 @@ public: { if (isUnspecified(address)) return false; - if (isLoopback(address)) - return false; if (!isPublic(address)) return false; if (address.port() == 0) @@ -1221,8 +1224,8 @@ Logic::onRedirects( bootcache.insert(beast::IPAddressConversion::fromAsio(*first)); if (n > 0) { - JLOG(journal.trace()) << beast::Leftw(18) << "Logic add " << n << " redirect IPs from " - << remoteAddress; + JLOG(journal.trace()) << std::left << std::setw(18) << "Logic add " << n + << " redirect IPs from " << remoteAddress; } } diff --git a/src/xrpld/peerfinder/detail/SlotImp.h b/include/xrpl/peerfinder/detail/SlotImp.h similarity index 96% rename from src/xrpld/peerfinder/detail/SlotImp.h rename to include/xrpl/peerfinder/detail/SlotImp.h index 898941b157..35c61b13cf 100644 --- a/src/xrpld/peerfinder/detail/SlotImp.h +++ b/include/xrpl/peerfinder/detail/SlotImp.h @@ -1,10 +1,9 @@ #pragma once -#include -#include - #include #include +#include +#include #include #include @@ -172,7 +171,7 @@ private: std::optional localEndpoint_; std::optional publicKey_; - static constexpr std::int32_t kUnknownPort = -1; + static std::int32_t constexpr kUnknownPort = -1; std::atomic listeningPort_; public: diff --git a/src/xrpld/peerfinder/detail/Source.h b/include/xrpl/peerfinder/detail/Source.h similarity index 95% rename from src/xrpld/peerfinder/detail/Source.h rename to include/xrpl/peerfinder/detail/Source.h index b205dc8dfb..5cdb535bdd 100644 --- a/src/xrpld/peerfinder/detail/Source.h +++ b/include/xrpl/peerfinder/detail/Source.h @@ -1,8 +1,7 @@ #pragma once -#include - #include +#include #include diff --git a/src/xrpld/peerfinder/detail/SourceStrings.h b/include/xrpl/peerfinder/detail/SourceStrings.h similarity index 90% rename from src/xrpld/peerfinder/detail/SourceStrings.h rename to include/xrpl/peerfinder/detail/SourceStrings.h index b79cf0df03..325a024764 100644 --- a/src/xrpld/peerfinder/detail/SourceStrings.h +++ b/include/xrpl/peerfinder/detail/SourceStrings.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/Store.h b/include/xrpl/peerfinder/detail/Store.h similarity index 100% rename from src/xrpld/peerfinder/detail/Store.h rename to include/xrpl/peerfinder/detail/Store.h diff --git a/src/xrpld/peerfinder/detail/Tuning.h b/include/xrpl/peerfinder/detail/Tuning.h similarity index 100% rename from src/xrpld/peerfinder/detail/Tuning.h rename to include/xrpl/peerfinder/detail/Tuning.h diff --git a/include/xrpl/peerfinder/make_Manager.h b/include/xrpl/peerfinder/make_Manager.h new file mode 100644 index 0000000000..5da372e588 --- /dev/null +++ b/include/xrpl/peerfinder/make_Manager.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include + +namespace xrpl::PeerFinder { + +/** + * @brief Create a new Manager. + * + * @param ioContext The io_context used to schedule asynchronous work. + * @param clock The clock used for timekeeping. + * @param journal The journal used for logging. + * @param store The persistence backend for the bootstrap cache. The caller + * retains ownership and must keep it alive (and opened) for the lifetime of + * the returned Manager. This lets consumers supply their own Store + * implementation (e.g. the SQLite-backed StoreSqdb in xrpld). + * @param collector The collector used to report metrics. + * @return The newly created Manager. + */ +std::unique_ptr +makeManager( + boost::asio::io_context& ioContext, + clock_type& clock, + beast::Journal journal, + Store& store, + beast::insight::Collector::ptr const& collector); + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/Bootcache.cpp b/src/libxrpl/peerfinder/Bootcache.cpp similarity index 80% rename from src/xrpld/peerfinder/detail/Bootcache.cpp rename to src/libxrpl/peerfinder/Bootcache.cpp index a0a753530b..a2a56b4d01 100644 --- a/src/xrpld/peerfinder/detail/Bootcache.cpp +++ b/src/libxrpl/peerfinder/Bootcache.cpp @@ -1,19 +1,19 @@ -#include - -#include -#include -#include -#include +#include #include #include #include #include #include +#include +#include +#include #include #include #include +#include +#include #include namespace xrpl::PeerFinder { @@ -82,13 +82,14 @@ Bootcache::load() auto const result(this->map_.insert(value_type(endpoint, valence))); if (!result.second) { - JLOG(this->journal_.error()) << beast::Leftw(18) << "Bootcache discard " << endpoint; + JLOG(this->journal_.error()) + << std::left << std::setw(18) << "Bootcache discard " << endpoint; } })); if (n > 0) { - JLOG(journal_.info()) << beast::Leftw(18) << "Bootcache loaded " << n + JLOG(journal_.info()) << std::left << std::setw(18) << "Bootcache loaded " << n << ((n > 1) ? " addresses" : " address"); prune(); } @@ -100,7 +101,7 @@ Bootcache::insert(beast::IP::Endpoint const& endpoint) auto const result(map_.insert(value_type(endpoint, 0))); if (result.second) { - JLOG(journal_.trace()) << beast::Leftw(18) << "Bootcache insert " << endpoint; + JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache insert " << endpoint; prune(); flagForUpdate(); } @@ -121,7 +122,7 @@ Bootcache::insertStatic(beast::IP::Endpoint const& endpoint) if (result.second) { - JLOG(journal_.trace()) << beast::Leftw(18) << "Bootcache insert " << endpoint; + JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache insert " << endpoint; prune(); flagForUpdate(); } @@ -146,8 +147,9 @@ Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) XRPL_ASSERT(result.second, "xrpl::PeerFinder::Bootcache::onSuccess : endpoint inserted"); } Entry const& entry(result.first->right); - JLOG(journal_.info()) << beast::Leftw(18) << "Bootcache connect " << endpoint << " with " - << entry.valence() << ((entry.valence() > 1) ? " successes" : " success"); + JLOG(journal_.info()) << std::left << std::setw(18) << "Bootcache connect " << endpoint + << " with " << entry.valence() + << ((entry.valence() > 1) ? " successes" : " success"); flagForUpdate(); } @@ -170,8 +172,8 @@ Bootcache::onFailure(beast::IP::Endpoint const& endpoint) } Entry const& entry(result.first->right); auto const n(std::abs(entry.valence())); - JLOG(journal_.debug()) << beast::Leftw(18) << "Bootcache failed " << endpoint << " with " << n - << ((n > 1) ? " attempts" : " attempt"); + JLOG(journal_.debug()) << std::left << std::setw(18) << "Bootcache failed " << endpoint + << " with " << n << ((n > 1) ? " attempts" : " attempt"); flagForUpdate(); } @@ -209,17 +211,19 @@ Bootcache::prune() // Work backwards because bimap doesn't handle // erasing using a reverse iterator very well. // - for (auto iter(map_.right.end()); count-- > 0 && iter != map_.right.begin(); ++pruned) + for (auto iter(map_.right.end()); count > 0 && iter != map_.right.begin(); ++pruned) { + --count; --iter; beast::IP::Endpoint const& endpoint(iter->get_left()); Entry const& entry(iter->get_right()); - JLOG(journal_.trace()) << beast::Leftw(18) << "Bootcache pruned" << endpoint + JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache pruned" << endpoint << " at valence " << entry.valence(); iter = map_.right.erase(iter); } - JLOG(journal_.debug()) << beast::Leftw(18) << "Bootcache pruned " << pruned << " entries total"; + JLOG(journal_.debug()) << std::left << std::setw(18) << "Bootcache pruned " << pruned + << " entries total"; } // Updates the Store with the current set of entries if needed. diff --git a/src/libxrpl/peerfinder/Config.cpp b/src/libxrpl/peerfinder/Config.cpp new file mode 100644 index 0000000000..3e2f74f42b --- /dev/null +++ b/src/libxrpl/peerfinder/Config.cpp @@ -0,0 +1,135 @@ +#include + +#include + +#include +#include +#include +#include + +namespace xrpl::PeerFinder { + +std::size_t +Config::calcOutPeers() const +{ + return std::max( + ((maxPeers * Tuning::kOutPercent) + 50) / 100, std::size_t(Tuning::kMinOutCount)); +} + +void +Config::applyTuning() +{ + if (ipLimit == 0) + { + // Unless a limit is explicitly set, we allow between + // 2 and 5 connections from non RFC-1918 "private" + // IP addresses. + ipLimit = 2; + + if (inPeers > Tuning::kDefaultMaxPeers) + ipLimit += std::min(5, static_cast(inPeers / Tuning::kDefaultMaxPeers)); + } + + // We don't allow a single IP to consume all incoming slots, + // unless we only have one incoming slot available. + ipLimit = std::max(1, std::min(ipLimit, static_cast(inPeers / 2))); +} + +void +Config::onWrite(beast::PropertyStream::Map& map) const +{ + map["max_peers"] = maxPeers; + map["out_peers"] = outPeers; + map["want_incoming"] = wantIncoming; + map["auto_connect"] = autoConnect; + map["port"] = listeningPort; + map["features"] = features; + map["ip_limit"] = ipLimit; + map["verify_endpoints"] = verifyEndpoints; +} + +Config +Config::makeConfig( + bool peerPrivate, + bool standalone, + PeerLimitConfig const& limits, + std::uint16_t port, + bool validationPublicKey, + int ipLimit, + bool verifyEndpoints) +{ + PeerFinder::Config config; + + if (!limits.maxPeers) + { + if (limits.inPeers && !limits.outPeers) + throw std::runtime_error("Both inbound and outbound peer limits must be configured"); + + if (limits.outPeers && !limits.inPeers) + throw std::runtime_error("Both inbound and outbound peer limits must be configured"); + + if (limits.inPeers && *limits.inPeers > 1000) + throw std::runtime_error("Inbound peer limit must be less than or equal to 1000"); + + if (limits.outPeers && (*limits.outPeers < 10 || *limits.outPeers > 1000)) + throw std::runtime_error("Outbound peer limit must be in the range 10-1000"); + } + + config.peerPrivate = peerPrivate; + + // Servers with peer privacy don't want to allow incoming connections + config.wantIncoming = (!config.peerPrivate) && (port != 0); + + if (limits.maxPeers || (!limits.inPeers && !limits.outPeers)) + { + if (limits.maxPeers && *limits.maxPeers != 0) + config.maxPeers = *limits.maxPeers; + + config.maxPeers = std::max(config.maxPeers, Tuning::kMinOutCount); + config.outPeers = config.calcOutPeers(); + + // Calculate the number of outbound peers we want. If we dont want + // or can't accept incoming, this will simply be equal to maxPeers. + if (!config.wantIncoming) + config.outPeers = config.maxPeers; + + // Calculate the largest number of inbound connections we could + // take. + if (config.maxPeers >= config.outPeers) + { + config.inPeers = config.maxPeers - config.outPeers; + } + else + { + config.inPeers = 0; + } + } + else + { + config.outPeers = *limits.outPeers; + config.inPeers = *limits.inPeers; + config.maxPeers = 0; + } + + // This will cause servers configured as validators to request that + // peers they connect to never report their IP address. We set this + // after we set the 'wantIncoming' because we want a "soft" version + // of peer privacy unless the operator explicitly asks for it. + if (validationPublicKey) + config.peerPrivate = true; + + // if it's a private peer or we are running as standalone + // automatic connections would defeat the purpose. + config.autoConnect = !standalone && !peerPrivate; + config.listeningPort = port; + config.features = ""; + config.ipLimit = ipLimit; + config.verifyEndpoints = verifyEndpoints; + + // Enforce business rules + config.applyTuning(); + + return config; +} + +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/Endpoint.cpp b/src/libxrpl/peerfinder/Endpoint.cpp similarity index 76% rename from src/xrpld/peerfinder/detail/Endpoint.cpp rename to src/libxrpl/peerfinder/Endpoint.cpp index 15de5cd153..12f2725ea5 100644 --- a/src/xrpld/peerfinder/detail/Endpoint.cpp +++ b/src/libxrpl/peerfinder/Endpoint.cpp @@ -1,7 +1,5 @@ -#include -#include - #include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp b/src/libxrpl/peerfinder/PeerfinderManager.cpp similarity index 92% rename from src/xrpld/peerfinder/detail/PeerfinderManager.cpp rename to src/libxrpl/peerfinder/PeerfinderManager.cpp index 2727a03013..2219627f09 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp +++ b/src/libxrpl/peerfinder/PeerfinderManager.cpp @@ -1,11 +1,4 @@ -#include - -#include -#include -#include -#include -#include -#include +#include #include #include @@ -13,7 +6,15 @@ #include #include #include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include @@ -38,10 +39,9 @@ public: std::optional> work_; clock_type& clock_; beast::Journal journal_; - StoreSqdb store_; + Store& store_; Checker checker_; Logic logic_; - BasicConfig const& config_; // NOLINTEND(readability-identifier-naming) //-------------------------------------------------------------------------- @@ -50,16 +50,15 @@ public: boost::asio::io_context& ioContext, clock_type& clock, beast::Journal journal, - BasicConfig const& config, + Store& store, beast::insight::Collector::ptr const& collector) : io_context_(ioContext) , work_(std::in_place, boost::asio::make_work_guard(io_context_)) , clock_(clock) , journal_(journal) - , store_(journal) + , store_(store) , checker_(io_context_) , logic_(clock, store_, checker_, journal) - , config_(config) , stats_([this] { collectMetrics(); }, collector) { } @@ -206,7 +205,6 @@ public: void start() override { - store_.open(config_); logic_.load(); } @@ -261,10 +259,10 @@ makeManager( boost::asio::io_context& ioContext, clock_type& clock, beast::Journal journal, - BasicConfig const& config, + Store& store, beast::insight::Collector::ptr const& collector) { - return std::make_unique(ioContext, clock, journal, config, collector); + return std::make_unique(ioContext, clock, journal, store, collector); } } // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/SlotImp.cpp b/src/libxrpl/peerfinder/SlotImp.cpp similarity index 93% rename from src/xrpld/peerfinder/detail/SlotImp.cpp rename to src/libxrpl/peerfinder/SlotImp.cpp index a54ddb56e7..0a4f32fd62 100644 --- a/src/xrpld/peerfinder/detail/SlotImp.cpp +++ b/src/libxrpl/peerfinder/SlotImp.cpp @@ -1,12 +1,10 @@ -#include +#include -#include -#include -#include - -#include #include #include +#include +#include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/SourceStrings.cpp b/src/libxrpl/peerfinder/SourceStrings.cpp similarity index 93% rename from src/xrpld/peerfinder/detail/SourceStrings.cpp rename to src/libxrpl/peerfinder/SourceStrings.cpp index 7b28db4306..f47e0cd51d 100644 --- a/src/xrpld/peerfinder/detail/SourceStrings.cpp +++ b/src/libxrpl/peerfinder/SourceStrings.cpp @@ -1,9 +1,8 @@ -#include - -#include +#include #include #include +#include #include #include diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index e579989181..f52220a90c 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -16,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index a0d91d3aed..43f6ef2506 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -9,12 +9,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include diff --git a/src/test/peerfinder/Livecache_test.cpp b/src/test/peerfinder/Livecache_test.cpp deleted file mode 100644 index 4f2d6e97e1..0000000000 --- a/src/test/peerfinder/Livecache_test.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include -#include - -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace xrpl::PeerFinder { - -bool -operator==(Endpoint const& a, Endpoint const& b) -{ - return (a.hops == b.hops && a.address == b.address); -} - -class Livecache_test : public beast::unit_test::Suite -{ - TestStopwatch clock_; - test::SuiteJournal journal_; - -public: - Livecache_test() : journal_("Livecache_test", *this) - { - } - - // Add the address as an endpoint - template - void - add(beast::IP::Endpoint ep, C& c, std::uint32_t hops = 0) - { - Endpoint const cep{ep, hops}; - c.insert(cep); - } - - void - testBasicInsert() - { - testcase("Basic Insert"); - Livecache<> c(clock_, journal_); - BEAST_EXPECT(c.empty()); - - for (auto i = 0; i < 10; ++i) - add(beast::IP::randomEP(true), c); - - BEAST_EXPECT(!c.empty()); - BEAST_EXPECT(c.size() == 10); - - for (auto i = 0; i < 10; ++i) - add(beast::IP::randomEP(false), c); - - BEAST_EXPECT(!c.empty()); - BEAST_EXPECT(c.size() == 20); - } - - void - testInsertUpdate() - { - testcase("Insert/Update"); - Livecache<> c(clock_, journal_); - - auto ep1 = Endpoint{beast::IP::randomEP(), 2}; - c.insert(ep1); - BEAST_EXPECT(c.size() == 1); - // third position list will contain the entry - BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2); - - auto ep2 = Endpoint{ep1.address, 4}; - // this will not change the entry has higher hops - c.insert(ep2); - BEAST_EXPECT(c.size() == 1); - // still in third position list - BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2); - - auto ep3 = Endpoint{ep1.address, 2}; - // this will not change the entry has the same hops as existing - c.insert(ep3); - BEAST_EXPECT(c.size() == 1); - // still in third position list - BEAST_EXPECT((c.hops.begin() + 2)->begin()->hops == 2); - - auto ep4 = Endpoint{ep1.address, 1}; - c.insert(ep4); - BEAST_EXPECT(c.size() == 1); - // now at second position list - BEAST_EXPECT((c.hops.begin() + 1)->begin()->hops == 1); - } - - void - testExpire() - { - testcase("Expire"); - using namespace std::chrono_literals; - Livecache<> c(clock_, journal_); - - auto ep1 = Endpoint{beast::IP::randomEP(), 1}; - c.insert(ep1); - BEAST_EXPECT(c.size() == 1); - c.expire(); - BEAST_EXPECT(c.size() == 1); - // verify that advancing to 1 sec before expiration - // leaves our entry intact - clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); - c.expire(); - BEAST_EXPECT(c.size() == 1); - // now advance to the point of expiration - clock_.advance(1s); - c.expire(); - BEAST_EXPECT(c.empty()); - } - - void - testHistogram() - { - testcase("Histogram"); - static constexpr auto kNumEps = 40; - Livecache<> c(clock_, journal_); - for (auto i = 0; i < kNumEps; ++i) - add(beast::IP::randomEP(true), c, xrpl::randInt()); - auto h = c.hops.histogram(); - if (!BEAST_EXPECT(!h.empty())) - return; - std::vector v; - boost::split(v, h, boost::algorithm::is_any_of(",")); - auto sum = 0; - for (auto const& n : v) - { - auto val = boost::lexical_cast(boost::trim_copy(n)); - sum += val; - BEAST_EXPECT(val >= 0); - } - BEAST_EXPECT(sum == kNumEps); - } - - void - testShuffle() - { - testcase("Shuffle"); - Livecache<> c(clock_, journal_); - for (auto i = 0; i < 100; ++i) - add(beast::IP::randomEP(true), c, xrpl::randInt(Tuning::kMaxHops + 1)); - - using at_hop = std::vector; - using all_hops = std::array; - - auto cmpEp = [](Endpoint const& a, Endpoint const& b) { - return (b.hops < a.hops || (b.hops == a.hops && b.address < a.address)); - }; - all_hops before; - all_hops beforeSorted; - for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end(); - ++i.first, ++i.second) - { - std::ranges::copy(*i.second, std::back_inserter(before[i.first])); - std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first])); - std::ranges::sort(beforeSorted[i.first], cmpEp); - } - - c.hops.shuffle(); - - all_hops after; - all_hops afterSorted; - for (auto i = std::make_pair(0, c.hops.begin()); i.second != c.hops.end(); - ++i.first, ++i.second) - { - std::ranges::copy(*i.second, std::back_inserter(after[i.first])); - std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first])); - std::ranges::sort(afterSorted[i.first], cmpEp); - } - - // each hop bucket should contain the same items - // before and after sort, albeit in different order - bool allMatch = true; - for (auto i = 0; i < before.size(); ++i) - { - BEAST_EXPECT(before[i].size() == after[i].size()); - allMatch = allMatch && (before[i] == after[i]); - BEAST_EXPECT(beforeSorted[i] == afterSorted[i]); - } - BEAST_EXPECT(!allMatch); - } - - void - run() override - { - testBasicInsert(); - testInsertUpdate(); - testExpire(); - testHistogram(); - testShuffle(); - } -}; - -BEAST_DEFINE_TESTSUITE(Livecache, peerfinder, xrpl); - -} // namespace xrpl::PeerFinder diff --git a/src/test/peerfinder/PeerFinder_test.cpp b/src/test/peerfinder/PeerFinder_test.cpp deleted file mode 100644 index cf91800951..0000000000 --- a/src/test/peerfinder/PeerFinder_test.cpp +++ /dev/null @@ -1,789 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::PeerFinder { - -class PeerFinder_test : public beast::unit_test::Suite -{ - test::SuiteJournal journal_; - -public: - PeerFinder_test() : journal_("PeerFinder_test", *this) - { - } - - struct TestStore : Store - { - std::size_t - load(load_callback const& cb) override - { - return 0; - } - - void - save(std::vector const&) override - { - } - }; - - struct TestChecker - { - void - stop() - { - } - - void - wait() - { - } - - template - void - asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) - { - // NOLINTNEXTLINE(misc-const-correctness) - boost::system::error_code ec; - handler(ec); - } - }; - - void - testBackoff1() - { - auto const seconds = 10000; - testcase("backoff 1"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5")); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - logic.config(c); - } - std::size_t n = 0; - for (std::size_t i = 0; i < seconds; ++i) - { - auto const list = logic.autoconnect(); - if (!list.empty()) - { - BEAST_EXPECT(list.size() == 1); - auto const [slot, _] = logic.newOutboundSlot(list.front()); - BEAST_EXPECT( - logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5"))); - logic.onClosed(slot); - ++n; - } - clock.advance(std::chrono::seconds(1)); - logic.oncePerSecond(); - } - // Less than 20 attempts - BEAST_EXPECT(n < 20); - } - - // with activate - void - testBackoff2() - { - auto const seconds = 10000; - testcase("backoff 2"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.1:5")); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - logic.config(c); - } - - PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); - std::size_t n = 0; - - for (std::size_t i = 0; i < seconds; ++i) - { - auto const list = logic.autoconnect(); - if (!list.empty()) - { - BEAST_EXPECT(list.size() == 1); - auto const [slot, _] = logic.newOutboundSlot(list.front()); - if (!BEAST_EXPECT( - logic.onConnected(slot, beast::IP::Endpoint::fromString("65.0.0.2:5")))) - return; - if (!BEAST_EXPECT(logic.activate(slot, pk, false) == PeerFinder::Result::Success)) - return; - logic.onClosed(slot); - ++n; - } - clock.advance(std::chrono::seconds(1)); - logic.oncePerSecond(); - } - // No more often than once per minute - BEAST_EXPECT(n <= (seconds + 59) / 60); - } - - // test accepting an incoming slot for an already existing outgoing slot - void - testDuplicateOutIn() - { - testcase("duplicate out/in"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); - auto const [slot1, r] = logic.newOutboundSlot(remote); - BEAST_EXPECT(slot1 != nullptr); - BEAST_EXPECT(r == Result::Success); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - auto const [slot2, r2] = logic.newInboundSlot(local, remote); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - BEAST_EXPECT(r2 == Result::DuplicatePeer); - - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.onClosed(slot2); - - logic.onClosed(slot1); - } - - // test establishing outgoing slot for an already existing incoming slot - void - testDuplicateInOut() - { - testcase("duplicate in/out"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - auto const [slot1, r] = logic.newInboundSlot(local, remote); - BEAST_EXPECT(slot1 != nullptr); - BEAST_EXPECT(r == Result::Success); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - - auto const [slot2, r2] = logic.newOutboundSlot(remote); - BEAST_EXPECT(r2 == Result::DuplicatePeer); - BEAST_EXPECT(logic.connectedAddresses.count(remote.address()) == 1); - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.onClosed(slot2); - logic.onClosed(slot1); - } - - void - testPeerLimitExceeded() - { - testcase("peer limit exceeded"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - auto const [slot, r] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025")); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(r == Result::Success); - - auto const [slot1, r1] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026")); - BEAST_EXPECT(slot1 != nullptr); - BEAST_EXPECT(r1 == Result::Success); - - auto const [slot2, r2] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1027")); - BEAST_EXPECT(r2 == Result::IpLimitExceeded); - - if (!BEAST_EXPECT(slot2 == nullptr)) - logic.onClosed(slot2); - logic.onClosed(slot1); - logic.onClosed(slot); - } - - void - testActivateDuplicatePeer() - { - testcase("test activate duplicate peer"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first); - - auto const [slot, rSlot] = - logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1025")); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(rSlot == Result::Success); - - auto const [slot2, r2Slot] = - logic.newOutboundSlot(beast::IP::Endpoint::fromString("55.104.0.2:1026")); - BEAST_EXPECT(slot2 != nullptr); - BEAST_EXPECT(r2Slot == Result::Success); - - BEAST_EXPECT(logic.onConnected(slot, local)); - BEAST_EXPECT(logic.onConnected(slot2, local)); - - BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success); - - // activating a different slot with the same node ID (pk) must fail - BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::DuplicatePeer); - - logic.onClosed(slot); - - // accept the same key for a new slot after removing the old slot - BEAST_EXPECT(logic.activate(slot2, pk1, false) == Result::Success); - logic.onClosed(slot2); - } - - void - testActivateInboundDisabled() - { - testcase("test activate inbound disabled"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - logic.config(c); - } - - PublicKey const pk1(randomKeyPair(KeyType::Secp256k1).first); - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - auto const [slot, rSlot] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1025")); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(rSlot == Result::Success); - - BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::InboundDisabled); - - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - c.inPeers = 1; - logic.config(c); - } - // new inbound slot must succeed when inbound connections are enabled - BEAST_EXPECT(logic.activate(slot, pk1, false) == Result::Success); - - // creating a new inbound slot must succeed as IP Limit is not exceeded - auto const [slot2, r2Slot] = - logic.newInboundSlot(local, beast::IP::Endpoint::fromString("55.104.0.2:1026")); - BEAST_EXPECT(slot2 != nullptr); - BEAST_EXPECT(r2Slot == Result::Success); - - PublicKey const pk2(randomKeyPair(KeyType::Secp256k1).first); - - // an inbound slot exceeding inPeers limit must fail - BEAST_EXPECT(logic.activate(slot2, pk2, false) == Result::Full); - - logic.onClosed(slot2); - logic.onClosed(slot); - } - - void - testAddFixedPeerNoPort() - { - testcase("test addFixedPeer no port"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - try - { - logic.addFixedPeer("test", beast::IP::Endpoint::fromString("65.0.0.2")); - fail("invalid endpoint successfully added"); - } - catch (std::runtime_error const& e) - { - pass(); - } - } - - void - testIsValidAddress() - { - testcase("is_valid_address"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - - auto const pass = [&](std::string const& s) { - BEAST_EXPECT(logic.isValidAddress(beast::IP::Endpoint::fromString(s))); - }; - auto const fail = [&](std::string const& s) { - BEAST_EXPECT(!logic.isValidAddress(beast::IP::Endpoint::fromString(s))); - }; - - // Invalid: port 0 - fail("65.0.0.1:0"); - - // --- IPv4 ranges --- - // For each range: 1 before (pass), first (fail), last (fail), - // 1 after (pass) - - // 0.0.0.0/8 - "This network" - // No "before" - nothing before 0.0.0.0 - fail("0.0.0.0:8080"); - fail("0.255.255.255:8080"); - pass("1.0.0.0:8080"); - - // 10.0.0.0/8 - Private (RFC 1918) - pass("9.255.255.255:8080"); - fail("10.0.0.0:8080"); - fail("10.255.255.255:8080"); - pass("11.0.0.0:8080"); - - // 100.64.0.0/10 - Shared Address Space / CGNAT (RFC 6598) - pass("100.63.255.255:8080"); - fail("100.64.0.0:8080"); - fail("100.127.255.255:8080"); - pass("100.128.0.0:8080"); - - // 127.0.0.0/8 - Loopback - pass("126.255.255.255:8080"); - fail("127.0.0.0:8080"); - fail("127.255.255.255:8080"); - pass("128.0.0.0:8080"); - - // 169.254.0.0/16 - Link-local - pass("169.253.255.255:8080"); - fail("169.254.0.0:8080"); - fail("169.254.255.255:8080"); - pass("169.255.0.0:8080"); - - // 172.16.0.0/12 - Private (RFC 1918) - pass("172.15.255.255:8080"); - fail("172.16.0.0:8080"); - fail("172.31.255.255:8080"); - pass("172.32.0.0:8080"); - - // 192.0.0.0/24 - IETF Protocol Assignments (RFC 6890) - pass("191.255.255.255:8080"); - fail("192.0.0.0:8080"); - fail("192.0.0.255:8080"); - pass("192.0.1.0:8080"); - - // 192.0.2.0/24 - TEST-NET-1 (RFC 5737) - pass("192.0.1.255:8080"); - fail("192.0.2.0:8080"); - fail("192.0.2.255:8080"); - pass("192.0.3.0:8080"); - - // 192.88.99.0/24 - 6to4 Relay Anycast (RFC 7526) - pass("192.88.98.255:8080"); - fail("192.88.99.0:8080"); - fail("192.88.99.255:8080"); - pass("192.88.100.0:8080"); - - // 192.168.0.0/16 - Private (RFC 1918) - pass("192.167.255.255:8080"); - fail("192.168.0.0:8080"); - fail("192.168.255.255:8080"); - pass("192.169.0.0:8080"); - - // 198.18.0.0/15 - Benchmarking (RFC 2544) - pass("198.17.255.255:8080"); - fail("198.18.0.0:8080"); - fail("198.19.255.255:8080"); - pass("198.20.0.0:8080"); - - // 198.51.100.0/24 - TEST-NET-2 (RFC 5737) - pass("198.51.99.255:8080"); - fail("198.51.100.0:8080"); - fail("198.51.100.255:8080"); - pass("198.51.101.0:8080"); - - // 203.0.113.0/24 - TEST-NET-3 (RFC 5737) - pass("203.0.112.255:8080"); - fail("203.0.113.0:8080"); - fail("203.0.113.255:8080"); - pass("203.0.114.0:8080"); - - // 224.0.0.0/4 - Multicast - pass("223.255.255.255:8080"); - fail("224.0.0.0:8080"); - fail("239.255.255.255:8080"); - // 240.0.0.0 (after multicast) is also blocked (reserved) - - // 240.0.0.0/4 - Reserved (RFC 1112) - // 239.255.255.255 (before reserved) is also blocked (multicast) - fail("240.0.0.0:8080"); - fail("255.255.255.255:8080"); - - // --- IPv6 ranges --- - - // ::1 - Loopback (single address) - fail("[::1]:8080"); - - // :: - Unspecified (single address) - fail("[::]:8080"); - - // fc00::/7 - Unique Local Address (ULA) - pass("[fb00::1]:8080"); - fail("[fc00::1]:8080"); - fail("[fdff::1]:8080"); - pass("[fe00::1]:8080"); - - // fe80::/10 - Link-local - pass("[fe7f::1]:8080"); - fail("[fe80::1]:8080"); - fail("[febf::1]:8080"); - pass("[fec0::1]:8080"); - - // ff00::/8 - Multicast - pass("[feff::1]:8080"); - fail("[ff00::1]:8080"); - fail("[ffff::1]:8080"); - // No "after" - ffff:... is the highest IPv6 range - - // 100::/64 - Discard prefix (RFC 6666) - pass("[ff::1]:8080"); - fail("[100::]:8080"); - fail("[100::ffff:ffff:ffff:ffff]:8080"); - pass("[100:0:0:1::1]:8080"); - - // 2001::/32 - IETF Protocol Assignments / Teredo (RFC 4380) - pass("[2000:ffff::1]:8080"); - fail("[2001::]:8080"); - fail("[2001:0:ffff::1]:8080"); - pass("[2001:1::1]:8080"); - - // 2001:20::/28 - ORCHIDv2 (RFC 7343) - pass("[2001:1f::1]:8080"); - fail("[2001:20::1]:8080"); - fail("[2001:2f::1]:8080"); - pass("[2001:30::1]:8080"); - - // 2001:db8::/32 - Documentation (RFC 3849) - pass("[2001:db7::1]:8080"); - fail("[2001:db8::1]:8080"); - fail("[2001:db8:ffff::1]:8080"); - pass("[2001:db9::1]:8080"); - - // 2002::/16 - 6to4 (RFC 3056, deprecated) - pass("[2001:ffff::1]:8080"); - fail("[2002::1]:8080"); - fail("[2002:ffff::1]:8080"); - pass("[2003::1]:8080"); - - // --- IPv6 v4-mapped (delegates to IPv4 checks) --- - fail("[::ffff:10.0.0.1]:8080"); - fail("[::ffff:100.64.0.1]:8080"); - fail("[::ffff:169.254.1.1]:8080"); - fail("[::ffff:192.0.2.1]:8080"); - fail("[::ffff:198.18.0.1]:8080"); - fail("[::ffff:224.0.0.1]:8080"); - fail("[::ffff:240.0.0.1]:8080"); - - // --- Valid public addresses --- - pass("8.8.8.8:443"); - pass("65.0.0.1:8080"); - pass("[2001:4860:4860::8888]:8080"); - pass("[2606:4700:4700::1111]:8080"); - } - - void - testVerifyEndpoints() - { - // Helper that sets up a Logic instance, creates and activates a slot, - // then calls on_endpoints with the given list and returns the - // livecache size afterwards. - auto run = [&](bool verifyEndpoints, Endpoints eps) -> std::size_t { - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - { - Config c; - c.autoConnect = false; - c.listeningPort = 1024; - c.ipLimit = 2; - c.verifyEndpoints = verifyEndpoints; - logic.config(c); - } - - auto const remote = beast::IP::Endpoint::fromString("65.0.0.1:5"); - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1024"); - - auto const [slot, r] = logic.newOutboundSlot(remote); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(r == Result::Success); - BEAST_EXPECT(logic.onConnected(slot, local)); - - PublicKey const pk(randomKeyPair(KeyType::Secp256k1).first); - BEAST_EXPECT(logic.activate(slot, pk, false) == Result::Success); - - logic.onEndpoints(slot, std::move(eps)); - - auto const size = logic.livecache.size(); - logic.onClosed(slot); - return size; - }; - - { - testcase("verify_endpoints enabled"); - - // Valid public addresses - Endpoints eps; - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); - // Invalid: private address - eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); - // Invalid: port 0 - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); - - // With verification enabled, only the 2 valid endpoints survive - BEAST_EXPECT(run(true, eps) == 2); - } - { - testcase("verify_endpoints disabled"); - - Endpoints eps; - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.1:5"), 1); - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.2:6"), 1); - // Private address — kept when verification is off - eps.emplace_back(beast::IP::Endpoint::fromString("10.0.0.1:5"), 1); - // Port 0 — kept when verification is off - eps.emplace_back(beast::IP::Endpoint::fromString("44.0.0.3:0"), 1); - - // Without verification, all 4 endpoints survive - BEAST_EXPECT(run(false, eps) == 4); - } - } - - void - testOnConnectedSelfConnection() - { - testcase("test onConnected self connection"); - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - - auto const local = beast::IP::Endpoint::fromString("65.0.0.2:1234"); - auto const [slot, r] = logic.newOutboundSlot(local); - BEAST_EXPECT(slot != nullptr); - BEAST_EXPECT(r == Result::Success); - - // Must fail when a slot is to our own IP address - BEAST_EXPECT(!logic.onConnected(slot, local)); - logic.onClosed(slot); - } - - void - testConfig() - { - // if peers_max is configured then peers_in_max and peers_out_max - // are ignored - auto run = [&](std::string const& test, - std::optional maxPeers, - std::optional maxIn, - std::optional maxOut, - std::uint16_t port, - std::uint16_t expectOut, - std::uint16_t expectIn, - std::uint16_t expectIpLimit) { - xrpl::Config c; - - testcase(test); - - std::string toLoad; - int max = 0; - if (maxPeers) - { - max = maxPeers.value(); - toLoad += "[peers_max]\n" + std::to_string(max) + "\n" + "[peers_in_max]\n" + - std::to_string(maxIn.value_or(0)) + "\n" + "[peers_out_max]\n" + - std::to_string(maxOut.value_or(0)) + "\n"; - } - else if (maxIn && maxOut) - { - toLoad += "[peers_in_max]\n" + std::to_string(*maxIn) + "\n" + "[peers_out_max]\n" + - std::to_string(*maxOut) + "\n"; - } - - c.loadFromString(toLoad); - BEAST_EXPECT( - (c.peersMax == max && c.peersInMax == 0 && c.peersOutMax == 0) || - (c.peersInMax == *maxIn && c.peersOutMax == *maxOut)); - - Config const config = Config::makeConfig(c, port, false, 0, true); - - Counts counts; - counts.onConfig(config); - BEAST_EXPECT( - counts.outMax() == expectOut && counts.inMax() == expectIn && - config.ipLimit == expectIpLimit); - - TestStore store; - TestChecker checker; - TestStopwatch clock; - Logic logic(clock, store, checker, journal_); - logic.config(config); - - BEAST_EXPECT(logic.config() == config); - }; - - // if max_peers == 0 => maxPeers = 21, - // else if max_peers < 10 => maxPeers = 10 else maxPeers = - // max_peers - // expectOut => if legacy => max(0.15 * maxPeers, 10), - // if legacy && !wantIncoming => maxPeers else max_out_peers - // expectIn => if legacy && wantIncoming => maxPeers - outPeers - // else if !wantIncoming => 0 else max_in_peers - // ipLimit => if expectIn <= 21 => 2 else 2 + min(5, expectIn/21) - // ipLimit = max(1, min(ipLimit, expectIn/2)) - - // legacy test with max_peers - run("legacy no config", {}, {}, {}, 4000, 10, 11, 2); - run("legacy max_peers 0", 0, 100, 10, 4000, 10, 11, 2); - run("legacy max_peers 5", 5, 100, 10, 4000, 10, 0, 1); - run("legacy max_peers 20", 20, 100, 10, 4000, 10, 10, 2); - run("legacy max_peers 100", 100, 100, 10, 4000, 15, 85, 6); - run("legacy max_peers 20, private", 20, 100, 10, 0, 20, 0, 1); - - // test with max_in_peers and max_out_peers - run("new in 100/out 10", {}, 100, 10, 4000, 10, 100, 6); - run("new in 0/out 10", {}, 0, 10, 4000, 10, 0, 1); - run("new in 100/out 10, private", {}, 100, 10, 0, 10, 0, 6); - } - - void - testInvalidConfig() - { - testcase("invalid config"); - - auto run = [&](std::string const& toLoad) { - xrpl::Config c; - try - { - c.loadFromString(toLoad); - fail(); - } - catch (...) - { - pass(); - } - }; - run(R"xrpldConfig( -[peers_in_max] -100 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_out_max] -100 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_in_max] -100 -[peers_out_max] -5 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_in_max] -1001 -[peers_out_max] -10 -)xrpldConfig"); - run(R"xrpldConfig( -[peers_in_max] -10 -[peers_out_max] -1001 -)xrpldConfig"); - } - - void - run() override - { - testBackoff1(); - testBackoff2(); - testDuplicateOutIn(); - testDuplicateInOut(); - testConfig(); - testInvalidConfig(); - testPeerLimitExceeded(); - testActivateDuplicatePeer(); - testActivateInboundDisabled(); - testAddFixedPeerNoPort(); - testOnConnectedSelfConnection(); - testIsValidAddress(); - testVerifyEndpoints(); - } -}; - -BEAST_DEFINE_TESTSUITE(PeerFinder, peerfinder, xrpl); - -} // namespace xrpl::PeerFinder diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index cafe72eff9..2e131b895e 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -21,7 +21,7 @@ set_target_properties( ) # Lets test sources include the shared helpers as . target_include_directories(xrpl_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(xrpl_tests PRIVATE GTest::gtest xrpl.libxrpl) +target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl) # One source subdirectory per module. Network unit tests are currently not # supported on Windows. @@ -29,6 +29,7 @@ set(test_modules basics crypto json + peerfinder resource shamap tx diff --git a/src/tests/libxrpl/main.cpp b/src/tests/libxrpl/main.cpp index 5142bbe08a..f9114bffc4 100644 --- a/src/tests/libxrpl/main.cpp +++ b/src/tests/libxrpl/main.cpp @@ -1,8 +1,9 @@ +#include #include int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); + ::testing::InitGoogleMock(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/src/tests/libxrpl/peerfinder/Livecache.cpp b/src/tests/libxrpl/peerfinder/Livecache.cpp new file mode 100644 index 0000000000..464ec0e5da --- /dev/null +++ b/src/tests/libxrpl/peerfinder/Livecache.cpp @@ -0,0 +1,294 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { +namespace { + +class LivecacheTest : public ::testing::Test +{ +protected: + static beast::Journal + journal() + { + return beast::Journal{TestSink::instance()}; + } + + static beast::IP::Endpoint + endpoint(std::uint16_t index, bool v4 = true) + { + auto const port = static_cast(10000 + index); + + if (v4) + { + auto bytes = beast::IP::AddressV4::bytes_type{ + {54, + static_cast((index / 256) % 256), + static_cast(index % 256), + 1}}; + return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV4{bytes}}, port}; + } + + auto bytes = beast::IP::AddressV6::bytes_type{ + {0x20, + 0x01, + 0x0d, + 0xb8, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + static_cast((index / 256) % 256), + static_cast(index % 256), + 1}}; + return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV6{bytes}}, port}; + } + + void + addEndpoint(beast::IP::Endpoint const& ep, std::uint32_t hops = 0) + { + cache_.insert(Endpoint{ep, hops}); + } + + TestStopwatch clock_; + Livecache<> cache_{clock_, journal()}; +}; + +} // namespace + +TEST_F(LivecacheTest, basic_insert) +{ + EXPECT_TRUE(cache_.empty()); + + for (auto i = 0; i < 10; ++i) + addEndpoint(endpoint(i, true)); + + EXPECT_FALSE(cache_.empty()); + EXPECT_EQ(cache_.size(), 10u); + + for (auto i = 10; i < 20; ++i) + addEndpoint(endpoint(i, false)); + + EXPECT_FALSE(cache_.empty()); + EXPECT_EQ(cache_.size(), 20u); +} + +TEST_F(LivecacheTest, insert_update_keeps_lowest_hop_count) +{ + auto const ep1 = Endpoint{endpoint(1), 2}; + cache_.insert(ep1); + ASSERT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); + + auto const ep2 = Endpoint{ep1.address, 4}; + cache_.insert(ep2); + EXPECT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); + + auto const ep3 = Endpoint{ep1.address, 2}; + cache_.insert(ep3); + EXPECT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u); + + auto const ep4 = Endpoint{ep1.address, 1}; + cache_.insert(ep4); + EXPECT_EQ(cache_.size(), 1u); + EXPECT_EQ((cache_.hops.begin() + 1)->begin()->hops, 1u); +} + +TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back) +{ + auto const ep1 = Endpoint{endpoint(1), 1}; + auto const ep2 = Endpoint{endpoint(2), 1}; + cache_.insert(ep1); + cache_.insert(ep2); + + auto hop = *(cache_.hops.begin() + 1); + ASSERT_NE(hop.begin(), hop.end()); + ASSERT_NE(hop.cbegin(), hop.cend()); + ASSERT_NE(hop.rbegin(), hop.rend()); + ASSERT_NE(hop.crbegin(), hop.crend()); + + auto const firstAddress = hop.begin()->address; + hop.moveBack(hop.begin()); + EXPECT_EQ(hop.rbegin()->address, firstAddress); + + auto const& constHops = cache_.hops; + EXPECT_NE(constHops.begin(), constHops.end()); + EXPECT_NE(constHops.cbegin(), constHops.cend()); + EXPECT_NE(constHops.rbegin(), constHops.rend()); + EXPECT_NE(constHops.crbegin(), constHops.crend()); + + auto const constHop = *(constHops.cbegin() + 1); + EXPECT_EQ(std::distance(constHop.begin(), constHop.end()), 2); + EXPECT_EQ(std::distance(constHop.cbegin(), constHop.cend()), 2); + EXPECT_EQ(std::distance(constHop.rbegin(), constHop.rend()), 2); + EXPECT_EQ(std::distance(constHop.crbegin(), constHop.crend()), 2); +} + +TEST_F(LivecacheTest, on_write_reports_entries_and_expiration) +{ + cache_.insert(Endpoint{endpoint(1), 1}); + cache_.insert(Endpoint{endpoint(2), Tuning::kMaxHops + 1}); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + cache_.onWrite(map); + } + + auto const& top = stream.top(); + EXPECT_EQ(top["size"].asUInt(), 2u); + EXPECT_FALSE(top["hist"].asString().empty()); + ASSERT_TRUE(top.isMember("entries")); + ASSERT_EQ(top["entries"].size(), 2u); + auto const& entry = top["entries"][json::UInt{0}]; + EXPECT_TRUE(entry.isMember("hops")); + EXPECT_TRUE(entry.isMember("address")); + EXPECT_TRUE(entry.isMember("expires")); +} + +TEST_F(LivecacheTest, expire_removes_entries_after_ttl) +{ + using namespace std::chrono_literals; + + cache_.insert(Endpoint{endpoint(1), 1}); + ASSERT_EQ(cache_.size(), 1u); + + cache_.expire(); + EXPECT_EQ(cache_.size(), 1u); + + clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); + cache_.expire(); + EXPECT_EQ(cache_.size(), 1u); + + clock_.advance(1s); + cache_.expire(); + EXPECT_TRUE(cache_.empty()); +} + +TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl) +{ + using namespace std::chrono_literals; + + cache_.insert(Endpoint{endpoint(1), 1}); + cache_.insert(Endpoint{endpoint(2), 2}); + + clock_.advance(Tuning::kLiveCacheSecondsToLive); + cache_.expire(); + EXPECT_TRUE(cache_.empty()); +} + +TEST_F(LivecacheTest, histogram_counts_all_entries) +{ + constexpr auto kNumEndpoints = 40; + + for (auto i = 0; i < kNumEndpoints; ++i) + { + addEndpoint(endpoint(static_cast(i)), xrpl::randInt()); + } + + auto const histogram = cache_.hops.histogram(); + ASSERT_FALSE(histogram.empty()); + + std::vector values; + boost::split(values, histogram, boost::algorithm::is_any_of(",")); + + auto sum = 0; + for (auto const& value : values) + { + auto const count = boost::lexical_cast(boost::trim_copy(value)); + sum += count; + EXPECT_GE(count, 0); + } + EXPECT_EQ(sum, kNumEndpoints); +} + +TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) +{ + for (auto i = 0; i < 100; ++i) + { + addEndpoint(endpoint(static_cast(i)), xrpl::randInt(Tuning::kMaxHops + 1)); + } + + using AtHop = std::vector; + using AllHops = std::array; + + auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { + return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address); + }; + auto const sameEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { + return lhs.hops == rhs.hops && lhs.address == rhs.address; + }; + auto const sameEndpoints = + [&sameEndpoint](std::vector const& lhs, std::vector const& rhs) { + return lhs.size() == rhs.size() && + std::equal(lhs.begin(), lhs.end(), rhs.begin(), sameEndpoint); + }; + + AllHops before; + AllHops beforeSorted; + for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end(); + ++i.first, ++i.second) + { + std::ranges::copy(*i.second, std::back_inserter(before[i.first])); + std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first])); + std::ranges::sort(beforeSorted[i.first], compareEndpoint); + } + + cache_.hops.shuffle(); + + AllHops after; + AllHops afterSorted; + for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end(); + ++i.first, ++i.second) + { + std::ranges::copy(*i.second, std::back_inserter(after[i.first])); + std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first])); + std::ranges::sort(afterSorted[i.first], compareEndpoint); + } + + auto allBucketsKeptOriginalOrder = true; + for (auto i = 0u; i < before.size(); ++i) + { + EXPECT_EQ(before[i].size(), after[i].size()); + allBucketsKeptOriginalOrder = + allBucketsKeptOriginalOrder && sameEndpoints(before[i], after[i]); + EXPECT_TRUE(sameEndpoints(beforeSorted[i], afterSorted[i])); + } + EXPECT_FALSE(allBucketsKeptOriginalOrder); +} + +} // namespace xrpl::PeerFinder diff --git a/src/tests/libxrpl/peerfinder/PeerFinder.cpp b/src/tests/libxrpl/peerfinder/PeerFinder.cpp new file mode 100644 index 0000000000..31fa59d1ce --- /dev/null +++ b/src/tests/libxrpl/peerfinder/PeerFinder.cpp @@ -0,0 +1,1270 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::PeerFinder { +namespace { + +using ::testing::_; +using ::testing::NiceMock; +using ::testing::Return; + +beast::Journal +journal() +{ + return beast::Journal{TestSink::instance()}; +} + +beast::IP::Endpoint +endpoint(std::string const& value) +{ + return beast::IP::Endpoint::fromString(value); +} + +class MockStore : public Store +{ +public: + MOCK_METHOD(std::size_t, load, (Store::load_callback const& cb), (override)); + MOCK_METHOD(void, save, (std::vector const& entries), (override)); +}; + +class CapturingStore : public Store +{ +public: + std::vector entriesToLoad; + std::vector> saves; + + std::size_t + load(Store::load_callback const& cb) override + { + for (auto const& entry : entriesToLoad) + cb(entry.endpoint, entry.valence); + return entriesToLoad.size(); + } + + void + save(std::vector const& entries) override + { + saves.push_back(entries); + } +}; + +Store::Entry +storeEntry(beast::IP::Endpoint const& endpoint, int valence) +{ + Store::Entry entry; + entry.endpoint = endpoint; + entry.valence = valence; + return entry; +} + +void +allowEmptyStore(MockStore& store) +{ + ON_CALL(store, load(_)).WillByDefault(Return(0)); + ON_CALL(store, save(_)).WillByDefault([](std::vector const&) {}); +} + +class MockChecker +{ +public: + MOCK_METHOD(void, stop, ()); + MOCK_METHOD(void, wait, ()); + MOCK_METHOD(void, recordAsyncConnect, (beast::IP::Endpoint const& ep)); + + boost::system::error_code nextError; + bool completeAsync = true; + std::vector asyncConnects; + + template + void + asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) + { + asyncConnects.push_back(ep); + recordAsyncConnect(ep); + if (completeAsync) + std::forward(handler)(nextError); + } +}; + +class TestSource : public Source +{ +public: + explicit TestSource(std::string name) : name_(std::move(name)) + { + } + + std::string const& + name() override + { + return name_; + } + + void + cancel() override + { + ++cancelCount; + } + + void + fetch(Results& results, beast::Journal) override + { + ++fetchCount; + results = resultsToFetch; + } + + Results resultsToFetch; + int fetchCount = 0; + int cancelCount = 0; + +private: + std::string name_; +}; + +class DefaultCancelSource : public Source +{ +public: + std::string const& + name() override + { + return name_; + } + + void + fetch(Results& results, beast::Journal) override + { + results = resultsToFetch; + } + + Results resultsToFetch; + +private: + std::string name_{"default"}; +}; + +class PeerFinderTest : public ::testing::Test +{ +public: + PeerFinderTest() + { + allowEmptyStore(store_); + } + +protected: + void + configure(std::size_t ipLimit = 2) + { + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = static_cast(ipLimit); + logic_.config(config); + } + + NiceMock store_; + NiceMock checker_; + TestStopwatch clock_; + Logic> logic_{clock_, store_, checker_, journal()}; +}; + +int +savedValence(std::vector const& entries, beast::IP::Endpoint const& endpoint) +{ + for (auto const& entry : entries) + { + if (entry.endpoint == endpoint) + return entry.valence; + } + + ADD_FAILURE() << "missing saved endpoint " << endpoint.toString(); + return 0; +} + +TEST_F(PeerFinderTest, backoff_limits_repeated_connection_attempts) +{ + auto constexpr kSECONDS = 10000; + + logic_.addFixedPeer("test", endpoint("65.0.0.1:5")); + configure(); + + std::size_t attempts = 0; + for (std::size_t i = 0; i < kSECONDS; ++i) + { + auto const list = logic_.autoconnect(); + if (!list.empty()) + { + ASSERT_EQ(list.size(), 1u); + auto const [slot, result] = logic_.newOutboundSlot(list.front()); + ASSERT_NE(slot, nullptr); + ASSERT_EQ(result, Result::Success); + EXPECT_TRUE(logic_.onConnected(slot, endpoint("65.0.0.2:5"))); + logic_.onClosed(slot); + ++attempts; + } + clock_.advance(std::chrono::seconds(1)); + logic_.oncePerSecond(); + } + + EXPECT_LT(attempts, 20u); +} + +TEST_F(PeerFinderTest, activated_peer_backoff_allows_at_most_one_attempt_per_minute) +{ + auto constexpr kSECONDS = 10000; + + logic_.addFixedPeer("test", endpoint("65.0.0.1:5")); + configure(); + + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + + std::size_t attempts = 0; + for (std::size_t i = 0; i < kSECONDS; ++i) + { + auto const list = logic_.autoconnect(); + if (!list.empty()) + { + ASSERT_EQ(list.size(), 1u); + auto const [slot, result] = logic_.newOutboundSlot(list.front()); + ASSERT_NE(slot, nullptr); + ASSERT_EQ(result, Result::Success); + ASSERT_TRUE(logic_.onConnected(slot, endpoint("65.0.0.2:5"))); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + logic_.onClosed(slot); + ++attempts; + } + clock_.advance(std::chrono::seconds(1)); + logic_.oncePerSecond(); + } + + EXPECT_LE(attempts, (kSECONDS + 59u) / 60u); +} + +TEST_F(PeerFinderTest, duplicate_inbound_slot_is_rejected_for_existing_outbound_slot) +{ + configure(); + + auto const remote = endpoint("65.0.0.1:5"); + auto const [slot1, result1] = logic_.newOutboundSlot(remote); + ASSERT_NE(slot1, nullptr); + EXPECT_EQ(result1, Result::Success); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + + auto const local = endpoint("65.0.0.2:1024"); + auto const [slot2, result2] = logic_.newInboundSlot(local, remote); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + EXPECT_EQ(result2, Result::DuplicatePeer); + EXPECT_EQ(slot2, nullptr); + + if (slot2) + logic_.onClosed(slot2); + logic_.onClosed(slot1); +} + +TEST_F(PeerFinderTest, duplicate_outbound_slot_is_rejected_for_existing_inbound_slot) +{ + configure(); + + auto const remote = endpoint("65.0.0.1:5"); + auto const local = endpoint("65.0.0.2:1024"); + + auto const [slot1, result1] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot1, nullptr); + EXPECT_EQ(result1, Result::Success); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + + auto const [slot2, result2] = logic_.newOutboundSlot(remote); + EXPECT_EQ(result2, Result::DuplicatePeer); + EXPECT_EQ(logic_.connectedAddresses.count(remote.address()), 1u); + EXPECT_EQ(slot2, nullptr); + + if (slot2) + logic_.onClosed(slot2); + logic_.onClosed(slot1); +} + +TEST_F(PeerFinderTest, peer_limit_exceeded_rejects_additional_inbound_slot) +{ + configure(); + + auto const local = endpoint("65.0.0.2:1024"); + auto const [slot, result] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1025")); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + auto const [slot1, result1] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1026")); + ASSERT_NE(slot1, nullptr); + EXPECT_EQ(result1, Result::Success); + + auto const [slot2, result2] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1027")); + EXPECT_EQ(result2, Result::IpLimitExceeded); + EXPECT_EQ(slot2, nullptr); + + if (slot2) + logic_.onClosed(slot2); + logic_.onClosed(slot1); + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, activate_rejects_duplicate_public_key) +{ + configure(); + + auto const local = endpoint("65.0.0.2:1024"); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + + auto const [slot, result] = logic_.newOutboundSlot(endpoint("55.104.0.2:1025")); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + auto const [slot2, result2] = logic_.newOutboundSlot(endpoint("55.104.0.2:1026")); + ASSERT_NE(slot2, nullptr); + EXPECT_EQ(result2, Result::Success); + + EXPECT_TRUE(logic_.onConnected(slot, local)); + EXPECT_TRUE(logic_.onConnected(slot2, local)); + + EXPECT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + EXPECT_EQ(logic_.activate(slot2, publicKey, false), Result::DuplicatePeer); + + logic_.onClosed(slot); + + EXPECT_EQ(logic_.activate(slot2, publicKey, false), Result::Success); + logic_.onClosed(slot2); +} + +TEST_F(PeerFinderTest, activate_rejects_inbound_when_inbound_connections_are_disabled) +{ + configure(); + + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + auto const local = endpoint("65.0.0.2:1024"); + + auto const [slot, result] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1025")); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + EXPECT_EQ(logic_.activate(slot, publicKey, false), Result::InboundDisabled); + + { + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + } + + EXPECT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + auto const [slot2, result2] = logic_.newInboundSlot(local, endpoint("55.104.0.2:1026")); + ASSERT_NE(slot2, nullptr); + EXPECT_EQ(result2, Result::Success); + + PublicKey const publicKey2(randomKeyPair(KeyType::Secp256k1).first); + EXPECT_EQ(logic_.activate(slot2, publicKey2, false), Result::Full); + + logic_.onClosed(slot2); + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, add_fixed_peer_rejects_endpoint_without_port) +{ + EXPECT_THROW(logic_.addFixedPeer("test", endpoint("65.0.0.2")), std::runtime_error); +} + +TEST_F(PeerFinderTest, on_connected_rejects_self_connection) +{ + auto const local = endpoint("65.0.0.2:1234"); + auto const [slot, result] = logic_.newOutboundSlot(local); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + + EXPECT_FALSE(logic_.onConnected(slot, local)); + logic_.onClosed(slot); +} + +TEST(PeerFinderResult, converts_all_result_values_to_strings) +{ + EXPECT_EQ(to_string(Result::InboundDisabled), "inbound disabled"); + EXPECT_EQ(to_string(Result::DuplicatePeer), "peer already connected"); + EXPECT_EQ(to_string(Result::IpLimitExceeded), "ip limit exceeded"); + EXPECT_EQ(to_string(Result::Full), "slots full"); + EXPECT_EQ(to_string(Result::Success), "success"); + EXPECT_EQ(to_string(static_cast(-1)), "unknown"); +} + +TEST(PeerFinderEndpoint, orders_by_address) +{ + Endpoint const high{endpoint("65.0.0.2:10002"), 1}; + Endpoint const low{endpoint("65.0.0.1:10001"), 2}; + std::vector endpoints{high, low}; + + std::ranges::sort( + endpoints, [](Endpoint const& lhs, Endpoint const& rhs) { return lhs < rhs; }); + + EXPECT_EQ(endpoints.front().address, low.address); + EXPECT_EQ(endpoints.back().address, high.address); +} + +TEST(PeerFinderCounts, tracks_slot_states_and_capacity) +{ + TestStopwatch clock; + Counts counts; + Config config; + config.outPeers = 1; + config.inPeers = 1; + config.wantIncoming = true; + counts.onConfig(config); + + EXPECT_EQ(counts.outMax(), 1); + EXPECT_EQ(counts.inMax(), 1); + EXPECT_EQ(counts.inboundSlotsFree(), 1); + EXPECT_EQ(counts.outboundSlotsFree(), 1); + EXPECT_EQ(counts.totalActive(), 0); + EXPECT_FALSE(counts.isConnectedToNetwork()); + EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(counts.stateString(), "0/1 out, 0/1 in, 0 connecting, 0 closing"); + + SlotImp inbound(endpoint("65.0.0.1:10001"), endpoint("65.0.0.2:10002"), false, clock); + counts.add(inbound); + EXPECT_EQ(counts.acceptCount(), 1); + EXPECT_TRUE(counts.canActivate(inbound)); + counts.remove(inbound); + EXPECT_EQ(counts.acceptCount(), 0); + + inbound.activate(clock.now()); + counts.add(inbound); + EXPECT_EQ(counts.inboundActive(), 1); + EXPECT_EQ(counts.totalActive(), 1); + EXPECT_EQ(counts.inboundSlotsFree(), 0); + + SlotImp const extraInbound( + endpoint("65.0.0.3:10003"), endpoint("65.0.0.4:10004"), false, clock); + EXPECT_FALSE(counts.canActivate(extraInbound)); + counts.remove(inbound); + + SlotImp outbound(endpoint("65.0.0.5:10005"), false, clock); + counts.add(outbound); + EXPECT_EQ(counts.attempts(), 1); + EXPECT_EQ(counts.connectCount(), 1); + EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts - 1); + counts.remove(outbound); + + outbound.state(Slot::State::Connected); + EXPECT_TRUE(counts.canActivate(outbound)); + outbound.activate(clock.now()); + counts.add(outbound); + EXPECT_EQ(counts.outActive(), 1); + EXPECT_EQ(counts.outboundSlotsFree(), 0); + + SlotImp extraOutbound(endpoint("65.0.0.6:10006"), false, clock); + extraOutbound.state(Slot::State::Connected); + EXPECT_FALSE(counts.canActivate(extraOutbound)); + + SlotImp fixedOutbound(endpoint("65.0.0.7:10007"), true, clock); + fixedOutbound.state(Slot::State::Connected); + EXPECT_TRUE(counts.canActivate(fixedOutbound)); + fixedOutbound.activate(clock.now()); + counts.add(fixedOutbound); + EXPECT_EQ(counts.fixed(), 1u); + EXPECT_EQ(counts.fixedActive(), 1u); + counts.remove(fixedOutbound); + + SlotImp reservedOutbound(endpoint("65.0.0.8:10008"), false, clock); + reservedOutbound.reserved(true); + reservedOutbound.state(Slot::State::Connected); + EXPECT_TRUE(counts.canActivate(reservedOutbound)); + reservedOutbound.activate(clock.now()); + counts.add(reservedOutbound); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + counts.onWrite(map); + } + EXPECT_TRUE(stream.top().isMember("accept")); + EXPECT_TRUE(stream.top().isMember("connect")); + EXPECT_TRUE(stream.top().isMember("close")); + EXPECT_TRUE(stream.top().isMember("reserved")); + EXPECT_TRUE(stream.top().isMember("total")); + counts.remove(reservedOutbound); + counts.remove(outbound); + + SlotImp closing(endpoint("65.0.0.9:10009"), endpoint("65.0.0.10:10010"), false, clock); + closing.state(Slot::State::Closing); + counts.add(closing); + EXPECT_EQ(counts.closingCount(), 1); + counts.remove(closing); + + Counts saturatedAttempts; + saturatedAttempts.onConfig(config); + std::vector> attempts; + for (int i = 0; i < Tuning::kMaxConnectAttempts; ++i) + { + attempts.push_back( + std::make_unique( + endpoint("65.1.0." + std::to_string(i + 1) + ":" + std::to_string(11000 + i)), + false, + clock)); + saturatedAttempts.add(*attempts.back()); + } + EXPECT_EQ(saturatedAttempts.attempts(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(saturatedAttempts.attemptsNeeded(), 0u); + + Config disconnected; + disconnected.outPeers = 0; + counts.onConfig(disconnected); + EXPECT_TRUE(counts.isConnectedToNetwork()); +} + +TEST(PeerFinderHandouts, filters_redirect_slot_and_connect_targets) +{ + TestStopwatch clock; + auto const remote = endpoint("65.0.0.2:10002"); + auto const slot = std::make_shared(endpoint("65.0.0.1:10001"), remote, false, clock); + + RedirectHandouts redirects(slot); + EXPECT_EQ(redirects.slot(), slot); + EXPECT_TRUE(redirects.list().empty()); + EXPECT_FALSE(redirects.full()); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 0})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{remote.atPort(12000), 1})); + EXPECT_TRUE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 1})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:12000"), 1})); + EXPECT_EQ(redirects.list().size(), 1u); + + SlotHandouts slotHandouts(slot); + EXPECT_EQ(slotHandouts.slot(), slot); + EXPECT_FALSE(slotHandouts.full()); + EXPECT_FALSE( + slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.4:10004"), Tuning::kMaxHops + 1})); + EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{remote.atPort(12001), 1})); + + auto const recent = endpoint("65.0.0.5:10005"); + slot->recent.insert(recent, 2); + EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{recent, 2})); + EXPECT_TRUE(slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.6:10006"), 2})); + EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.6:12000"), 2})); + slotHandouts.insert(Endpoint{endpoint("65.0.0.7:10007"), 1}); + EXPECT_EQ(slotHandouts.list().size(), 2u); + + ConnectHandouts::Squelches squelches(clock); + ConnectHandouts connects(2, squelches); + EXPECT_TRUE(connects.empty()); + EXPECT_TRUE(connects.tryInsert(endpoint("65.0.0.8:10008"))); + EXPECT_FALSE(connects.empty()); + EXPECT_FALSE(connects.tryInsert(endpoint("65.0.0.8:12000"))); + EXPECT_TRUE(connects.tryInsert(Endpoint{endpoint("65.0.0.9:10009"), 1})); + EXPECT_TRUE(connects.full()); + EXPECT_FALSE(connects.tryInsert(endpoint("65.0.0.10:10010"))); + EXPECT_EQ(connects.list().size(), 2u); + + ConnectHandouts squelched(1, squelches); + EXPECT_FALSE(squelched.tryInsert(endpoint("65.0.0.9:12000"))); +} + +TEST(PeerFinderHandouts, distributes_livecache_entries) +{ + TestStopwatch clock; + Livecache<> cache(clock, journal()); + cache.insert(Endpoint{endpoint("65.0.0.10:10010"), 1}); + cache.insert(Endpoint{endpoint("65.0.0.11:10011"), 2}); + + auto const slot1 = std::make_shared( + endpoint("65.0.0.1:10001"), endpoint("65.0.0.2:10002"), false, clock); + auto const slot2 = std::make_shared( + endpoint("65.0.0.3:10003"), endpoint("65.0.0.4:10004"), false, clock); + std::vector targets; + targets.emplace_back(slot1); + targets.emplace_back(slot2); + + handout(targets.begin(), targets.end(), cache.hops.begin(), cache.hops.end()); + + EXPECT_FALSE(targets.front().list().empty()); + EXPECT_FALSE(targets.back().list().empty()); + + for (std::uint32_t i = 0; i < Tuning::kNumberOfEndpoints; ++i) + targets.front().insert(Endpoint{endpoint("65.1.0." + std::to_string(i + 1) + ":12000"), 1}); + + handout(targets.begin(), targets.begin() + 1, cache.hops.begin(), cache.hops.end()); + EXPECT_TRUE(targets.front().full()); +} + +TEST_F(PeerFinderTest, preprocess_filters_invalid_duplicate_and_extra_self_endpoints) +{ + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("65.0.0.2:10002"); + auto const slot = std::make_shared(local, remote, false, clock_); + Endpoints endpoints{ + Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1}, + Endpoint{endpoint("0.0.0.0:2459"), 0}, + Endpoint{endpoint("0.0.0.0:2460"), 0}, + Endpoint{endpoint("10.0.0.1:10004"), 1}, + Endpoint{endpoint("65.0.0.5"), 1}, + Endpoint{endpoint("65.0.0.6:10006"), 1}, + Endpoint{endpoint("65.0.0.6:10006"), 2}}; + + logic_.preprocess(slot, endpoints); + + ASSERT_EQ(endpoints.size(), 2u); + EXPECT_EQ(endpoints.front().address, remote.atPort(2459)); + EXPECT_EQ(endpoints.front().hops, 1u); + EXPECT_EQ(endpoints.back().address, endpoint("65.0.0.6:10006")); + EXPECT_EQ(endpoints.back().hops, 2u); +} + +TEST_F(PeerFinderTest, on_endpoints_checks_neighbor_before_caching_it) +{ + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("55.104.0.2:1025"); + auto const [slot, result] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + Endpoints const advertised{Endpoint{endpoint("0.0.0.0:2459"), 0}}; + logic_.onEndpoints(slot, advertised); + + ASSERT_EQ(checker_.asyncConnects.size(), 1u); + EXPECT_EQ(checker_.asyncConnects.front(), remote.atPort(2459)); + EXPECT_EQ(slot->listeningPort(), std::optional{2459}); + EXPECT_TRUE(slot->checked); + EXPECT_TRUE(slot->canAccept); + EXPECT_TRUE(logic_.livecache.empty()); + + clock_.advance(Tuning::kSecondsPerMessage); + logic_.onEndpoints(slot, advertised); + EXPECT_EQ(logic_.livecache.size(), 1u); + EXPECT_EQ(logic_.bootcache.size(), 1u); + + logic_.onEndpoints(slot, Endpoints{Endpoint{endpoint("65.0.0.9:10009"), 1}}); + EXPECT_EQ(logic_.livecache.size(), 1u); + + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, on_endpoints_skips_failed_neighbor_connectivity_checks) +{ + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + + checker_.nextError = boost::asio::error::host_unreachable; + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("55.104.0.3:1025"); + auto const [slot, result] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + Endpoints const advertised{Endpoint{endpoint("0.0.0.0:2459"), 0}}; + logic_.onEndpoints(slot, advertised); + EXPECT_TRUE(slot->checked); + EXPECT_FALSE(slot->canAccept); + + clock_.advance(Tuning::kSecondsPerMessage); + logic_.onEndpoints(slot, advertised); + EXPECT_TRUE(logic_.livecache.empty()); + + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, on_endpoints_waits_for_pending_connectivity_check) +{ + Config config; + config.autoConnect = false; + config.listeningPort = 1024; + config.ipLimit = 2; + config.inPeers = 1; + logic_.config(config); + + checker_.completeAsync = false; + auto const local = endpoint("65.0.0.1:10001"); + auto const remote = endpoint("55.104.0.4:1025"); + auto const [slot, result] = logic_.newInboundSlot(local, remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + Endpoints const advertised{Endpoint{endpoint("0.0.0.0:2459"), 0}}; + logic_.onEndpoints(slot, advertised); + EXPECT_TRUE(slot->connectivityCheckInProgress); + + clock_.advance(Tuning::kSecondsPerMessage); + logic_.onEndpoints(slot, advertised); + EXPECT_EQ(checker_.asyncConnects.size(), 1u); + EXPECT_TRUE(logic_.livecache.empty()); + + checker_.completeAsync = true; + logic_.checkComplete(remote, remote.atPort(2459), boost::asio::error::operation_aborted); + slot->connectivityCheckInProgress = false; + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, builds_endpoint_messages_and_redirects_from_livecache) +{ + Config config; + config.autoConnect = false; + config.wantIncoming = true; + config.listeningPort = 2459; + config.inPeers = 2; + config.outPeers = 2; + config.ipLimit = 2; + logic_.config(config); + + auto const remote = endpoint("55.104.0.5:1025"); + auto const live = endpoint("65.0.0.10:10010"); + logic_.livecache.insert(Endpoint{live, 1}); + + auto const [slot, result] = logic_.newOutboundSlot(remote); + ASSERT_NE(slot, nullptr); + EXPECT_EQ(result, Result::Success); + ASSERT_TRUE(logic_.onConnected(slot, endpoint("65.0.0.1:10001"))); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + ASSERT_EQ(logic_.activate(slot, publicKey, false), Result::Success); + + auto const messages = logic_.buildEndpointsForPeers(); + ASSERT_EQ(messages.size(), 1u); + auto const& sent = messages.front().second; + EXPECT_TRUE(std::ranges::any_of(sent, [](Endpoint const& ep) { return ep.hops == 0; })); + EXPECT_TRUE( + std::ranges::any_of(sent, [&live](Endpoint const& ep) { return ep.address == live; })); + EXPECT_TRUE(logic_.buildEndpointsForPeers().empty()); + + auto const redirects = logic_.redirect(slot); + EXPECT_FALSE(redirects.empty()); + + logic_.onClosed(slot); +} + +TEST_F(PeerFinderTest, autoconnect_uses_livecache_then_bootcache) +{ + Config config; + config.autoConnect = true; + config.wantIncoming = false; + config.outPeers = 1; + config.inPeers = 0; + config.ipLimit = 1; + logic_.config(config); + + auto const live = endpoint("65.0.0.11:10011"); + logic_.livecache.insert(Endpoint{live, 1}); + auto const liveAddresses = logic_.autoconnect(); + ASSERT_EQ(liveAddresses.size(), 1u); + EXPECT_EQ(liveAddresses.front(), live); + + auto const boot = endpoint("65.0.0.12:10012"); + EXPECT_TRUE(logic_.bootcache.insertStatic(boot)); + auto const bootAddresses = logic_.autoconnect(); + ASSERT_EQ(bootAddresses.size(), 1u); + EXPECT_EQ(bootAddresses.front(), boot); +} + +TEST_F(PeerFinderTest, sources_redirects_status_and_validation_paths_are_exercised) +{ + auto const source = std::make_shared("static"); + source->resultsToFetch.addresses = {endpoint("65.0.0.13:10013")}; + logic_.addStaticSource(source); + EXPECT_EQ(source->fetchCount, 1); + EXPECT_EQ(logic_.bootcache.size(), 1u); + + auto const failing = std::make_shared("failing"); + failing->resultsToFetch.error = boost::asio::error::host_unreachable; + logic_.fetch(failing); + EXPECT_EQ(failing->fetchCount, 1); + + auto const dynamic = std::make_shared("dynamic"); + logic_.addSource(dynamic); + ASSERT_EQ(logic_.sources.size(), 1u); + EXPECT_EQ(logic_.sources.front(), dynamic); + + std::vector redirects{ + {boost::asio::ip::make_address("65.0.0.14"), 10014}, + {boost::asio::ip::make_address("65.0.0.15"), 10015}}; + logic_.onRedirects(redirects.begin(), redirects.end(), redirects.front()); + EXPECT_EQ(logic_.bootcache.size(), 3u); + + EXPECT_FALSE(logic_.isValidAddress(endpoint("0.0.0.0:10016"))); + EXPECT_FALSE(logic_.isValidAddress(endpoint("10.0.0.1:10017"))); + EXPECT_FALSE(logic_.isValidAddress(endpoint("65.0.0.16"))); + EXPECT_TRUE(logic_.isValidAddress(endpoint("65.0.0.16:10016"))); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + logic_.onWrite(map); + } + EXPECT_TRUE(stream.top().isMember("peers")); + EXPECT_TRUE(stream.top().isMember("counts")); + EXPECT_TRUE(stream.top().isMember("config")); + EXPECT_TRUE(stream.top().isMember("livecache")); + EXPECT_TRUE(stream.top().isMember("bootcache")); + + DefaultCancelSource defaultCancel; + Source::Results results; + EXPECT_TRUE(results.addresses.empty()); + defaultCancel.cancel(); + defaultCancel.fetch(results, journal()); + + logic_.fetchSource = dynamic; + logic_.stop(); + EXPECT_TRUE(logic_.stopping); + EXPECT_EQ(dynamic->cancelCount, 1); + + auto const ignored = std::make_shared("ignored"); + logic_.fetch(ignored); + EXPECT_EQ(ignored->fetchCount, 0); + + logic_.checkComplete( + endpoint("65.0.0.18:10018"), endpoint("65.0.0.19:10019"), boost::system::error_code{}); +} + +TEST(PeerFinderBootcache, loads_unique_entries_and_clears_cache) +{ + CapturingStore store; + TestStopwatch clock; + auto const ep1 = endpoint("65.0.0.1:10001"); + auto const ep2 = endpoint("65.0.0.2:10002"); + store.entriesToLoad = {storeEntry(ep1, 3), storeEntry(ep2, -2), storeEntry(ep1, 4)}; + + Bootcache cache(store, clock, journal()); + cache.load(); + + EXPECT_FALSE(cache.empty()); + EXPECT_EQ(cache.size(), 2u); + EXPECT_EQ(*cache.begin(), ep1); + EXPECT_EQ(*cache.cbegin(), ep1); + EXPECT_NE(cache.begin(), cache.end()); + EXPECT_NE(cache.cbegin(), cache.cend()); + + cache.clear(); + EXPECT_TRUE(cache.empty()); + EXPECT_EQ(cache.begin(), cache.end()); +} + +TEST(PeerFinderBootcache, records_connection_outcomes_and_persists_pending_updates) +{ + CapturingStore store; + TestStopwatch clock; + auto const ep1 = endpoint("65.0.0.1:10001"); + auto const ep2 = endpoint("65.0.0.2:10002"); + auto const ep3 = endpoint("65.0.0.3:10003"); + auto const ep4 = endpoint("65.0.0.4:10004"); + + { + Bootcache cache(store, clock, journal()); + + EXPECT_TRUE(cache.insert(ep1)); + EXPECT_FALSE(cache.insert(ep1)); + + cache.onSuccess(ep1); + EXPECT_TRUE(cache.insertStatic(ep1)); + EXPECT_FALSE(cache.insertStatic(ep1)); + + EXPECT_TRUE(cache.insertStatic(ep2)); + cache.onSuccess(ep3); + cache.onFailure(ep3); + cache.onFailure(ep4); + + EXPECT_EQ(cache.size(), 4u); + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + cache.onWrite(map); + } + EXPECT_TRUE(stream.top().isMember("entries")); + EXPECT_EQ(stream.top()["entries"].size(), 4u); + } + + ASSERT_EQ(store.saves.size(), 1u); + auto const& saved = store.saves.front(); + ASSERT_EQ(saved.size(), 4u); + EXPECT_EQ(savedValence(saved, ep1), Bootcache::kStaticValence); + EXPECT_EQ(savedValence(saved, ep2), Bootcache::kStaticValence); + EXPECT_EQ(savedValence(saved, ep3), -1); + EXPECT_EQ(savedValence(saved, ep4), -1); +} + +TEST(PeerFinderBootcache, periodic_activity_saves_after_cooldown) +{ + using namespace std::chrono_literals; + + CapturingStore store; + TestStopwatch clock; + + { + Bootcache cache(store, clock, journal()); + EXPECT_TRUE(cache.insert(endpoint("65.0.0.1:10001"))); + + cache.periodicActivity(); + EXPECT_TRUE(store.saves.empty()); + + clock.advance(Tuning::kBootcacheCooldownTime + 1s); + cache.periodicActivity(); + ASSERT_EQ(store.saves.size(), 1u); + + cache.periodicActivity(); + EXPECT_EQ(store.saves.size(), 1u); + } + + EXPECT_EQ(store.saves.size(), 1u); +} + +TEST(PeerFinderBootcache, prunes_when_cache_exceeds_limit) +{ + CapturingStore store; + TestStopwatch clock; + Bootcache cache(store, clock, journal()); + + for (std::uint16_t i = 0; i <= Tuning::kBootcacheSize; ++i) + { + EXPECT_TRUE(cache.insert(endpoint( + "65.0." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256) + ":" + + std::to_string(10000 + i)))); + } + + EXPECT_LE(cache.size(), Tuning::kBootcacheSize); +} + +TEST(PeerFinderEndpoint, clamps_hops_to_overflow_bucket) +{ + auto const address = endpoint("65.0.0.1:10001"); + Endpoint const ep(address, Tuning::kMaxHops + 10); + + EXPECT_EQ(ep.address, address); + EXPECT_EQ(ep.hops, Tuning::kMaxHops + 1); +} + +TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) +{ + using State = Slot::State; + using namespace std::chrono_literals; + + TestStopwatch clock; + auto const local = endpoint("65.0.0.1:10000"); + auto const remote = endpoint("65.0.0.2:10001"); + SlotImp inbound(local, remote, true, clock); + + EXPECT_TRUE(inbound.inbound()); + EXPECT_TRUE(inbound.fixed()); + EXPECT_FALSE(inbound.reserved()); + EXPECT_EQ(inbound.state(), State::Accept); + EXPECT_EQ(inbound.remoteEndpoint(), remote); + EXPECT_EQ(inbound.localEndpoint(), std::optional{local}); + EXPECT_FALSE(inbound.publicKey()); + EXPECT_FALSE(inbound.listeningPort()); + EXPECT_FALSE(inbound.checked); + EXPECT_FALSE(inbound.canAccept); + EXPECT_FALSE(inbound.connectivityCheckInProgress); + + auto const newLocal = endpoint("65.0.0.3:10002"); + auto const newRemote = endpoint("65.0.0.4:10003"); + PublicKey const publicKey(randomKeyPair(KeyType::Secp256k1).first); + + inbound.localEndpoint(newLocal); + inbound.remoteEndpoint(newRemote); + inbound.publicKey(publicKey); + inbound.reserved(true); + inbound.setListeningPort(2459); + + EXPECT_EQ(inbound.localEndpoint(), std::optional{newLocal}); + EXPECT_EQ(inbound.remoteEndpoint(), newRemote); + EXPECT_EQ(inbound.publicKey(), std::optional{publicKey}); + EXPECT_TRUE(inbound.reserved()); + EXPECT_EQ(inbound.listeningPort(), std::optional{2459}); + EXPECT_FALSE(inbound.prefix().empty()); + + inbound.state(State::Closing); + EXPECT_EQ(inbound.state(), State::Closing); + + SlotImp outbound(remote, false, clock); + EXPECT_FALSE(outbound.inbound()); + EXPECT_FALSE(outbound.fixed()); + EXPECT_EQ(outbound.state(), State::Connect); + EXPECT_TRUE(outbound.checked); + EXPECT_TRUE(outbound.canAccept); + + outbound.state(State::Connected); + outbound.activate(clock.now()); + EXPECT_EQ(outbound.state(), State::Active); + EXPECT_EQ(outbound.whenAcceptEndpoints, clock.now()); + + auto const recent = endpoint("65.0.0.5:10004"); + EXPECT_FALSE(outbound.recent.filter(recent, 2)); + + outbound.recent.insert(recent, 2); + EXPECT_TRUE(outbound.recent.filter(recent, 2)); + EXPECT_TRUE(outbound.recent.filter(recent, 3)); + EXPECT_FALSE(outbound.recent.filter(recent, 1)); + + outbound.recent.insert(recent, 4); + EXPECT_FALSE(outbound.recent.filter(recent, 1)); + + outbound.recent.insert(recent, 1); + EXPECT_TRUE(outbound.recent.filter(recent, 1)); + EXPECT_FALSE(outbound.recent.filter(recent, 0)); + + clock.advance(Tuning::kLiveCacheSecondsToLive + 1s); + outbound.expire(); + EXPECT_FALSE(outbound.recent.filter(recent, 1)); +} + +TEST(PeerFinderConfig, writes_property_stream_and_compares_verify_endpoints) +{ + Config config; + config.maxPeers = 42; + config.outPeers = 12; + config.inPeers = 30; + config.peerPrivate = false; + config.wantIncoming = true; + config.autoConnect = false; + config.listeningPort = 2459; + config.features = "feature"; + config.ipLimit = 4; + config.verifyEndpoints = false; + + JsonPropertyStream stream; + { + beast::PropertyStream::Map map(stream); + config.onWrite(map); + } + + auto const& json = stream.top(); + EXPECT_EQ(json["max_peers"].asUInt(), config.maxPeers); + EXPECT_EQ(json["out_peers"].asUInt(), config.outPeers); + EXPECT_TRUE(json.isMember("want_incoming")); + EXPECT_TRUE(json.isMember("auto_connect")); + EXPECT_EQ(json["port"].asUInt(), config.listeningPort); + EXPECT_EQ(json["features"].asString(), config.features); + EXPECT_EQ(json["ip_limit"].asInt(), config.ipLimit); + EXPECT_TRUE(json.isMember("verify_endpoints")); + + Config same = config; + EXPECT_EQ(config, same); + same.verifyEndpoints = true; + EXPECT_NE(config, same); +} + +TEST(PeerFinderConfig, validator_and_standalone_settings_disable_auto_connect) +{ + PeerLimitConfig const limits{.maxPeers = 50, .inPeers = {}, .outPeers = {}}; + + Config const config = Config::makeConfig(false, true, limits, 2459, true, 7, false); + + EXPECT_TRUE(config.peerPrivate); + EXPECT_FALSE(config.autoConnect); + EXPECT_FALSE(config.verifyEndpoints); + EXPECT_EQ(config.ipLimit, 7); +} + +TEST(PeerFinderConfig, calculates_outbound_peers_and_clamps_ip_limits) +{ + Config config; + config.maxPeers = 1; + EXPECT_EQ(config.calcOutPeers(), Tuning::kMinOutCount); + + config.maxPeers = 100; + EXPECT_EQ(config.calcOutPeers(), 15u); + + config.inPeers = 1; + config.ipLimit = 0; + config.applyTuning(); + EXPECT_EQ(config.ipLimit, 1); + + Config explicitLimit; + explicitLimit.inPeers = 8; + explicitLimit.ipLimit = 99; + explicitLimit.applyTuning(); + EXPECT_EQ(explicitLimit.ipLimit, 4); + + Config largeInbound; + largeInbound.inPeers = 200; + largeInbound.ipLimit = 0; + largeInbound.applyTuning(); + EXPECT_EQ(largeInbound.ipLimit, 7); +} + +TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits) +{ + struct ConfigCase + { + std::string name; + std::optional maxPeers; + std::optional maxIn; + std::optional maxOut; + std::uint16_t port; + std::uint16_t expectedOut; + std::uint16_t expectedIn; + std::uint16_t expectedIpLimit; + }; + + std::vector const cases{ + {.name = "legacy no config", + .maxPeers = {}, + .maxIn = {}, + .maxOut = {}, + .port = 4000, + .expectedOut = 10, + .expectedIn = 11, + .expectedIpLimit = 2}, + {.name = "legacy max_peers 0", + .maxPeers = 0, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 11, + .expectedIpLimit = 2}, + {.name = "legacy max_peers 5", + .maxPeers = 5, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 0, + .expectedIpLimit = 1}, + {.name = "legacy max_peers 20", + .maxPeers = 20, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 10, + .expectedIpLimit = 2}, + {.name = "legacy max_peers 100", + .maxPeers = 100, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 15, + .expectedIn = 85, + .expectedIpLimit = 6}, + {.name = "legacy max_peers 20, private", + .maxPeers = 20, + .maxIn = 100, + .maxOut = 10, + .port = 0, + .expectedOut = 20, + .expectedIn = 0, + .expectedIpLimit = 1}, + {.name = "new in 100/out 10", + .maxPeers = {}, + .maxIn = 100, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 100, + .expectedIpLimit = 6}, + {.name = "new in 0/out 10", + .maxPeers = {}, + .maxIn = 0, + .maxOut = 10, + .port = 4000, + .expectedOut = 10, + .expectedIn = 0, + .expectedIpLimit = 1}, + {.name = "new in 100/out 10, private", + .maxPeers = {}, + .maxIn = 100, + .maxOut = 10, + .port = 0, + .expectedOut = 10, + .expectedIn = 0, + .expectedIpLimit = 6}}; + + for (auto const& testCase : cases) + { + SCOPED_TRACE(testCase.name); + + PeerLimitConfig const limits{ + .maxPeers = testCase.maxPeers, .inPeers = testCase.maxIn, .outPeers = testCase.maxOut}; + + Config const config = + Config::makeConfig(false, false, limits, testCase.port, false, 0, true); + + Counts counts; + counts.onConfig(config); + EXPECT_EQ(counts.outMax(), testCase.expectedOut); + EXPECT_EQ(counts.inMax(), testCase.expectedIn); + EXPECT_EQ(config.ipLimit, testCase.expectedIpLimit); + + NiceMock store; + allowEmptyStore(store); + NiceMock checker; + TestStopwatch clock; + Logic> logic(clock, store, checker, journal()); + logic.config(config); + + EXPECT_EQ(logic.config(), config); + } +} + +TEST(PeerFinderConfig, rejects_incomplete_or_out_of_range_peer_limits) +{ + std::vector const configs{ + {.maxPeers = {}, .inPeers = 100, .outPeers = {}}, + {.maxPeers = {}, .inPeers = {}, .outPeers = 100}, + {.maxPeers = {}, .inPeers = 100, .outPeers = 5}, + {.maxPeers = {}, .inPeers = 1001, .outPeers = 10}, + {.maxPeers = {}, .inPeers = 10, .outPeers = 1001}}; + + for (auto const& limits : configs) + { + EXPECT_THROW( + Config::makeConfig(false, false, limits, 4000, false, 0, true), std::exception); + } +} + +} // namespace +} // namespace xrpl::PeerFinder diff --git a/src/xrpld/app/rdb/PeerFinder.h b/src/xrpld/app/rdb/PeerFinder.h index 4f186ff7e2..5d916000a3 100644 --- a/src/xrpld/app/rdb/PeerFinder.h +++ b/src/xrpld/app/rdb/PeerFinder.h @@ -1,9 +1,8 @@ #pragma once -#include - #include #include +#include #include #include diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp b/src/xrpld/app/rdb/detail/PeerFinder.cpp index abdcd1c61a..72a275c7cd 100644 --- a/src/xrpld/app/rdb/detail/PeerFinder.cpp +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp @@ -1,12 +1,11 @@ #include -#include - #include #include #include #include #include +#include #include #include // IWYU pragma: keep diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 064b4ecd3e..0f0b3242de 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -7,8 +7,6 @@ #include #include #include -#include -#include #include #include @@ -16,6 +14,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h index d7836e3c84..f9ba33571f 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.h +++ b/src/xrpld/overlay/detail/ConnectAttempt.h @@ -3,12 +3,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 6a6a6edace..f9972548d1 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -10,8 +10,6 @@ #include #include #include -#include -#include #include #include #include @@ -39,6 +37,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -179,12 +180,13 @@ OverlayImpl::OverlayImpl( , journal_(app_.getJournal("Overlay")) , serverHandler_(serverHandler) , resourceManager_(resourceManager) + , store_(app_.getJournal("PeerFinder")) , peerFinder_( PeerFinder::makeManager( ioContext, stopwatch(), app_.getJournal("PeerFinder"), - config, + store_, collector)) , resolver_(resolver) , nextId_(1) @@ -201,6 +203,7 @@ OverlayImpl::OverlayImpl( return ret; }()) { + store_.open(config); beast::PropertyStream::Source::add(peerFinder_.get()); } @@ -505,7 +508,7 @@ OverlayImpl::remove(std::shared_ptr const& slot) void OverlayImpl::start() { - PeerFinder::Config const config = PeerFinder::Config::makeConfig( + PeerFinder::Config const config = PeerFinder::makeConfig( app_.config(), serverHandler_.setup().overlay.port(), app_.getValidationPublicKey().has_value(), diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index 092ac86a6d..cd2c7d630b 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -8,8 +8,7 @@ #include #include #include -#include -#include +#include #include #include @@ -24,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -109,6 +110,7 @@ private: beast::Journal const journal_; ServerHandler& serverHandler_; Resource::Manager& resourceManager_; + PeerFinder::StoreSqdb store_; std::unique_ptr peerFinder_; TrafficCount traffic_; hash_map, std::weak_ptr> peers_; diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 8838970b5f..c720bdf30b 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -19,8 +19,6 @@ #include #include #include -#include -#include #include #include @@ -43,6 +41,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index ea6eccd656..90f8a917f4 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include @@ -24,6 +22,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index 0530343641..f96ea31943 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -1,368 +1,19 @@ #pragma once #include -#include -#include -#include -#include -#include -#include +#include -#include - -#include -#include #include -#include -#include -#include -#include -#include namespace xrpl::PeerFinder { -using clock_type = beast::AbstractClock; - -/** - * Represents a set of addresses. - */ -using IPAddresses = std::vector; - -//------------------------------------------------------------------------------ - -/** - * PeerFinder configuration settings. - */ -struct Config -{ - /** - * The largest number of public peer slots to allow. - * This includes both inbound and outbound, but does not include - * fixed peers. - */ - std::size_t maxPeers{Tuning::kDefaultMaxPeers}; - - /** - * The number of automatic outbound connections to maintain. - * Outbound connections are only maintained if autoConnect - * is `true`. - */ - std::size_t outPeers; - - /** - * The number of automatic inbound connections to maintain. - * Inbound connections are only maintained if wantIncoming - * is `true`. - */ - std::size_t inPeers{0}; - - /** - * `true` if we want our IP address kept private. - */ - bool peerPrivate = true; - - /** - * `true` if we want to accept incoming connections. - */ - bool wantIncoming{true}; - - /** - * `true` if we want to establish connections automatically - */ - bool autoConnect{true}; - - /** - * The listening port number. - */ - std::uint16_t listeningPort{0}; - - /** - * The set of features we advertise. - */ - std::string features; - - /** - * Limit how many incoming connections we allow per IP - */ - int ipLimit{0}; - - /** - * `true` if we want to verify endpoints in TMEndpoints messages - */ - bool verifyEndpoints = true; - - //-------------------------------------------------------------------------- - - /** - * Create a configuration with default values. - */ - Config(); - - /** - * Returns a suitable value for outPeers according to the rules. - */ - [[nodiscard]] std::size_t - calcOutPeers() const; - - /** - * Adjusts the values so they follow the business rules. - */ - void - applyTuning(); - - /** - * Write the configuration into a property stream - */ - void - onWrite(beast::PropertyStream::Map& map) const; - - /** - * Make PeerFinder::Config from configuration parameters - * @param config server's configuration - * @param port server's listening port - * @param validationPublicKey true if validation public key is not empty - * @param ipLimit limit of incoming connections per IP - * @param verifyEndpoints `true` if we want to verify endpoints in - * TMEndpoints messages - * @return PeerFinder::Config - */ - static Config - makeConfig( - xrpl::Config const& config, - std::uint16_t port, - bool validationPublicKey, - int ipLimit, - bool verifyEndpoints); - - friend bool - operator==(Config const& lhs, Config const& rhs) = default; -}; - -//------------------------------------------------------------------------------ - -/** - * Describes a connectable peer address along with some metadata. - */ -struct Endpoint -{ - Endpoint() = default; - - Endpoint(beast::IP::Endpoint ep, std::uint32_t hops); - - std::uint32_t hops = 0; - beast::IP::Endpoint address; -}; - -inline bool -operator<(Endpoint const& lhs, Endpoint const& rhs) -{ - return lhs.address < rhs.address; -} - -/** - * A set of Endpoint used for connecting. - */ -using Endpoints = std::vector; - -//------------------------------------------------------------------------------ - -/** - * Possible results from activating a slot. - */ -enum class Result { InboundDisabled, DuplicatePeer, IpLimitExceeded, Full, Success }; - -/** - * @brief Converts a `Result` enum value to its string representation. - * - * This function provides a human-readable string for a given `Result` enum, - * which is useful for logging, debugging, or displaying status messages. - * - * @param result The `Result` enum value to convert. - * @return A `std::string_view` representing the enum value. Returns "unknown" - * if the enum value is not explicitly handled. - * - * @note This function returns a `std::string_view` for performance. - * A `std::string` would need to allocate memory on the heap and copy the - * string literal into it every time the function is called. - */ -inline std::string_view -to_string(Result result) noexcept -{ - switch (result) - { - case Result::InboundDisabled: - return "inbound disabled"; - case Result::DuplicatePeer: - return "peer already connected"; - case Result::IpLimitExceeded: - return "ip limit exceeded"; - case Result::Full: - return "slots full"; - case Result::Success: - return "success"; - } - - return "unknown"; -} - -/** - * Maintains a set of IP addresses used for getting into the network. - */ -class Manager : public beast::PropertyStream::Source -{ -protected: - Manager() noexcept; - -public: - /** - * Destroy the object. - * Any pending source fetch operations are aborted. - * There may be some listener calls made before the - * destructor returns. - */ - ~Manager() override = default; - - /** - * Set the configuration for the manager. - * The new settings will be applied asynchronously. - * Thread safety: - * Can be called from any threads at any time. - */ - virtual void - setConfig(Config const& config) = 0; - - /** - * Transition to the started state, synchronously. - */ - virtual void - start() = 0; - - /** - * Transition to the stopped state, synchronously. - */ - virtual void - stop() = 0; - - /** - * Returns the configuration for the manager. - */ - virtual Config - config() = 0; - - /** - * Add a peer that should always be connected. - * This is useful for maintaining a private cluster of peers. - * The string is the name as specified in the configuration - * file, along with the set of corresponding IP addresses. - */ - virtual void - addFixedPeer(std::string_view name, std::vector const& addresses) = 0; - - /** - * Add a set of strings as fallback IP::Endpoint sources. - * @param name A label used for diagnostics. - */ - virtual void - addFallbackStrings(std::string const& name, std::vector const& strings) = 0; - - /** - * Add a URL as a fallback location to obtain IP::Endpoint sources. - * @param name A label used for diagnostics. - */ - /* VFALCO NOTE Unimplemented - virtual void addFallbackURL (std::string const& name, - std::string const& url) = 0; - */ - - //-------------------------------------------------------------------------- - - /** - * Create a new inbound slot with the specified remote endpoint. - * If nullptr is returned, then the slot could not be assigned. - * Usually this is because of a detected self-connection. - */ - virtual std::pair, Result> - newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) = 0; - - /** - * Create a new outbound slot with the specified remote endpoint. - * If nullptr is returned, then the slot could not be assigned. - * Usually this is because of a duplicate connection. - */ - virtual std::pair, Result> - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) = 0; - - /** - * Called when mtENDPOINTS is received. - */ - virtual void - onEndpoints(std::shared_ptr const& slot, Endpoints const& endpoints) = 0; - - /** - * Called when the slot is closed. - * This always happens when the socket is closed, unless the socket - * was canceled. - */ - virtual void - onClosed(std::shared_ptr const& slot) = 0; - - /** - * Called when an outbound connection is deemed to have failed - */ - virtual void - onFailure(std::shared_ptr const& slot) = 0; - - /** - * Called when we received redirect IPs from a busy peer. - */ - virtual void - onRedirects( - boost::asio::ip::tcp::endpoint const& remoteAddress, - std::vector const& eps) = 0; - - //-------------------------------------------------------------------------- - - /** - * Called when an outbound connection attempt succeeds. - * The local endpoint must be valid. If the caller receives an error - * when retrieving the local endpoint from the socket, it should - * proceed as if the connection attempt failed by calling on_closed - * instead of on_connected. - * @return `true` if the connection should be kept - */ - virtual bool - onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) = 0; - - /** - * Request an active slot type. - */ - virtual Result - activate(std::shared_ptr const& slot, PublicKey const& key, bool reserved) = 0; - - /** - * Returns a set of endpoints suitable for redirection. - */ - virtual std::vector - redirect(std::shared_ptr const& slot) = 0; - - /** - * Return a set of addresses we should connect to. - */ - virtual std::vector - autoconnect() = 0; - - virtual std::vector, std::vector>> - buildEndpointsForPeers() = 0; - - /** - * Perform periodic activity. - * This should be called once per second. - */ - virtual void - oncePerSecond() = 0; -}; +Config +makeConfig( + xrpl::Config const& config, + std::uint16_t port, + bool validationPublicKey, + int ipLimit, + bool verifyEndpoints); } // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index 5d276dc9c5..a893b969e0 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -1,124 +1,39 @@ #include #include -#include -#include +#include -#include -#include #include namespace xrpl::PeerFinder { -Config::Config() : outPeers(calcOutPeers()) - -{ -} - -std::size_t -Config::calcOutPeers() const -{ - return std::max( - ((maxPeers * Tuning::kOutPercent) + 50) / 100, std::size_t(Tuning::kMinOutCount)); -} - -void -Config::applyTuning() -{ - if (ipLimit == 0) - { - // Unless a limit is explicitly set, we allow between - // 2 and 5 connections from non RFC-1918 "private" - // IP addresses. - ipLimit = 2; - - if (inPeers > Tuning::kDefaultMaxPeers) - ipLimit += std::min(5, static_cast(inPeers / Tuning::kDefaultMaxPeers)); - } - - // We don't allow a single IP to consume all incoming slots, - // unless we only have one incoming slot available. - ipLimit = std::max(1, std::min(ipLimit, static_cast(inPeers / 2))); -} - -void -Config::onWrite(beast::PropertyStream::Map& map) const -{ - map["max_peers"] = maxPeers; - map["out_peers"] = outPeers; - map["want_incoming"] = wantIncoming; - map["auto_connect"] = autoConnect; - map["port"] = listeningPort; - map["features"] = features; - map["ip_limit"] = ipLimit; - map["verify_endpoints"] = verifyEndpoints; -} - Config -Config::makeConfig( +makeConfig( xrpl::Config const& cfg, std::uint16_t port, bool validationPublicKey, int ipLimit, bool verifyEndpoints) { - PeerFinder::Config config; - - config.peerPrivate = cfg.peerPrivate; - - // Servers with peer privacy don't want to allow incoming connections - config.wantIncoming = (!config.peerPrivate) && (port != 0); - + PeerLimitConfig limits; if ((cfg.peersOutMax == 0u) && (cfg.peersInMax == 0u)) { - if (cfg.peersMax != 0) - config.maxPeers = cfg.peersMax; - - config.maxPeers = std::max(config.maxPeers, Tuning::kMinOutCount); - config.outPeers = config.calcOutPeers(); - - // Calculate the number of outbound peers we want. If we dont want - // or can't accept incoming, this will simply be equal to maxPeers. - if (!config.wantIncoming) - config.outPeers = config.maxPeers; - - // Calculate the largest number of inbound connections we could - // take. - if (config.maxPeers >= config.outPeers) - { - config.inPeers = config.maxPeers - config.outPeers; - } - else - { - config.inPeers = 0; - } + limits.maxPeers = cfg.peersMax; } else { - config.outPeers = cfg.peersOutMax; - config.inPeers = cfg.peersInMax; - config.maxPeers = 0; + limits.inPeers = cfg.peersInMax; + limits.outPeers = cfg.peersOutMax; } - // This will cause servers configured as validators to request that - // peers they connect to never report their IP address. We set this - // after we set the 'wantIncoming' because we want a "soft" version - // of peer privacy unless the operator explicitly asks for it. - if (validationPublicKey) - config.peerPrivate = true; - - // if it's a private peer or we are running as standalone - // automatic connections would defeat the purpose. - config.autoConnect = !cfg.standalone() && !cfg.peerPrivate; - config.listeningPort = port; - config.features = ""; - config.ipLimit = ipLimit; - config.verifyEndpoints = verifyEndpoints; - - // Enforce business rules - config.applyTuning(); - - return config; + return Config::makeConfig( + cfg.peerPrivate, + cfg.standalone(), + limits, + port, + validationPublicKey, + ipLimit, + verifyEndpoints); } } // namespace xrpl::PeerFinder diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h b/src/xrpld/peerfinder/detail/StoreSqdb.h index b17d2fdc5b..b0973a42b3 100644 --- a/src/xrpld/peerfinder/detail/StoreSqdb.h +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h @@ -1,11 +1,11 @@ #pragma once #include -#include #include #include #include +#include #include #include diff --git a/src/xrpld/peerfinder/detail/iosformat.h b/src/xrpld/peerfinder/detail/iosformat.h deleted file mode 100644 index 46c69ef602..0000000000 --- a/src/xrpld/peerfinder/detail/iosformat.h +++ /dev/null @@ -1,201 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace beast { - -// A collection of handy stream manipulators and -// functions to produce nice looking log output. - -/** - * Left justifies a field at the specified width. - */ -struct Leftw -{ - explicit Leftw(int width) : width(width) - { - } - int const width; - template - friend std::basic_ios& - operator<<(std::basic_ios& ios, Leftw const& p) - { - ios.setf(std::ios_base::left, std::ios_base::adjustfield); - ios.width(p.width); - return ios; - } -}; - -/** - * Produce a section heading and fill the rest of the line with dashes. - */ -template -std::basic_string -heading(std::basic_string title, int width = 80, CharT fill = CharT('-')) -{ - title.reserve(width); - title.push_back(CharT(' ')); - title.resize(width, fill); - return title; -} - -/** - * Produce a dashed line separator, with a specified or default size. - */ -struct Divider -{ - using CharT = char; - explicit Divider(int width = 80, CharT fill = CharT('-')) : width(width), fill(fill) - { - } - int const width; - CharT const fill; - template - friend std::basic_ostream& - operator<<(std::basic_ostream& os, Divider const& d) - { - os << std::basic_string(d.width, d.fill); - return os; - } -}; - -/** - * Creates a padded field with an optional fill character. - */ -struct Fpad -{ - explicit Fpad(int width, int pad = 0, char fill = ' ') : width(width + pad), fill(fill) - { - } - int const width; - char const fill; - template - friend std::basic_ostream& - operator<<(std::basic_ostream& os, Fpad const& f) - { - os << std::basic_string(f.width, f.fill); - return os; - } -}; - -//------------------------------------------------------------------------------ - -namespace detail { - -template -std::string -to_string(T const& t) -{ - std::stringstream ss; - ss << t; - return ss.str(); -} - -} // namespace detail - -/** - * Justifies a field at the specified width. - */ -/** @{ */ -template < - class CharT, - class Traits = std::char_traits, - class Allocator = std::allocator> -class FieldT -{ -public: - using string_t = std::basic_string; - FieldT(string_t const& text, int width, int pad, bool right) - : text(text), width(width), pad(pad), right(right) - { - } - string_t const text; - int const width; - int const pad; - bool const right; - template - friend std::basic_ostream& - operator<<(std::basic_ostream& os, FieldT const& f) - { - std::size_t const length(f.text.length()); - if (f.right) - { - if (length < f.width) - os << std::basic_string(f.width - length, CharT2(' ')); - os << f.text; - } - else - { - os << f.text; - if (length < f.width) - os << std::basic_string(f.width - length, CharT2(' ')); - } - if (f.pad != 0) - os << string_t(f.pad, CharT(' ')); - return os; - } -}; - -template -FieldT -field( - std::basic_string const& text, - int width = 8, - int pad = 0, - bool right = false) -{ - return FieldT(text, width, pad, right); -} - -template -FieldT -field(CharT const* text, int width = 8, int pad = 0, bool right = false) -{ - return FieldT, std::allocator>( - std::basic_string, std::allocator>(text), - width, - pad, - right); -} - -template -FieldT -field(T const& t, int width = 8, int pad = 0, bool right = false) -{ - std::string const text(detail::to_string(t)); - return field(text, width, pad, right); -} - -template -FieldT -rField(std::basic_string const& text, int width = 8, int pad = 0) -{ - return FieldT(text, width, pad, true); -} - -template -FieldT -rField(CharT const* text, int width = 8, int pad = 0) -{ - return FieldT, std::allocator>( - std::basic_string, std::allocator>(text), - width, - pad, - true); -} - -template -FieldT -rField(T const& t, int width = 8, int pad = 0) -{ - std::string const text(detail::to_string(t)); - return field(text, width, pad, true); -} -/** @} */ - -} // namespace beast diff --git a/src/xrpld/peerfinder/make_Manager.h b/src/xrpld/peerfinder/make_Manager.h deleted file mode 100644 index 1c13d7a4ca..0000000000 --- a/src/xrpld/peerfinder/make_Manager.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -#include - -#include - -namespace xrpl::PeerFinder { - -/** - * Create a new Manager. - */ -std::unique_ptr -makeManager( - boost::asio::io_context& ioContext, - clock_type& clock, - beast::Journal journal, - BasicConfig const& config, - beast::insight::Collector::ptr const& collector); - -} // namespace xrpl::PeerFinder From 4c0180b3dbb6eee7443ac02124601d09107d9dc8 Mon Sep 17 00:00:00 2001 From: Marek Foss Date: Thu, 23 Jul 2026 22:38:21 +0100 Subject: [PATCH 30/86] test: Migrate csf and xrpld-consensus Beast non-JTx tests to GTest (#7046) Co-authored-by: Alex Kremer --- .cspell.config.yaml | 4 +- .github/scripts/levelization/README.md | 18 +- .../scripts/levelization/results/ordering.txt | 24 +- cmake/XrplCore.cmake | 11 + .../xrpl/consensus/CensorshipDetector.h | 4 +- .../xrpl}/consensus/Consensus.h | 7 +- .../xrpl}/consensus/ConsensusParms.h | 0 .../xrpl}/consensus/ConsensusProposal.h | 0 .../xrpl}/consensus/ConsensusTypes.h | 5 +- .../xrpl}/consensus/DisputedTx.h | 3 +- .../xrpl}/consensus/LedgerTrie.h | 0 .../xrpl}/consensus/README.md | 0 .../xrpl}/consensus/Validations.h | 3 +- .../consensus/Consensus.cpp | 7 +- src/test/app/RCLValidations_test.cpp | 2 +- .../consensus/ByzantineFailureSim_test.cpp | 88 - src/test/consensus/Consensus_test.cpp | 1432 ---------------- .../DistributedValidatorsSim_test.cpp | 253 --- src/test/consensus/LedgerTiming_test.cpp | 118 -- src/test/consensus/LedgerTrie_test.cpp | 716 -------- .../consensus/RCLCensorshipDetector_test.cpp | 83 - src/test/consensus/ScaleFreeSim_test.cpp | 109 -- src/test/consensus/Validations_test.cpp | 1059 ------------ src/test/csf/BasicNetwork_test.cpp | 134 -- src/test/csf/Digraph_test.cpp | 81 - src/test/csf/Histogram_test.cpp | 66 - src/test/csf/Scheduler_test.cpp | 68 - src/tests/libxrpl/CMakeLists.txt | 11 + .../{base_uint_test.cpp => base_uint.cpp} | 3 +- .../libxrpl/consensus/ByzantineFailureSim.cpp | 81 + .../libxrpl/consensus/CensorshipDetector.cpp | 81 + src/tests/libxrpl/consensus/Consensus.cpp | 1455 +++++++++++++++++ .../consensus/DistributedValidatorsSim.cpp | 252 +++ src/tests/libxrpl/consensus/LedgerTiming.cpp | 105 ++ src/tests/libxrpl/consensus/LedgerTrie.cpp | 693 ++++++++ src/tests/libxrpl/consensus/ScaleFreeSim.cpp | 100 ++ src/tests/libxrpl/consensus/Validations.cpp | 1030 ++++++++++++ src/tests/libxrpl/csf/BasicNetwork.cpp | 122 ++ .../libxrpl}/csf/BasicNetwork.h | 4 +- .../libxrpl}/csf/CollectorRef.h | 12 +- src/tests/libxrpl/csf/Digraph.cpp | 72 + src/{test => tests/libxrpl}/csf/Digraph.h | 0 src/tests/libxrpl/csf/Histogram.cpp | 59 + src/{test => tests/libxrpl}/csf/Histogram.h | 0 src/{test => tests/libxrpl}/csf/Peer.h | 31 +- src/{test => tests/libxrpl}/csf/PeerGroup.h | 8 +- src/{test => tests/libxrpl}/csf/Proposal.h | 8 +- src/{test => tests/libxrpl}/csf/README.md | 0 src/tests/libxrpl/csf/Scheduler.cpp | 61 + src/{test => tests/libxrpl}/csf/Scheduler.h | 0 src/{test => tests/libxrpl}/csf/Sim.h | 18 +- src/{test => tests/libxrpl}/csf/SimTime.h | 0 src/{test => tests/libxrpl}/csf/TrustGraph.h | 4 +- src/{test => tests/libxrpl}/csf/Tx.h | 0 src/{test => tests/libxrpl}/csf/Validation.h | 4 +- src/{test => tests/libxrpl}/csf/collectors.h | 12 +- src/{test => tests/libxrpl}/csf/csf_graph.png | Bin .../libxrpl}/csf/csf_overview.png | Bin src/{test => tests/libxrpl}/csf/events.h | 6 +- src/{test => tests/libxrpl}/csf/impl/Sim.cpp | 6 +- .../libxrpl}/csf/impl/ledgers.cpp | 6 +- src/{test => tests/libxrpl}/csf/ledgers.h | 4 +- src/{test => tests/libxrpl}/csf/random.h | 4 +- src/{test => tests/libxrpl}/csf/submitters.h | 6 +- src/{test => tests/libxrpl}/csf/timers.h | 4 +- src/xrpld/app/consensus/RCLConsensus.cpp | 8 +- src/xrpld/app/consensus/RCLConsensus.h | 10 +- src/xrpld/app/consensus/RCLCxPeerPos.h | 3 +- src/xrpld/app/consensus/RCLValidations.cpp | 2 +- src/xrpld/app/consensus/RCLValidations.h | 3 +- src/xrpld/app/misc/NetworkOPs.cpp | 4 +- src/xrpld/overlay/detail/PeerImp.cpp | 2 +- 72 files changed, 4253 insertions(+), 4336 deletions(-) rename src/xrpld/app/consensus/RCLCensorshipDetector.h => include/xrpl/consensus/CensorshipDetector.h (98%) rename {src/xrpld => include/xrpl}/consensus/Consensus.h (99%) rename {src/xrpld => include/xrpl}/consensus/ConsensusParms.h (100%) rename {src/xrpld => include/xrpl}/consensus/ConsensusProposal.h (100%) rename {src/xrpld => include/xrpl}/consensus/ConsensusTypes.h (98%) rename {src/xrpld => include/xrpl}/consensus/DisputedTx.h (99%) rename {src/xrpld => include/xrpl}/consensus/LedgerTrie.h (100%) rename {src/xrpld => include/xrpl}/consensus/README.md (100%) rename {src/xrpld => include/xrpl}/consensus/Validations.h (99%) rename src/{xrpld => libxrpl}/consensus/Consensus.cpp (98%) delete mode 100644 src/test/consensus/ByzantineFailureSim_test.cpp delete mode 100644 src/test/consensus/Consensus_test.cpp delete mode 100644 src/test/consensus/DistributedValidatorsSim_test.cpp delete mode 100644 src/test/consensus/LedgerTiming_test.cpp delete mode 100644 src/test/consensus/LedgerTrie_test.cpp delete mode 100644 src/test/consensus/RCLCensorshipDetector_test.cpp delete mode 100644 src/test/consensus/ScaleFreeSim_test.cpp delete mode 100644 src/test/consensus/Validations_test.cpp delete mode 100644 src/test/csf/BasicNetwork_test.cpp delete mode 100644 src/test/csf/Digraph_test.cpp delete mode 100644 src/test/csf/Histogram_test.cpp delete mode 100644 src/test/csf/Scheduler_test.cpp rename src/tests/libxrpl/basics/{base_uint_test.cpp => base_uint.cpp} (99%) create mode 100644 src/tests/libxrpl/consensus/ByzantineFailureSim.cpp create mode 100644 src/tests/libxrpl/consensus/CensorshipDetector.cpp create mode 100644 src/tests/libxrpl/consensus/Consensus.cpp create mode 100644 src/tests/libxrpl/consensus/DistributedValidatorsSim.cpp create mode 100644 src/tests/libxrpl/consensus/LedgerTiming.cpp create mode 100644 src/tests/libxrpl/consensus/LedgerTrie.cpp create mode 100644 src/tests/libxrpl/consensus/ScaleFreeSim.cpp create mode 100644 src/tests/libxrpl/consensus/Validations.cpp create mode 100644 src/tests/libxrpl/csf/BasicNetwork.cpp rename src/{test => tests/libxrpl}/csf/BasicNetwork.h (99%) rename src/{test => tests/libxrpl}/csf/CollectorRef.h (97%) create mode 100644 src/tests/libxrpl/csf/Digraph.cpp rename src/{test => tests/libxrpl}/csf/Digraph.h (100%) create mode 100644 src/tests/libxrpl/csf/Histogram.cpp rename src/{test => tests/libxrpl}/csf/Histogram.h (100%) rename src/{test => tests/libxrpl}/csf/Peer.h (98%) rename src/{test => tests/libxrpl}/csf/PeerGroup.h (98%) rename src/{test => tests/libxrpl}/csf/Proposal.h (66%) rename src/{test => tests/libxrpl}/csf/README.md (100%) create mode 100644 src/tests/libxrpl/csf/Scheduler.cpp rename src/{test => tests/libxrpl}/csf/Scheduler.h (100%) rename src/{test => tests/libxrpl}/csf/Sim.h (94%) rename src/{test => tests/libxrpl}/csf/SimTime.h (100%) rename src/{test => tests/libxrpl}/csf/TrustGraph.h (99%) rename src/{test => tests/libxrpl}/csf/Tx.h (100%) rename src/{test => tests/libxrpl}/csf/Validation.h (99%) rename src/{test => tests/libxrpl}/csf/collectors.h (99%) rename src/{test => tests/libxrpl}/csf/csf_graph.png (100%) rename src/{test => tests/libxrpl}/csf/csf_overview.png (100%) rename src/{test => tests/libxrpl}/csf/events.h (96%) rename src/{test => tests/libxrpl}/csf/impl/Sim.cpp (93%) rename src/{test => tests/libxrpl}/csf/impl/ledgers.cpp (98%) rename src/{test => tests/libxrpl}/csf/ledgers.h (99%) rename src/{test => tests/libxrpl}/csf/random.h (98%) rename src/{test => tests/libxrpl}/csf/submitters.h (96%) rename src/{test => tests/libxrpl}/csf/timers.h (96%) diff --git a/.cspell.config.yaml b/.cspell.config.yaml index c862379f08..9cd8417362 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -30,7 +30,9 @@ ignoreRegExpList: - ABCDEFGHIJKLMNOPQRSTUVWXYZ - ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz overrides: - - filename: "**/*_test.cpp" # all test files + - filename: + - "**/*_test.cpp" # legacy boost.test files + - "src/tests/**/*.cpp" # gtest test files ignoreRegExpList: - /"[^"]*"/g # double-quoted strings - /'[^']*'/g # single-quoted strings diff --git a/.github/scripts/levelization/README.md b/.github/scripts/levelization/README.md index f657344827..93748c43e1 100644 --- a/.github/scripts/levelization/README.md +++ b/.github/scripts/levelization/README.md @@ -40,18 +40,18 @@ listed later. | 04 | xrpl/protocol | | 05 | xrpl/core xrpl/resource xrpl/server | | 06 | xrpl/ledger xrpl/nodestore xrpl/net | -| 07 | xrpl/shamap | +| 07 | xrpl/shamap xrpl/consensus | ## xrpld Modules (Application Implementation) -| Level / Tier | Module(s) | -| ------------ | -------------------------------- | -| 05 | xrpld/conditions xrpld/consensus | -| 06 | xrpld/core xrpld/peerfinder | -| 07 | xrpld/shamap xrpld/overlay | -| 08 | xrpld/app | -| 09 | xrpld/rpc | -| 10 | xrpld/perflog | +| Level / Tier | Module(s) | +| ------------ | --------------------------- | +| 05 | xrpld/conditions | +| 06 | xrpld/core xrpld/peerfinder | +| 07 | xrpld/shamap xrpld/overlay | +| 08 | xrpld/app | +| 09 | xrpld/rpc | +| 10 | xrpld/perflog | ## Test Modules diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index fdd134dc8a..709ba4d6d4 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -6,6 +6,8 @@ libxrpl.conditions > xrpl.basics libxrpl.conditions > xrpl.conditions libxrpl.config > xrpl.basics libxrpl.config > xrpl.config +libxrpl.consensus > xrpl.basics +libxrpl.consensus > xrpl.consensus libxrpl.core > xrpl.basics libxrpl.core > xrpl.core libxrpl.core > xrpl.json @@ -63,9 +65,9 @@ test.app > test.jtx test.app > test.unit_test test.app > xrpl.basics test.app > xrpl.config +test.app > xrpl.consensus test.app > xrpl.core test.app > xrpld.app -test.app > xrpld.consensus test.app > xrpld.core test.app > xrpld.overlay test.app > xrpld.rpc @@ -86,12 +88,9 @@ test.basics > xrpl.protocol test.beast > xrpl.basics test.conditions > xrpl.basics test.conditions > xrpl.conditions -test.consensus > test.csf test.consensus > test.jtx -test.consensus > test.unit_test test.consensus > xrpl.basics test.consensus > xrpld.app -test.consensus > xrpld.consensus test.consensus > xrpl.ledger test.consensus > xrpl.protocol test.consensus > xrpl.shamap @@ -106,10 +105,6 @@ test.core > xrpl.json test.core > xrpl.protocol test.core > xrpl.rdb test.core > xrpl.server -test.csf > xrpl.basics -test.csf > xrpld.consensus -test.csf > xrpl.json -test.csf > xrpl.ledger test.json > test.jtx test.json > xrpl.json test.jtx > test.unit_test @@ -189,6 +184,7 @@ test.unit_test > xrpl.basics test.unit_test > xrpl.protocol tests.libxrpl > xrpl.basics tests.libxrpl > xrpl.config +tests.libxrpl > xrpl.consensus tests.libxrpl > xrpl.core tests.libxrpl > xrpl.json tests.libxrpl > xrpl.ledger @@ -204,6 +200,10 @@ tests.libxrpl > xrpl.tx xrpl.conditions > xrpl.basics xrpl.conditions > xrpl.protocol xrpl.config > xrpl.basics +xrpl.consensus > xrpl.basics +xrpl.consensus > xrpl.json +xrpl.consensus > xrpl.ledger +xrpl.consensus > xrpl.protocol xrpl.core > xrpl.basics xrpl.core > xrpl.json xrpl.core > xrpl.protocol @@ -246,8 +246,8 @@ xrpl.tx > xrpl.protocol xrpld.app > test.unit_test xrpld.app > xrpl.basics xrpld.app > xrpl.config +xrpld.app > xrpl.consensus xrpld.app > xrpl.core -xrpld.app > xrpld.consensus xrpld.app > xrpld.core xrpld.app > xrpl.json xrpld.app > xrpl.ledger @@ -260,10 +260,6 @@ xrpld.app > xrpl.resource xrpld.app > xrpl.server xrpld.app > xrpl.shamap xrpld.app > xrpl.tx -xrpld.consensus > xrpl.basics -xrpld.consensus > xrpl.json -xrpld.consensus > xrpl.ledger -xrpld.consensus > xrpl.protocol xrpld.core > xrpl.basics xrpld.core > xrpl.config xrpld.core > xrpl.core @@ -272,8 +268,8 @@ xrpld.core > xrpl.protocol xrpld.core > xrpl.rdb xrpld.overlay > xrpl.basics xrpld.overlay > xrpl.config +xrpld.overlay > xrpl.consensus xrpld.overlay > xrpl.core -xrpld.overlay > xrpld.consensus xrpld.overlay > xrpld.core xrpld.overlay > xrpld.peerfinder xrpld.overlay > xrpl.json diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index 62a8fe143b..a3e08145d5 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -207,6 +207,16 @@ target_link_libraries( add_module(xrpl tx) target_link_libraries(xrpl.libxrpl.tx PUBLIC xrpl.libxrpl.ledger) +add_module(xrpl consensus) +target_link_libraries( + xrpl.libxrpl.consensus + PUBLIC + xrpl.libxrpl.basics + xrpl.libxrpl.json + xrpl.libxrpl.protocol + xrpl.libxrpl.ledger +) + add_library(xrpl.libxrpl) set_target_properties(xrpl.libxrpl PROPERTIES OUTPUT_NAME xrpl) @@ -226,6 +236,7 @@ target_link_modules( beast conditions config + consensus core crypto git diff --git a/src/xrpld/app/consensus/RCLCensorshipDetector.h b/include/xrpl/consensus/CensorshipDetector.h similarity index 98% rename from src/xrpld/app/consensus/RCLCensorshipDetector.h rename to include/xrpl/consensus/CensorshipDetector.h index 6d0e20031e..3d2708f68f 100644 --- a/src/xrpld/app/consensus/RCLCensorshipDetector.h +++ b/include/xrpl/consensus/CensorshipDetector.h @@ -10,7 +10,7 @@ namespace xrpl { template -class RCLCensorshipDetector +class CensorshipDetector { public: struct TxIDSeq @@ -49,7 +49,7 @@ private: TxIDSeqVec tracker_; public: - RCLCensorshipDetector() = default; + CensorshipDetector() = default; /** * Add transactions being proposed for the current consensus round. diff --git a/src/xrpld/consensus/Consensus.h b/include/xrpl/consensus/Consensus.h similarity index 99% rename from src/xrpld/consensus/Consensus.h rename to include/xrpl/consensus/Consensus.h index 440191939b..f9d5f7ef02 100644 --- a/src/xrpld/consensus/Consensus.h +++ b/include/xrpl/consensus/Consensus.h @@ -1,15 +1,14 @@ #pragma once -#include -#include -#include - #include #include #include #include #include #include +#include +#include +#include #include #include #include diff --git a/src/xrpld/consensus/ConsensusParms.h b/include/xrpl/consensus/ConsensusParms.h similarity index 100% rename from src/xrpld/consensus/ConsensusParms.h rename to include/xrpl/consensus/ConsensusParms.h diff --git a/src/xrpld/consensus/ConsensusProposal.h b/include/xrpl/consensus/ConsensusProposal.h similarity index 100% rename from src/xrpld/consensus/ConsensusProposal.h rename to include/xrpl/consensus/ConsensusProposal.h diff --git a/src/xrpld/consensus/ConsensusTypes.h b/include/xrpl/consensus/ConsensusTypes.h similarity index 98% rename from src/xrpld/consensus/ConsensusTypes.h rename to include/xrpl/consensus/ConsensusTypes.h index 4553e46f48..4dac2d9912 100644 --- a/src/xrpld/consensus/ConsensusTypes.h +++ b/include/xrpl/consensus/ConsensusTypes.h @@ -1,11 +1,10 @@ #pragma once -#include -#include - #include #include #include +#include +#include #include #include diff --git a/src/xrpld/consensus/DisputedTx.h b/include/xrpl/consensus/DisputedTx.h similarity index 99% rename from src/xrpld/consensus/DisputedTx.h rename to include/xrpl/consensus/DisputedTx.h index 12ed00d460..c194716d43 100644 --- a/src/xrpld/consensus/DisputedTx.h +++ b/include/xrpl/consensus/DisputedTx.h @@ -1,9 +1,8 @@ #pragma once -#include - #include #include +#include #include #include diff --git a/src/xrpld/consensus/LedgerTrie.h b/include/xrpl/consensus/LedgerTrie.h similarity index 100% rename from src/xrpld/consensus/LedgerTrie.h rename to include/xrpl/consensus/LedgerTrie.h diff --git a/src/xrpld/consensus/README.md b/include/xrpl/consensus/README.md similarity index 100% rename from src/xrpld/consensus/README.md rename to include/xrpl/consensus/README.md diff --git a/src/xrpld/consensus/Validations.h b/include/xrpl/consensus/Validations.h similarity index 99% rename from src/xrpld/consensus/Validations.h rename to include/xrpl/consensus/Validations.h index 2696804c86..ebb13c5e7c 100644 --- a/src/xrpld/consensus/Validations.h +++ b/include/xrpl/consensus/Validations.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -11,6 +9,7 @@ #include #include #include +#include #include #include diff --git a/src/xrpld/consensus/Consensus.cpp b/src/libxrpl/consensus/Consensus.cpp similarity index 98% rename from src/xrpld/consensus/Consensus.cpp rename to src/libxrpl/consensus/Consensus.cpp index d529ab2e44..6f398cf66c 100644 --- a/src/xrpld/consensus/Consensus.cpp +++ b/src/libxrpl/consensus/Consensus.cpp @@ -1,10 +1,9 @@ -#include - -#include -#include +#include #include #include +#include +#include #include #include diff --git a/src/test/app/RCLValidations_test.cpp b/src/test/app/RCLValidations_test.cpp index aaf84225a7..9ace2e21af 100644 --- a/src/test/app/RCLValidations_test.cpp +++ b/src/test/app/RCLValidations_test.cpp @@ -2,12 +2,12 @@ #include #include -#include #include #include #include #include +#include #include #include #include diff --git a/src/test/consensus/ByzantineFailureSim_test.cpp b/src/test/consensus/ByzantineFailureSim_test.cpp deleted file mode 100644 index c3c51125b5..0000000000 --- a/src/test/consensus/ByzantineFailureSim_test.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include - -namespace xrpl::test { - -class ByzantineFailureSim_test : public beast::unit_test::Suite -{ - void - run() override - { - using namespace csf; - using namespace std::chrono; - - // This test simulates a specific topology with nodes generating - // different ledgers due to a simulated byzantine failure (injecting - // an extra non-consensus transaction). - - Sim sim; - ConsensusParms const parms{}; - - SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); - PeerGroup a = sim.createGroup(1); - PeerGroup b = sim.createGroup(1); - PeerGroup c = sim.createGroup(1); - PeerGroup d = sim.createGroup(1); - PeerGroup e = sim.createGroup(1); - PeerGroup f = sim.createGroup(1); - PeerGroup g = sim.createGroup(1); - - a.trustAndConnect(a + b + c + g, delay); - b.trustAndConnect(b + a + c + d + e, delay); - c.trustAndConnect(c + a + b + d + e, delay); - d.trustAndConnect(d + b + c + e + f, delay); - e.trustAndConnect(e + b + c + d + f, delay); - f.trustAndConnect(f + d + e + g, delay); - g.trustAndConnect(g + a + f, delay); - - PeerGroup const network = a + b + c + d + e + f + g; - - StreamCollector sc{std::cout}; - - sim.collectors.add(sc); - - for (TrustGraph::ForkInfo const& fi : sim.trustGraph.forkablePairs(0.8)) - { - std::cout << "Can fork " << PeerGroup{fi.unlA} << " " - << " " << PeerGroup{fi.unlB} << " overlap " << fi.overlap << " required " - << fi.required << "\n"; - }; - - // set prior state - sim.run(1); - - PeerGroup byzantineNodes = a + b + c + g; - // All peers see some TX 0 - for (Peer* peer : network) - { - peer->submit(Tx{0}); - // Peers 0,1,2,6 will close the next ledger differently by injecting - // a non-consensus approved transaction - if (byzantineNodes.contains(peer)) - { - peer->txInjections.emplace(peer->lastClosedLedger.seq(), Tx{42}); - } - } - sim.run(4); - std::cout << "Branches: " << sim.branches() << "\n"; - std::cout << "Fully synchronized: " << std::boolalpha << sim.synchronized() << "\n"; - // Not tessting anything currently. - pass(); - } -}; - -BEAST_DEFINE_TESTSUITE_MANUAL(ByzantineFailureSim, consensus, xrpl); - -} // namespace xrpl::test diff --git a/src/test/consensus/Consensus_test.cpp b/src/test/consensus/Consensus_test.cpp deleted file mode 100644 index 45f58d16ba..0000000000 --- a/src/test/consensus/Consensus_test.cpp +++ /dev/null @@ -1,1432 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -class Consensus_test : public beast::unit_test::Suite -{ - SuiteJournal journal_; - -public: - Consensus_test() : journal_("Consensus_test", *this) - { - } - - void - testShouldCloseLedger() - { - using namespace std::chrono_literals; - testcase("should close ledger"); - - // Use default parameters - ConsensusParms const p{}; - - // Bizarre times forcibly close - BEAST_EXPECT(shouldCloseLedger(true, 10, 10, 10, -10s, 10s, 1s, 1s, p, journal_)); - BEAST_EXPECT(shouldCloseLedger(true, 10, 10, 10, 100h, 10s, 1s, 1s, p, journal_)); - BEAST_EXPECT(shouldCloseLedger(true, 10, 10, 10, 10s, 100h, 1s, 1s, p, journal_)); - - // Rest of network has closed - BEAST_EXPECT(shouldCloseLedger(true, 10, 3, 5, 10s, 10s, 10s, 10s, p, journal_)); - - // No transactions means wait until end of internval - BEAST_EXPECT(!shouldCloseLedger(false, 10, 0, 0, 1s, 1s, 1s, 10s, p, journal_)); - BEAST_EXPECT(shouldCloseLedger(false, 10, 0, 0, 1s, 10s, 1s, 10s, p, journal_)); - - // Enforce minimum ledger open time - BEAST_EXPECT(!shouldCloseLedger(true, 10, 0, 0, 10s, 10s, 1s, 10s, p, journal_)); - - // Don't go too much faster than last time - BEAST_EXPECT(!shouldCloseLedger(true, 10, 0, 0, 10s, 10s, 3s, 10s, p, journal_)); - - BEAST_EXPECT(shouldCloseLedger(true, 10, 0, 0, 10s, 10s, 10s, 10s, p, journal_)); - } - - void - testCheckConsensus() - { - using namespace std::chrono_literals; - testcase("check consensus"); - - // Use default parameters - ConsensusParms const p{}; - - /////////////// - // Disputes still in doubt - // - // Not enough time has elapsed - BEAST_EXPECT( - ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 2s, false, p, true, journal_)); - - // If not enough peers have proposed, ensure - // more time for proposals - BEAST_EXPECT( - ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true, journal_)); - - // Enough time has elapsed and we all agree - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true, journal_)); - - // Enough time has elapsed and we don't yet agree - BEAST_EXPECT( - ConsensusState::No == checkConsensus(10, 2, 1, 0, 3s, 10s, false, p, true, journal_)); - - // Our peers have moved on - // Enough time has elapsed and we all agree - BEAST_EXPECT( - ConsensusState::MovedOn == - checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true, journal_)); - - // If no peers, don't agree until time has passed. - BEAST_EXPECT( - ConsensusState::No == checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true, journal_)); - - // Agree if no peers and enough time has passed. - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(0, 0, 0, 0, 3s, 16s, false, p, true, journal_)); - - // Expire if too much time has passed without agreement - BEAST_EXPECT( - ConsensusState::Expired == - checkConsensus(10, 8, 1, 0, 1s, 19s, false, p, true, journal_)); - - /////////////// - // Stalled - // - // Not enough time has elapsed - BEAST_EXPECT( - ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 2s, true, p, true, journal_)); - - // If not enough peers have proposed, ensure - // more time for proposals - BEAST_EXPECT( - ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 4s, true, p, true, journal_)); - - // Enough time has elapsed and we all agree - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(10, 2, 2, 0, 3s, 10s, true, p, true, journal_)); - - // Enough time has elapsed and we don't yet agree, but there's nothing - // left to dispute - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(10, 2, 1, 0, 3s, 10s, true, p, true, journal_)); - - // Our peers have moved on - // Enough time has elapsed and we all agree, nothing left to dispute - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(10, 2, 1, 8, 3s, 10s, true, p, true, journal_)); - - // If no peers, don't agree until time has passed. - BEAST_EXPECT( - ConsensusState::No == checkConsensus(0, 0, 0, 0, 3s, 10s, true, p, true, journal_)); - - // Agree if no peers and enough time has passed. - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(0, 0, 0, 0, 3s, 16s, true, p, true, journal_)); - - // We are done if there's nothing left to dispute, no matter how much - // time has passed - BEAST_EXPECT( - ConsensusState::Yes == checkConsensus(10, 8, 1, 0, 1s, 19s, true, p, true, journal_)); - } - - void - testStandalone() - { - using namespace std::chrono_literals; - using namespace csf; - testcase("standalone"); - - Sim s; - PeerGroup const peers = s.createGroup(1); - Peer* peer = peers[0]; - peer->targetLedgers = 1; - peer->start(); - peer->submit(Tx{1}); - - s.scheduler.step(); - - // Inspect that the proper ledger was created - auto const& lcl = peer->lastClosedLedger; - BEAST_EXPECT(peer->prevLedgerID() == lcl.id()); - BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); - BEAST_EXPECT(lcl.txs().size() == 1); - BEAST_EXPECT(lcl.txs().contains(Tx{1})); - BEAST_EXPECT(peer->prevProposers == 0); - } - - void - testPeersAgree() - { - using namespace csf; - using namespace std::chrono; - testcase("peers agree"); - - ConsensusParms const parms{}; - Sim sim; - PeerGroup peers = sim.createGroup(5); - - // Connected trust and network graphs with single fixed delay - peers.trustAndConnect(peers, round(0.2 * parms.ledgerGRANULARITY)); - - // everyone submits their own ID as a TX - for (Peer* p : peers) - p->submit(Tx(static_cast(p->id))); - - sim.run(1); - - // All peers are in sync - if (BEAST_EXPECT(sim.synchronized())) - { - for (Peer const* peer : peers) - { - auto const& lcl = peer->lastClosedLedger; - BEAST_EXPECT(lcl.id() == peer->prevLedgerID()); - BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); - // All peers proposed - BEAST_EXPECT(peer->prevProposers == peers.size() - 1); - // All transactions were accepted - for (std::uint32_t i = 0; i < peers.size(); ++i) - BEAST_EXPECT(lcl.txs().contains(Tx{i})); - } - } - } - - void - testSlowPeers() - { - using namespace csf; - using namespace std::chrono; - testcase("slow peers"); - - // Several tests of a complete trust graph with a subset of peers - // that have significantly longer network delays to the rest of the - // network - - // Test when a slow peer doesn't delay a consensus quorum (4/5 agree) - { - ConsensusParms const parms{}; - Sim sim; - PeerGroup slow = sim.createGroup(1); - PeerGroup fast = sim.createGroup(4); - PeerGroup network = fast + slow; - - // Fully connected trust graph - network.trust(network); - - // Fast and slow network connections - fast.connect(fast, round(0.2 * parms.ledgerGRANULARITY)); - - slow.connect(network, round(1.1 * parms.ledgerGRANULARITY)); - - // All peers submit their own ID as a transaction - for (Peer* peer : network) - peer->submit(Tx{static_cast(peer->id)}); - - sim.run(1); - - // Verify all peers have same LCL but are missing transaction 0 - // All peers are in sync even with a slower peer 0 - if (BEAST_EXPECT(sim.synchronized())) - { - for (Peer const* peer : network) - { - auto const& lcl = peer->lastClosedLedger; - BEAST_EXPECT(lcl.id() == peer->prevLedgerID()); - BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); - - BEAST_EXPECT(peer->prevProposers == network.size() - 1); - BEAST_EXPECT(peer->prevRoundTime == network[0]->prevRoundTime); - - BEAST_EXPECT(not lcl.txs().contains(Tx{0})); - for (std::uint32_t i = 2; i < network.size(); ++i) - BEAST_EXPECT(lcl.txs().contains(Tx{i})); - - // Tx 0 didn't make it - BEAST_EXPECT(peer->openTxs.contains(Tx{0})); - } - } - } - - // Test when the slow peers delay a consensus quorum (4/6 agree) - { - // Run two tests - // 1. The slow peers are participating in consensus - // 2. The slow peers are just observing - - for (auto isParticipant : {true, false}) - { - ConsensusParms const parms{}; - - Sim sim; - PeerGroup slow = sim.createGroup(2); - PeerGroup fast = sim.createGroup(4); - PeerGroup network = fast + slow; - - // Connected trust graph - network.trust(network); - - // Fast and slow network connections - fast.connect(fast, round(0.2 * parms.ledgerGRANULARITY)); - - slow.connect(network, round(1.1 * parms.ledgerGRANULARITY)); - - for (Peer* peer : slow) - peer->runAsValidator = isParticipant; - - // All peers submit their own ID as a transaction and relay it - // to peers - for (Peer* peer : network) - peer->submit(Tx{static_cast(peer->id)}); - - sim.run(1); - - if (BEAST_EXPECT(sim.synchronized())) - { - // Verify all peers have same LCL but are missing - // transaction 0,1 which was not received by all peers - // before the ledger closed - for (Peer const* peer : network) - { - // Closed ledger has all but transaction 0,1 - auto const& lcl = peer->lastClosedLedger; - BEAST_EXPECT(lcl.seq() == Ledger::Seq{1}); - BEAST_EXPECT(not lcl.txs().contains(Tx{0})); - BEAST_EXPECT(not lcl.txs().contains(Tx{1})); - for (std::uint32_t i = slow.size(); i < network.size(); ++i) - BEAST_EXPECT(lcl.txs().contains(Tx{i})); - - // Tx 0-1 didn't make it - BEAST_EXPECT(peer->openTxs.contains(Tx{0})); - BEAST_EXPECT(peer->openTxs.contains(Tx{1})); - } - - Peer const* slowPeer = slow[0]; - if (isParticipant) - { - BEAST_EXPECT(slowPeer->prevProposers == network.size() - 1); - } - else - { - BEAST_EXPECT(slowPeer->prevProposers == fast.size()); - } - - for (Peer const* peer : fast) - { - // Due to the network link delay settings - // Peer 0 initially proposes {0} - // Peer 1 initially proposes {1} - // Peers 2-5 initially propose {2,3,4,5} - // Since peers 2-5 agree, 4/6 > the initial 50% needed - // to include a disputed transaction, so Peer 0/1 switch - // to agree with those peers. Peer 0/1 then closes with - // an 80% quorum of agreeing positions (5/6) match. - // - // Peers 2-5 do not change position, since tx 0 or tx 1 - // have less than the 50% initial threshold. They also - // cannot declare consensus, since 4/6 agreeing - // positions are < 80% threshold. They therefore need an - // additional timerEntry call to see the updated - // positions from Peer 0 & 1. - - if (isParticipant) - { - BEAST_EXPECT(peer->prevProposers == network.size() - 1); - BEAST_EXPECT(peer->prevRoundTime > slowPeer->prevRoundTime); - } - else - { - BEAST_EXPECT(peer->prevProposers == fast.size() - 1); - // so all peers should have closed together - BEAST_EXPECT(peer->prevRoundTime == slowPeer->prevRoundTime); - } - } - } - } - } - } - - void - testCloseTimeDisagree() - { - using namespace csf; - using namespace std::chrono; - testcase("close time disagree"); - - // This is a very specialized test to get ledgers to disagree on - // the close time. It unfortunately assumes knowledge about current - // timing constants. This is a necessary evil to get coverage up - // pending more extensive refactorings of timing constants. - - // In order to agree-to-disagree on the close time, there must be no - // clear majority of nodes agreeing on a close time. This test - // sets a relative offset to the peers internal clocks so that they - // send proposals with differing times. - - // However, agreement is on the effective close time, not the - // exact close time. The minimum closeTimeResolution is given by - // ledgerPossibleTimeResolutions[0], which is currently 10s. This means - // the skews need to be at least 10 seconds to have different effective - // close times. - - // Complicating this matter is that nodes will ignore proposals - // with times more than proposeFRESHNESS =20s in the past. So at - // the minimum granularity, we have at most 3 types of skews - // (0s,10s,20s). - - // This test therefore has 6 nodes, with 2 nodes having each type of - // skew. Then no majority (1/3 < 1/2) of nodes will agree on an - // actual close time. - - ConsensusParms const parms{}; - Sim sim; - - PeerGroup groupA = sim.createGroup(2); - PeerGroup const groupB = sim.createGroup(2); - PeerGroup const groupC = sim.createGroup(2); - PeerGroup network = groupA + groupB + groupC; - - network.trust(network); - network.connect(network, round(0.2 * parms.ledgerGRANULARITY)); - - // Run consensus without skew until we have a short close time - // resolution - Peer const* firstPeer = *groupA.begin(); - while (firstPeer->lastClosedLedger.closeTimeResolution() >= parms.proposeFRESHNESS) - sim.run(1); - - // Introduce a shift on the time of 2/3 of peers - for (Peer* peer : groupA) - peer->clockSkew = parms.proposeFRESHNESS / 2; - for (Peer* peer : groupB) - peer->clockSkew = parms.proposeFRESHNESS; - - sim.run(1); - - // All nodes agreed to disagree on the close time - if (BEAST_EXPECT(sim.synchronized())) - { - for (Peer const* peer : network) - BEAST_EXPECT(!peer->lastClosedLedger.closeAgree()); - } - } - - void - testWrongLCL() - { - using namespace csf; - using namespace std::chrono; - testcase("wrong LCL"); - - // Specialized test to exercise a temporary fork in which some peers - // are working on an incorrect prior ledger. - - ConsensusParms const parms{}; - - // Vary the time it takes to process validations to exercise detecting - // the wrong LCL at different phases of consensus - for (auto validationDelay : {0ms, parms.ledgerMinClose}) - { - // Consider 10 peers: - // 0 1 2 3 4 5 6 7 8 9 - // minority majorityA majorityB - // - // Nodes 0-1 trust nodes 0-4 - // Nodes 2-9 trust nodes 2-9 - // - // By submitting tx 0 to nodes 0-4 and tx 1 to nodes 5-9, - // nodes 0-1 will generate the wrong LCL (with tx 0). The remaining - // nodes will instead accept the ledger with tx 1. - - // Nodes 0-1 will detect this mismatch during a subsequent round - // since nodes 2-4 will validate a different ledger. - - // Nodes 0-1 will acquire the proper ledger from the network and - // resume consensus and eventually generate the dominant network - // ledger. - - // This topology can potentially fork with the above trust relations - // but that is intended for this test. - - Sim sim; - - PeerGroup minority = sim.createGroup(2); - PeerGroup const majorityA = sim.createGroup(3); - PeerGroup const majorityB = sim.createGroup(5); - - PeerGroup majority = majorityA + majorityB; - PeerGroup const network = minority + majority; - - SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); - minority.trustAndConnect(minority + majorityA, delay); - majority.trustAndConnect(majority, delay); - - CollectByNode jumps; - sim.collectors.add(jumps); - - BEAST_EXPECT(sim.trustGraph.canFork(parms.minConsensusPct / 100.)); - - // initial round to set prior state - sim.run(1); - - // Nodes in smaller UNL have seen tx 0, nodes in other unl have seen - // tx 1 - for (Peer* peer : network) - peer->delays.recvValidation = validationDelay; - for (Peer* peer : (minority + majorityA)) - peer->openTxs.insert(Tx{0}); - for (Peer* peer : majorityB) - peer->openTxs.insert(Tx{1}); - - // Run for additional rounds - // With no validation delay, only 2 more rounds are needed. - // 1. Round to generate different ledgers - // 2. Round to detect different prior ledgers (but still generate - // wrong ones) and recover within that round since wrong LCL - // is detected before we close - // - // With a validation delay of ledgerMIN_CLOSE, we need 3 more - // rounds. - // 1. Round to generate different ledgers - // 2. Round to detect different prior ledgers (but still generate - // wrong ones) but end up declaring consensus on wrong LCL (but - // with the right transaction set!). This is because we detect - // the wrong LCL after we have closed the ledger, so we declare - // consensus based solely on our peer proposals. But we haven't - // had time to acquire the right ledger. - // 3. Round to correct - sim.run(3); - - // The network never actually forks, since node 0-1 never see a - // quorum of validations to fully validate the incorrect chain. - - // However, for a non zero-validation delay, the network is not - // synchronized because nodes 0 and 1 are running one ledger behind - if (BEAST_EXPECT(sim.branches() == 1)) - { - for (Peer const* peer : majority) - { - // No jumps for majority nodes - BEAST_EXPECT(jumps[peer->id].closeJumps.empty()); - BEAST_EXPECT(jumps[peer->id].fullyValidatedJumps.empty()); - } - for (Peer const* peer : minority) - { - auto& peerJumps = jumps[peer->id]; - // last closed ledger jump between chains - { - if (BEAST_EXPECT(peerJumps.closeJumps.size() == 1)) - { - JumpCollector::Jump const& jump = peerJumps.closeJumps.front(); - // Jump is to a different chain - BEAST_EXPECT(jump.from.seq() <= jump.to.seq()); - BEAST_EXPECT(!jump.to.isAncestor(jump.from)); - } - } - // fully validated jump forward in same chain - { - if (BEAST_EXPECT(peerJumps.fullyValidatedJumps.size() == 1)) - { - JumpCollector::Jump const& jump = peerJumps.fullyValidatedJumps.front(); - // Jump is to a different chain with same seq - BEAST_EXPECT(jump.from.seq() < jump.to.seq()); - BEAST_EXPECT(jump.to.isAncestor(jump.from)); - } - } - } - } - } - - { - // Additional test engineered to switch LCL during the establish - // phase. This was added to trigger a scenario that previously - // crashed, in which switchLCL switched from establish to open - // phase, but still processed the establish phase logic. - - // Loner node will accept an initial ledger A, but all other nodes - // accept ledger B a bit later. By delaying the time it takes - // to process a validation, loner node will detect the wrongLCL - // after it is already in the establish phase of the next round. - - Sim sim; - PeerGroup loner = sim.createGroup(1); - PeerGroup const friends = sim.createGroup(3); - loner.trust(loner + friends); - - PeerGroup const others = sim.createGroup(6); - PeerGroup clique = friends + others; - clique.trust(clique); - - PeerGroup network = loner + clique; - network.connect(network, round(0.2 * parms.ledgerGRANULARITY)); - - // initial round to set prior state - sim.run(1); - for (Peer* peer : (loner + friends)) - peer->openTxs.insert(Tx(0)); - for (Peer* peer : others) - peer->openTxs.insert(Tx(1)); - - // Delay validation processing - for (Peer* peer : network) - peer->delays.recvValidation = parms.ledgerGRANULARITY; - - // additional rounds to generate wrongLCL and recover - sim.run(2); - - // Check all peers recovered - for (Peer const* p : network) - BEAST_EXPECT(p->prevLedgerID() == network[0]->prevLedgerID()); - } - } - - void - testConsensusCloseTimeRounding() - { - using namespace csf; - using namespace std::chrono; - testcase("consensus close time rounding"); - - // This is a specialized test engineered to yield ledgers with different - // close times even though the peers believe they had close time - // consensus on the ledger. - ConsensusParms const parms; - - Sim sim; - - // This requires a group of 4 fast and 2 slow peers to create a - // situation in which a subset of peers requires seeing additional - // proposals to declare consensus. - PeerGroup slow = sim.createGroup(2); - PeerGroup fast = sim.createGroup(4); - PeerGroup network = fast + slow; - - // Connected trust graph - network.trust(network); - - // Fast and slow network connections - fast.connect(fast, round(0.2 * parms.ledgerGRANULARITY)); - slow.connect(network, round(1.1 * parms.ledgerGRANULARITY)); - - // Run to the ledger *prior* to decreasing the resolution - sim.run(kIncreaseLedgerTimeResolutionEvery - 2); - - // In order to create the discrepancy, we want a case where if - // X = effCloseTime(closeTime, resolution, parentCloseTime) - // X != effCloseTime(X, resolution, parentCloseTime) - // - // That is, the effective close time is not a fixed point. This can - // happen if X = parentCloseTime + 1, but a subsequent rounding goes - // to the next highest multiple of resolution. - - // So we want to find an offset (now + offset) % 30s = 15 - // (now + offset) % 20s = 15 - // This way, the next ledger will close and round up Due to the - // network delay settings, the round of consensus will take 5s, so - // the next ledger's close time will - - NetClock::duration when = network[0]->now().time_since_epoch(); - - // Check we are before the 30s to 20s transition - NetClock::duration const resolution = network[0]->lastClosedLedger.closeTimeResolution(); - BEAST_EXPECT(resolution == NetClock::duration{30s}); - - while (((when % NetClock::duration{30s}) != NetClock::duration{15s}) || - ((when % NetClock::duration{20s}) != NetClock::duration{15s})) - when += 1s; - // Advance the clock without consensus running (IS THIS WHAT - // PREVENTS IT IN PRACTICE?) - sim.scheduler.stepFor(NetClock::time_point{when} - network[0]->now()); - - // Run one more ledger with 30s resolution - sim.run(1); - if (BEAST_EXPECT(sim.synchronized())) - { - // close time should be ahead of clock time since we engineered - // the close time to round up - for (Peer const* peer : network) - { - BEAST_EXPECT(peer->lastClosedLedger.closeTime() > peer->now()); - BEAST_EXPECT(peer->lastClosedLedger.closeAgree()); - } - } - - // All peers submit their own ID as a transaction - for (Peer* peer : network) - peer->submit(Tx{static_cast(peer->id)}); - - // Run 1 more round, this time it will have a decreased - // resolution of 20 seconds. - - // The network delays are engineered so that the slow peers - // initially have the wrong tx hash, but they see a majority - // of agreement from their peers and declare consensus - // - // The trick is that everyone starts with a raw close time of - // 84681s - // Which has - // effCloseTime(86481s, 20s, 86490s) = 86491s - // However, when the slow peers update their position, they change - // the close time to 86451s. The fast peers declare consensus with - // the 86481s as their position still. - // - // When accepted the ledger - // - fast peers use eff(86481s) -> 86491s as the close time - // - slow peers use eff(eff(86481s)) -> eff(86491s) -> 86500s! - - sim.run(1); - - BEAST_EXPECT(sim.synchronized()); - } - - void - testFork() - { - using namespace csf; - using namespace std::chrono; - testcase("fork"); - - std::uint32_t const numPeers = 10; - // Vary overlap between two UNLs - for (std::uint32_t overlap = 0; overlap <= numPeers; ++overlap) - { - ConsensusParms const parms{}; - Sim sim; - - std::uint32_t const numA = (numPeers - overlap) / 2; - std::uint32_t const numB = numPeers - numA - overlap; - - PeerGroup const aOnly = sim.createGroup(numA); - PeerGroup const bOnly = sim.createGroup(numB); - PeerGroup const commonOnly = sim.createGroup(overlap); - - PeerGroup a = aOnly + commonOnly; - PeerGroup b = bOnly + commonOnly; - - PeerGroup const network = a + b; - - SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); - a.trustAndConnect(a, delay); - b.trustAndConnect(b, delay); - - // Initial round to set prior state - sim.run(1); - for (Peer* peer : network) - { - // Nodes have only seen transactions from their neighbors - peer->openTxs.insert(Tx{static_cast(peer->id)}); - for (Peer const* to : sim.trustGraph.trustedPeers(peer)) - peer->openTxs.insert(Tx{static_cast(to->id)}); - } - sim.run(1); - - // Fork should not happen for 40% or greater overlap - // Since the overlapped nodes have a UNL that is the union of the - // two cliques, the maximum sized UNL list is the number of peers - if (overlap > 0.4 * numPeers) - { - BEAST_EXPECT(sim.synchronized()); - } - else - { - // Even if we do fork, there shouldn't be more than 3 ledgers - // One for cliqueA, one for cliqueB and one for nodes in both - BEAST_EXPECT(sim.branches() <= 3); - } - } - } - - void - testHubNetwork() - { - using namespace csf; - using namespace std::chrono; - testcase("hub network"); - - // Simulate a set of 5 validators that aren't directly connected but - // rely on a single hub node for communication - - ConsensusParms const parms{}; - Sim sim; - PeerGroup validators = sim.createGroup(5); - PeerGroup center = sim.createGroup(1); - validators.trust(validators); - center.trust(validators); - - SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); - validators.connect(center, delay); - - center[0]->runAsValidator = false; - - // prep round to set initial state. - sim.run(1); - - // everyone submits their own ID as a TX and relay it to peers - for (Peer* p : validators) - p->submit(Tx(static_cast(p->id))); - - sim.run(1); - - // All peers are in sync - BEAST_EXPECT(sim.synchronized()); - } - - // Helper collector for testPreferredByBranch - // Invasively disconnects network at bad times to cause splits - struct Disruptor - { - csf::PeerGroup& network; - csf::PeerGroup& groupCfast; - csf::PeerGroup& groupCsplit; - csf::SimDuration delay; - bool reconnected = false; - - Disruptor(csf::PeerGroup& net, csf::PeerGroup& c, csf::PeerGroup& split, csf::SimDuration d) - : network(net), groupCfast(c), groupCsplit(split), delay(d) - { - } - - template - void - on(csf::PeerID, csf::SimTime, E const&) - { - } - - void - on(csf::PeerID who, csf::SimTime, csf::FullyValidateLedger const& e) - { - using namespace std::chrono; - // As soon as the fastC node fully validates C, disconnect - // ALL c nodes from the network. The fast C node needs to disconnect - // as well to prevent it from relaying the validations it did see - if (who == groupCfast[0]->id && e.ledger.seq() == csf::Ledger::Seq{2}) - { - network.disconnect(groupCsplit); - network.disconnect(groupCfast); - } - } - - void - on(csf::PeerID who, csf::SimTime, csf::AcceptLedger const& e) - { - // As soon as anyone generates a child of B or C, reconnect the - // network so those validations make it through - if (!reconnected && e.ledger.seq() == csf::Ledger::Seq{3}) - { - reconnected = true; - network.connect(groupCsplit, delay); - } - } - }; - - void - testPreferredByBranch() - { - using namespace csf; - using namespace std::chrono; - testcase("preferred by branch"); - - // Simulate network splits that are prevented from forking when using - // preferred ledger by trie. This is a contrived example that involves - // excessive network splits, but demonstrates the safety improvement - // from the preferred ledger by trie approach. - - // Consider 10 validating nodes that comprise a single common UNL - // Ledger history: - // 1: A - // _/ \_ - // 2: B C - // _/ _/ \_ - // 3: D C' |||||||| (8 different ledgers) - - // - All nodes generate the common ledger A - // - 2 nodes generate B and 8 nodes generate C - // - Only 1 of the C nodes sees all the C validations and fully - // validates C. The rest of the C nodes split at just the right time - // such that they never see any C validations but their own. - // - The C nodes continue and generate 8 different child ledgers. - // - Meanwhile, the D nodes only saw 1 validation for C and 2 - // validations - // for B. - // - The network reconnects and the validations for generation 3 ledgers - // are observed (D and the 8 C's) - // - In the old approach, 2 votes for D outweighs 1 vote for each C' - // so the network would avalanche towards D and fully validate it - // EVEN though C was fully validated by one node - // - In the new approach, 2 votes for D are not enough to outweight the - // 8 implicit votes for C, so nodes will avalanche to C instead - - ConsensusParms const parms{}; - Sim sim; - - // Goes A->B->D - PeerGroup const groupABD = sim.createGroup(2); - // Single node that initially fully validates C before the split - PeerGroup groupCfast = sim.createGroup(1); - // Generates C, but fails to fully validate before the split - PeerGroup groupCsplit = sim.createGroup(7); - - PeerGroup groupNotFastC = groupABD + groupCsplit; - PeerGroup network = groupABD + groupCsplit + groupCfast; - - SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); - SimDuration const fDelay = round(0.1 * parms.ledgerGRANULARITY); - - network.trust(network); - // C must have a shorter delay to see all the validations before the - // other nodes - network.connect(groupCfast, fDelay); - // The rest of the network is connected at the same speed - groupNotFastC.connect(groupNotFastC, delay); - - Disruptor dc(network, groupCfast, groupCsplit, delay); - sim.collectors.add(dc); - - // Consensus round to generate ledger A - sim.run(1); - BEAST_EXPECT(sim.synchronized()); - - // Next round generates B and C - // To force B, we inject an extra transaction in to those nodes - for (Peer* peer : groupABD) - { - peer->txInjections.emplace(peer->lastClosedLedger.seq(), Tx{42}); - } - // The Disruptor will ensure that nodes disconnect before the C - // validations make it to all but the fastC node - sim.run(1); - - // We are no longer in sync, but have not yet forked: - // 9 nodes consider A the last fully validated ledger and fastC sees C - BEAST_EXPECT(!sim.synchronized()); - BEAST_EXPECT(sim.branches() == 1); - - // Run another round to generate the 8 different C' ledgers - for (Peer* p : network) - p->submit(Tx(static_cast(p->id))); - sim.run(1); - - // Still not forked - BEAST_EXPECT(!sim.synchronized()); - BEAST_EXPECT(sim.branches() == 1); - - // Disruptor will reconnect all but the fastC node - sim.run(1); - - if (BEAST_EXPECT(sim.branches() == 1)) - { - BEAST_EXPECT(sim.synchronized()); - } - else // old approach caused a fork - { - BEAST_EXPECT(sim.branches(groupNotFastC) == 1); - BEAST_EXPECT(sim.synchronized(groupNotFastC) == 1); - } - } - - // Helper collector for testPauseForLaggards - // This will remove the ledgerAccept delay used to - // initially create the slow vs. fast validator groups. - struct UndoDelay - { - csf::PeerGroup& g; - - UndoDelay(csf::PeerGroup& a) : g(a) - { - } - - template - void - on(csf::PeerID, csf::SimTime, E const&) - { - } - - void - on(csf::PeerID who, csf::SimTime, csf::AcceptLedger const& e) - { - for (csf::Peer* p : g) - { - if (p->id == who) - p->delays.ledgerAccept = std::chrono::seconds{0}; - } - } - }; - - void - testPauseForLaggards() - { - using namespace csf; - using namespace std::chrono; - testcase("pause for laggards"); - - // Test that validators that jump ahead of the network slow - // down. - - // We engineer the following validated ledger history scenario: - // - // / --> B1 --> C1 --> ... -> G1 "ahead" - // A - // \ --> B2 --> C2 "behind" - // - // After validating a common ledger A, a set of "behind" validators - // briefly run slower and validate the lower chain of ledgers. - // The "ahead" validators run normal speed and run ahead validating the - // upper chain of ledgers. - // - // Due to the uncommitted support definition of the preferred branch - // protocol, even if the "behind" validators are a majority, the "ahead" - // validators cannot jump to the proper branch until the "behind" - // validators catch up to the same sequence number. For this test to - // succeed, the ahead validators need to briefly slow down consensus. - - ConsensusParms const parms{}; - Sim sim; - SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); - - PeerGroup behind = sim.createGroup(3); - PeerGroup const ahead = sim.createGroup(2); - PeerGroup network = ahead + behind; - - hash_set trustedKeys; - for (Peer const* p : network) - trustedKeys.insert(p->key); - for (Peer* p : network) - p->trustedKeys = trustedKeys; - - network.trustAndConnect(network, delay); - - // Initial seed round to set prior state - sim.run(1); - - // Have the "behind" group initially take a really long time to - // accept a ledger after ending deliberation - for (Peer* p : behind) - p->delays.ledgerAccept = 20s; - - // Use the collector to revert the delay after the single - // slow ledger is generated - UndoDelay undoDelay{behind}; - sim.collectors.add(undoDelay); - // Run the simulation for 100 seconds of simulation time with - std::chrono::nanoseconds const simDuration = 100s; - - // Simulate clients submitting 1 tx every 5 seconds to a random - // validator - Rate const rate{.count = 1, .duration = 5s}; - auto peerSelector = makeSelector( - network.begin(), network.end(), std::vector(network.size(), 1.), sim.rng); - auto txSubmitter = makeSubmitter( - ConstantDistribution{rate.inv()}, - sim.scheduler.now(), - sim.scheduler.now() + simDuration, - peerSelector, - sim.scheduler, - sim.rng); - - // Run simulation - sim.run(simDuration); - - // Verify that the network recovered - BEAST_EXPECT(sim.synchronized()); - } - - void - testDisputes() - { - testcase("disputes"); - - using namespace csf; - - // Test dispute objects directly - using Dispute = DisputedTx; - - Tx const txTrue{99}; - Tx const txFalse{98}; - Tx const txFollowingTrue{97}; - Tx const txFollowingFalse{96}; - int const numPeers = 100; - ConsensusParms const p; - std::size_t peersUnchanged = 0; - - auto logs = std::make_unique(beast::Severity::Error); - auto j = logs->journal("Test"); - auto clog = std::make_unique(); - - // Three cases: - // 1 proposing, initial vote yes - // 2 proposing, initial vote no - // 3 not proposing, initial vote doesn't matter after the first update, - // use yes - { - Dispute proposingTrue{txTrue.id(), true, numPeers, journal_}; - Dispute proposingFalse{txFalse.id(), false, numPeers, journal_}; - Dispute followingTrue{txFollowingTrue.id(), true, numPeers, journal_}; - Dispute followingFalse{txFollowingFalse.id(), false, numPeers, journal_}; - BEAST_EXPECT(proposingTrue.id() == 99); - BEAST_EXPECT(proposingFalse.id() == 98); - BEAST_EXPECT(followingTrue.id() == 97); - BEAST_EXPECT(followingFalse.id() == 96); - - // Create an even split in the peer votes - for (int i = 0; i < numPeers; ++i) - { - BEAST_EXPECT(proposingTrue.setVote(PeerID(i), i < 50)); - BEAST_EXPECT(proposingFalse.setVote(PeerID(i), i < 50)); - BEAST_EXPECT(followingTrue.setVote(PeerID(i), i < 50)); - BEAST_EXPECT(followingFalse.setVote(PeerID(i), i < 50)); - } - // Switch the middle vote to match mine - BEAST_EXPECT(proposingTrue.setVote(PeerID(50), true)); - BEAST_EXPECT(proposingFalse.setVote(PeerID(49), false)); - BEAST_EXPECT(followingTrue.setVote(PeerID(50), true)); - BEAST_EXPECT(followingFalse.setVote(PeerID(49), false)); - - // no changes yet - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(clog->str().empty()); - - // I'm in the majority, my vote should not change - BEAST_EXPECT(!proposingTrue.updateVote(5, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(5, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(5, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(5, false, p)); - - BEAST_EXPECT(!proposingTrue.updateVote(10, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(10, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(10, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(10, false, p)); - - peersUnchanged = 2; - BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(clog->str().empty()); - - // Right now, the vote is 51%. The requirement is about to jump to - // 65% - BEAST_EXPECT(proposingTrue.updateVote(55, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(55, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(55, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(55, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == false); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - // 16 validators change their vote to match my original vote - for (int i = 0; i < 16; ++i) - { - auto pTrue = PeerID(numPeers - i - 1); - auto pFalse = PeerID(i); - BEAST_EXPECT(proposingTrue.setVote(pTrue, true)); - BEAST_EXPECT(proposingFalse.setVote(pFalse, false)); - BEAST_EXPECT(followingTrue.setVote(pTrue, true)); - BEAST_EXPECT(followingFalse.setVote(pFalse, false)); - } - // The vote should now be 66%, threshold is 65% - BEAST_EXPECT(proposingTrue.updateVote(60, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(60, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(60, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(60, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // Threshold jumps to 70% - BEAST_EXPECT(proposingTrue.updateVote(86, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(86, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(86, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(86, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == false); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // 5 more validators change their vote to match my original vote - for (int i = 16; i < 21; ++i) - { - auto pTrue = PeerID(numPeers - i - 1); - auto pFalse = PeerID(i); - BEAST_EXPECT(proposingTrue.setVote(pTrue, true)); - BEAST_EXPECT(proposingFalse.setVote(pFalse, false)); - BEAST_EXPECT(followingTrue.setVote(pTrue, true)); - BEAST_EXPECT(followingFalse.setVote(pFalse, false)); - } - - // The vote should now be 71%, threshold is 70% - BEAST_EXPECT(proposingTrue.updateVote(90, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(90, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(90, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(90, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // The vote should now be 71%, threshold is 70% - BEAST_EXPECT(!proposingTrue.updateVote(150, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(150, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(150, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(150, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // The vote should now be 71%, threshold is 70% - BEAST_EXPECT(!proposingTrue.updateVote(190, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(190, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(190, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(190, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - peersUnchanged = 3; - BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(clog->str().empty()); - - // Threshold jumps to 95% - BEAST_EXPECT(proposingTrue.updateVote(220, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(220, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(220, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(220, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == false); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // 25 more validators change their vote to match my original vote - for (int i = 21; i < 46; ++i) - { - auto pTrue = PeerID(numPeers - i - 1); - auto pFalse = PeerID(i); - BEAST_EXPECT(proposingTrue.setVote(pTrue, true)); - BEAST_EXPECT(proposingFalse.setVote(pFalse, false)); - BEAST_EXPECT(followingTrue.setVote(pTrue, true)); - BEAST_EXPECT(followingFalse.setVote(pFalse, false)); - } - - // The vote should now be 96%, threshold is 95% - BEAST_EXPECT(proposingTrue.updateVote(250, true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(250, true, p)); - BEAST_EXPECT(!followingTrue.updateVote(250, false, p)); - BEAST_EXPECT(!followingFalse.updateVote(250, false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - for (peersUnchanged = 0; peersUnchanged < 6; ++peersUnchanged) - { - BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingTrue.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(!followingFalse.stalled(p, false, peersUnchanged, j, clog)); - BEAST_EXPECT(clog->str().empty()); - } - - auto expectStalled = [this, &clog]( - int txid, - bool ourVote, - int ourTime, - int peerTime, - int support, - std::uint32_t line) { - using namespace std::string_literals; - - auto const s = clog->str(); - expect(s.find("stalled"), s, __FILE__, line); - expect(s.starts_with("Transaction "s + std::to_string(txid)), s, __FILE__, line); - expect(s.contains("voting "s + (ourVote ? "YES" : "NO")), s, __FILE__, line); - expect( - s.contains("for "s + std::to_string(ourTime) + " rounds."s), s, __FILE__, line); - expect( - s.contains("votes in "s + std::to_string(peerTime) + " rounds."), - s, - __FILE__, - line); - expect( - s.ends_with("has "s + std::to_string(support) + "% support. "s), - s, - __FILE__, - line); - clog = std::make_unique(); - }; - - for (int i = 0; i < 1; ++i) - { - BEAST_EXPECT(!proposingTrue.updateVote(250 + (10 * i), true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(250 + (10 * i), true, p)); - BEAST_EXPECT(!followingTrue.updateVote(250 + (10 * i), false, p)); - BEAST_EXPECT(!followingFalse.updateVote(250 + (10 * i), false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // true vote has changed recently, so not stalled - BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog)); - BEAST_EXPECT(clog->str().empty()); - // remaining votes have been unchanged in so long that we only - // need to hit the second round at 95% to be stalled, regardless - // of peers - BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog)); - expectStalled(98, false, 11, 0, 2, __LINE__); - BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog)); - expectStalled(97, true, 11, 0, 97, __LINE__); - BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog)); - expectStalled(96, false, 11, 0, 3, __LINE__); - - // true vote has changed recently, so not stalled - BEAST_EXPECT(!proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - BEAST_EXPECTS(clog->str().empty(), clog->str()); - // remaining votes have been unchanged in so long that we only - // need to hit the second round at 95% to be stalled, regardless - // of peers - BEAST_EXPECT(proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - expectStalled(98, false, 11, 6, 2, __LINE__); - BEAST_EXPECT(followingTrue.stalled(p, false, peersUnchanged, j, clog)); - expectStalled(97, true, 11, 6, 97, __LINE__); - BEAST_EXPECT(followingFalse.stalled(p, false, peersUnchanged, j, clog)); - expectStalled(96, false, 11, 6, 3, __LINE__); - } - for (int i = 1; i < 3; ++i) - { - BEAST_EXPECT(!proposingTrue.updateVote(250 + (10 * i), true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(250 + (10 * i), true, p)); - BEAST_EXPECT(!followingTrue.updateVote(250 + (10 * i), false, p)); - BEAST_EXPECT(!followingFalse.updateVote(250 + (10 * i), false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - // true vote changed 2 rounds ago, and peers are changing, so - // not stalled - BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog)); - BEAST_EXPECTS(clog->str().empty(), clog->str()); - // still stalled - BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog)); - expectStalled(98, false, 11 + i, 0, 2, __LINE__); - BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog)); - expectStalled(97, true, 11 + i, 0, 97, __LINE__); - BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog)); - expectStalled(96, false, 11 + i, 0, 3, __LINE__); - - // true vote changed 2 rounds ago, and peers are NOT changing, - // so stalled - BEAST_EXPECT(proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - expectStalled(99, true, 1 + i, 6, 97, __LINE__); - // still stalled - BEAST_EXPECT(proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - expectStalled(98, false, 11 + i, 6, 2, __LINE__); - BEAST_EXPECT(followingTrue.stalled(p, false, peersUnchanged, j, clog)); - expectStalled(97, true, 11 + i, 6, 97, __LINE__); - BEAST_EXPECT(followingFalse.stalled(p, false, peersUnchanged, j, clog)); - expectStalled(96, false, 11 + i, 6, 3, __LINE__); - } - for (int i = 3; i < 5; ++i) - { - BEAST_EXPECT(!proposingTrue.updateVote(250 + (10 * i), true, p)); - BEAST_EXPECT(!proposingFalse.updateVote(250 + (10 * i), true, p)); - BEAST_EXPECT(!followingTrue.updateVote(250 + (10 * i), false, p)); - BEAST_EXPECT(!followingFalse.updateVote(250 + (10 * i), false, p)); - - BEAST_EXPECT(proposingTrue.getOurVote() == true); - BEAST_EXPECT(proposingFalse.getOurVote() == false); - BEAST_EXPECT(followingTrue.getOurVote() == true); - BEAST_EXPECT(followingFalse.getOurVote() == false); - - BEAST_EXPECT(proposingTrue.stalled(p, true, 0, j, clog)); - expectStalled(99, true, 1 + i, 0, 97, __LINE__); - BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog)); - expectStalled(98, false, 11 + i, 0, 2, __LINE__); - BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog)); - expectStalled(97, true, 11 + i, 0, 97, __LINE__); - BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog)); - expectStalled(96, false, 11 + i, 0, 3, __LINE__); - - BEAST_EXPECT(proposingTrue.stalled(p, true, peersUnchanged, j, clog)); - expectStalled(99, true, 1 + i, 6, 97, __LINE__); - BEAST_EXPECT(proposingFalse.stalled(p, true, peersUnchanged, j, clog)); - expectStalled(98, false, 11 + i, 6, 2, __LINE__); - BEAST_EXPECT(followingTrue.stalled(p, false, peersUnchanged, j, clog)); - expectStalled(97, true, 11 + i, 6, 97, __LINE__); - BEAST_EXPECT(followingFalse.stalled(p, false, peersUnchanged, j, clog)); - expectStalled(96, false, 11 + i, 6, 3, __LINE__); - } - } - } - - void - run() override - { - testShouldCloseLedger(); - testCheckConsensus(); - - testStandalone(); - testPeersAgree(); - testSlowPeers(); - testCloseTimeDisagree(); - testWrongLCL(); - testConsensusCloseTimeRounding(); - testFork(); - testHubNetwork(); - testPreferredByBranch(); - testPauseForLaggards(); - testDisputes(); - } -}; - -BEAST_DEFINE_TESTSUITE(Consensus, consensus, xrpl); -} // namespace xrpl::test diff --git a/src/test/consensus/DistributedValidatorsSim_test.cpp b/src/test/consensus/DistributedValidatorsSim_test.cpp deleted file mode 100644 index 437ad81ee0..0000000000 --- a/src/test/consensus/DistributedValidatorsSim_test.cpp +++ /dev/null @@ -1,253 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -/** - * In progress simulations for diversifying and distributing validators - */ -class DistributedValidators_test : public beast::unit_test::Suite -{ - void - completeTrustCompleteConnectFixedDelay( - std::size_t numPeers, - std::chrono::milliseconds delay = std::chrono::milliseconds(200), - bool printHeaders = false) - { - using namespace csf; - using namespace std::chrono; - - // Initialize persistent collector logs specific to this method - std::string const prefix = - "DistributedValidators_" - "completeTrustCompleteConnectFixedDelay"; - std::fstream txLog(prefix + "_tx.csv", std::ofstream::app), - ledgerLog(prefix + "_ledger.csv", std::ofstream::app); - - // title - log << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl; - - // number of peers, UNLs, connections - BEAST_EXPECT(numPeers >= 1); - - Sim sim; - PeerGroup peers = sim.createGroup(numPeers); - - // complete trust graph - peers.trust(peers); - - // complete connect graph with fixed delay - peers.connect(peers, delay); - - // Initialize collectors to track statistics to report - TxCollector txCollector; - LedgerCollector ledgerCollector; - auto colls = makeCollectors(txCollector, ledgerCollector); - sim.collectors.add(colls); - - // Initial round to set prior state - sim.run(1); - - // Run for 10 minutes, submitting 100 tx/second - std::chrono::nanoseconds const simDuration = 10min; - std::chrono::nanoseconds const quiet = 10s; - Rate const rate{.count = 100, .duration = 1000ms}; - - // Initialize timers - HeartbeatTimer heart(sim.scheduler); - - // txs, start/stop/step, target - auto peerSelector = - makeSelector(peers.begin(), peers.end(), std::vector(numPeers, 1.), sim.rng); - auto txSubmitter = makeSubmitter( - ConstantDistribution{rate.inv()}, - sim.scheduler.now() + quiet, - sim.scheduler.now() + simDuration - quiet, - peerSelector, - sim.scheduler, - sim.rng); - - // run simulation for given duration - heart.start(); - sim.run(simDuration); - - // BEAST_EXPECT(sim.branches() == 1); - // BEAST_EXPECT(sim.synchronized()); - - log << std::right; - log << "| Peers: " << std::setw(2) << peers.size(); - log << " | Duration: " << std::setw(6) << duration_cast(simDuration).count() - << " ms"; - log << " | Branches: " << std::setw(1) << sim.branches(); - log << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N"); - log << " |" << std::endl; - - txCollector.report(simDuration, log, true); - ledgerCollector.report(simDuration, log, false); - - std::string const tag = std::to_string(numPeers); - txCollector.csv(simDuration, txLog, tag, printHeaders); - ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders); - - log << std::endl; - } - - void - completeTrustScaleFreeConnectFixedDelay( - std::size_t numPeers, - std::chrono::milliseconds delay = std::chrono::milliseconds(200), - bool printHeaders = false) - { - using namespace csf; - using namespace std::chrono; - - // Initialize persistent collector logs specific to this method - std::string const prefix = - "DistributedValidators__" - "completeTrustScaleFreeConnectFixedDelay"; - std::fstream txLog(prefix + "_tx.csv", std::ofstream::app), - ledgerLog(prefix + "_ledger.csv", std::ofstream::app); - - // title - log << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl; - - // number of peers, UNLs, connections - int const numCNLs = std::max(int(1.00 * numPeers), 1); - int const minCNLSize = std::max(int(0.25 * numCNLs), 1); - int const maxCNLSize = std::max(int(0.50 * numCNLs), 1); - BEAST_EXPECT(numPeers >= 1); - BEAST_EXPECT(numCNLs >= 1); - BEAST_EXPECT(1 <= minCNLSize && minCNLSize <= maxCNLSize && maxCNLSize <= numPeers); - - Sim sim; - PeerGroup peers = sim.createGroup(numPeers); - - // complete trust graph - peers.trust(peers); - - // scale-free connect graph with fixed delay - std::vector const ranks = sample(peers.size(), PowerLawDistribution{1, 3}, sim.rng); - randomRankedConnect( - peers, - ranks, - numCNLs, - std::uniform_int_distribution<>{minCNLSize, maxCNLSize}, - sim.rng, - delay); - - // Initialize collectors to track statistics to report - TxCollector txCollector; - LedgerCollector ledgerCollector; - auto colls = makeCollectors(txCollector, ledgerCollector); - sim.collectors.add(colls); - - // Initial round to set prior state - sim.run(1); - - // Run for 10 minutes, submitting 100 tx/second - std::chrono::nanoseconds const simDuration = 10min; - std::chrono::nanoseconds const quiet = 10s; - Rate const rate{.count = 100, .duration = 1000ms}; - - // Initialize timers - HeartbeatTimer heart(sim.scheduler); - - // txs, start/stop/step, target - auto peerSelector = - makeSelector(peers.begin(), peers.end(), std::vector(numPeers, 1.), sim.rng); - auto txSubmitter = makeSubmitter( - ConstantDistribution{rate.inv()}, - sim.scheduler.now() + quiet, - sim.scheduler.now() + simDuration - quiet, - peerSelector, - sim.scheduler, - sim.rng); - - // run simulation for given duration - heart.start(); - sim.run(simDuration); - - // BEAST_EXPECT(sim.branches() == 1); - // BEAST_EXPECT(sim.synchronized()); - - log << std::right; - log << "| Peers: " << std::setw(2) << peers.size(); - log << " | Duration: " << std::setw(6) << duration_cast(simDuration).count() - << " ms"; - log << " | Branches: " << std::setw(1) << sim.branches(); - log << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N"); - log << " |" << std::endl; - - txCollector.report(simDuration, log, true); - ledgerCollector.report(simDuration, log, false); - - std::string const tag = std::to_string(numPeers); - txCollector.csv(simDuration, txLog, tag, printHeaders); - ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders); - - log << std::endl; - } - - void - run() override - { - std::string const defaultArgs = "5 200"; - std::string const args = arg().empty() ? defaultArgs : arg(); - std::stringstream argStream(args); - - int maxNumValidators = 0; - int delayCount(200); - argStream >> maxNumValidators; - argStream >> delayCount; - - std::chrono::milliseconds const delay(delayCount); - - log << "DistributedValidators: 1 to " << maxNumValidators << " Peers" << std::endl; - - /** - * Simulate with N = 1 to N - * - complete trust graph is complete - * - complete network connectivity - * - fixed delay for network links - */ - completeTrustCompleteConnectFixedDelay(1, delay, true); - for (int i = 2; i <= maxNumValidators; i++) - { - completeTrustCompleteConnectFixedDelay(i, delay); - } - - /** - * Simulate with N = 1 to N - * - complete trust graph is complete - * - scale-free network connectivity - * - fixed delay for network links - */ - completeTrustScaleFreeConnectFixedDelay(1, delay, true); - for (int i = 2; i <= maxNumValidators; i++) - { - completeTrustScaleFreeConnectFixedDelay(i, delay); - } - } -}; - -BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(DistributedValidators, consensus, xrpl, 2); - -} // namespace xrpl::test diff --git a/src/test/consensus/LedgerTiming_test.cpp b/src/test/consensus/LedgerTiming_test.cpp deleted file mode 100644 index 632361c799..0000000000 --- a/src/test/consensus/LedgerTiming_test.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -namespace xrpl::test { - -class LedgerTiming_test : public beast::unit_test::Suite -{ - void - testGetNextLedgerTimeResolution() - { - // helper to iteratively call into getNextLedgerTimeResolution - struct TestRes - { - std::uint32_t decrease = 0; - std::uint32_t equal = 0; - std::uint32_t increase = 0; - - static TestRes - run(bool previousAgree, std::uint32_t rounds) - { - TestRes res; - auto closeResolution = kLedgerDefaultTimeResolution; - auto nextCloseResolution = closeResolution; - std::uint32_t round = 0; - do - { - nextCloseResolution = - getNextLedgerTimeResolution(closeResolution, previousAgree, ++round); - if (nextCloseResolution < closeResolution) - { - ++res.decrease; - } - else if (nextCloseResolution > closeResolution) - { - ++res.increase; - } - else - { - ++res.equal; - } - std::swap(nextCloseResolution, closeResolution); - } while (round < rounds); - return res; - } - }; - - // If we never agree on close time, only can increase resolution - // until hit the max - auto decreases = TestRes::run(false, 10); - BEAST_EXPECT(decreases.increase == 3); - BEAST_EXPECT(decreases.decrease == 0); - BEAST_EXPECT(decreases.equal == 7); - - // If we always agree on close time, only can decrease resolution - // until hit the min - auto increases = TestRes::run(false, 100); - BEAST_EXPECT(increases.increase == 3); - BEAST_EXPECT(increases.decrease == 0); - BEAST_EXPECT(increases.equal == 97); - } - - void - testRoundCloseTime() - { - using namespace std::chrono_literals; - // A closeTime equal to the epoch is not modified - using tp = NetClock::time_point; - tp const def; - BEAST_EXPECT(def == roundCloseTime(def, 30s)); - - // Otherwise, the closeTime is rounded to the nearest - // rounding up on ties - BEAST_EXPECT(tp{0s} == roundCloseTime(tp{29s}, 60s)); - BEAST_EXPECT(tp{30s} == roundCloseTime(tp{30s}, 1s)); - BEAST_EXPECT(tp{60s} == roundCloseTime(tp{31s}, 60s)); - BEAST_EXPECT(tp{60s} == roundCloseTime(tp{30s}, 60s)); - BEAST_EXPECT(tp{60s} == roundCloseTime(tp{59s}, 60s)); - BEAST_EXPECT(tp{60s} == roundCloseTime(tp{60s}, 60s)); - BEAST_EXPECT(tp{60s} == roundCloseTime(tp{61s}, 60s)); - } - - void - testEffCloseTime() - { - using namespace std::chrono_literals; - using tp = NetClock::time_point; - tp close = effCloseTime(tp{10s}, 30s, tp{0s}); - BEAST_EXPECT(close == tp{1s}); - - close = effCloseTime(tp{16s}, 30s, tp{0s}); - BEAST_EXPECT(close == tp{30s}); - - close = effCloseTime(tp{16s}, 30s, tp{30s}); - BEAST_EXPECT(close == tp{31s}); - - close = effCloseTime(tp{16s}, 30s, tp{60s}); - BEAST_EXPECT(close == tp{61s}); - - close = effCloseTime(tp{31s}, 30s, tp{0s}); - BEAST_EXPECT(close == tp{30s}); - } - - void - run() override - { - testGetNextLedgerTimeResolution(); - testRoundCloseTime(); - testEffCloseTime(); - } -}; - -BEAST_DEFINE_TESTSUITE(LedgerTiming, consensus, xrpl); -} // namespace xrpl::test diff --git a/src/test/consensus/LedgerTrie_test.cpp b/src/test/consensus/LedgerTrie_test.cpp deleted file mode 100644 index a4eb7bc087..0000000000 --- a/src/test/consensus/LedgerTrie_test.cpp +++ /dev/null @@ -1,716 +0,0 @@ -#include - -#include - -#include - -#include -#include -#include - -namespace xrpl::test { - -class LedgerTrie_test : public beast::unit_test::Suite -{ - void - testInsert() - { - using namespace csf; - // Single entry by itself - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); - - t.insert(h["abc"]); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - } - // Suffix of existing (extending tree) - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - BEAST_EXPECT(t.checkInvariants()); - // extend with no siblings - t.insert(h["abcd"]); - BEAST_EXPECT(t.checkInvariants()); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); - - // extend with existing sibling - t.insert(h["abce"]); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abce"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abce"]) == 1); - } - // uncommitted of existing node - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abcd"]); - BEAST_EXPECT(t.checkInvariants()); - // uncommitted with no siblings - t.insert(h["abcdf"]); - BEAST_EXPECT(t.checkInvariants()); - - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abcdf"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcdf"]) == 1); - - // uncommitted with existing child - t.insert(h["abc"]); - BEAST_EXPECT(t.checkInvariants()); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abcdf"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcdf"]) == 1); - } - // Suffix + uncommitted of existing node - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abcd"]); - BEAST_EXPECT(t.checkInvariants()); - t.insert(h["abce"]); - BEAST_EXPECT(t.checkInvariants()); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abce"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abce"]) == 1); - } - // Suffix + uncommitted with existing child - { - // abcd : abcde, abcf - - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abcd"]); - BEAST_EXPECT(t.checkInvariants()); - t.insert(h["abcde"]); - BEAST_EXPECT(t.checkInvariants()); - t.insert(h["abcf"]); - BEAST_EXPECT(t.checkInvariants()); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abcf"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcf"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abcde"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcde"]) == 1); - } - - // Multiple counts - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["ab"], 4); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 4); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 4); - BEAST_EXPECT(t.tipSupport(h["a"]) == 0); - BEAST_EXPECT(t.branchSupport(h["a"]) == 4); - - t.insert(h["abc"], 2); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 4); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 6); - BEAST_EXPECT(t.tipSupport(h["a"]) == 0); - BEAST_EXPECT(t.branchSupport(h["a"]) == 6); - } - } - - void - testRemove() - { - using namespace csf; - // Not in trie - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - - BEAST_EXPECT(!t.remove(h["ab"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(!t.remove(h["a"])); - BEAST_EXPECT(t.checkInvariants()); - } - // In trie but with 0 tip support - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abcd"]); - t.insert(h["abce"]); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - BEAST_EXPECT(!t.remove(h["abc"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - } - // In trie with > 1 tip support - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"], 2); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); - BEAST_EXPECT(t.remove(h["abc"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - - t.insert(h["abc"], 1); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 2); - BEAST_EXPECT(t.remove(h["abc"], 2)); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - - t.insert(h["abc"], 3); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 3); - BEAST_EXPECT(t.remove(h["abc"], 300)); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - } - // In trie with = 1 tip support, no children - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["ab"]); - t.insert(h["abc"]); - - BEAST_EXPECT(t.tipSupport(h["ab"]) == 1); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); - - BEAST_EXPECT(t.remove(h["abc"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 1); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 0); - } - // In trie with = 1 tip support, 1 child - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["ab"]); - t.insert(h["abc"]); - t.insert(h["abcd"]); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); - - BEAST_EXPECT(t.remove(h["abc"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 1); - } - // In trie with = 1 tip support, > 1 children - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["ab"]); - t.insert(h["abc"]); - t.insert(h["abcd"]); - t.insert(h["abce"]); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 3); - - BEAST_EXPECT(t.remove(h["abc"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 2); - } - - // In trie with = 1 tip support, parent compaction - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["ab"]); - t.insert(h["abc"]); - t.insert(h["abd"]); - BEAST_EXPECT(t.checkInvariants()); - t.remove(h["ab"]); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abd"]) == 1); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 2); - - t.remove(h["abd"]); - BEAST_EXPECT(t.checkInvariants()); - - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); - } - } - - void - testEmpty() - { - using namespace csf; - LedgerTrie t; - LedgerHistoryHelper h; - BEAST_EXPECT(t.empty()); - - Ledger const genesis = h[""]; - t.insert(genesis); - BEAST_EXPECT(!t.empty()); - t.remove(genesis); - BEAST_EXPECT(t.empty()); - - t.insert(h["abc"]); - BEAST_EXPECT(!t.empty()); - t.remove(h["abc"]); - BEAST_EXPECT(t.empty()); - } - - void - testSupport() - { - using namespace csf; - - LedgerTrie t; - LedgerHistoryHelper h; - BEAST_EXPECT(t.tipSupport(h["a"]) == 0); - BEAST_EXPECT(t.tipSupport(h["axy"]) == 0); - - BEAST_EXPECT(t.branchSupport(h["a"]) == 0); - BEAST_EXPECT(t.branchSupport(h["axy"]) == 0); - - t.insert(h["abc"]); - BEAST_EXPECT(t.tipSupport(h["a"]) == 0); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abcd"]) == 0); - - BEAST_EXPECT(t.branchSupport(h["a"]) == 1); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abcd"]) == 0); - - t.insert(h["abe"]); - BEAST_EXPECT(t.tipSupport(h["a"]) == 0); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 1); - BEAST_EXPECT(t.tipSupport(h["abe"]) == 1); - - BEAST_EXPECT(t.branchSupport(h["a"]) == 2); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 2); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abe"]) == 1); - - t.remove(h["abc"]); - BEAST_EXPECT(t.tipSupport(h["a"]) == 0); - BEAST_EXPECT(t.tipSupport(h["ab"]) == 0); - BEAST_EXPECT(t.tipSupport(h["abc"]) == 0); - BEAST_EXPECT(t.tipSupport(h["abe"]) == 1); - - BEAST_EXPECT(t.branchSupport(h["a"]) == 1); - BEAST_EXPECT(t.branchSupport(h["ab"]) == 1); - BEAST_EXPECT(t.branchSupport(h["abc"]) == 0); - BEAST_EXPECT(t.branchSupport(h["abe"]) == 1); - } - - void - testGetPreferred() - { - using namespace csf; - using Seq = Ledger::Seq; - // Empty - { - LedgerTrie const t; - BEAST_EXPECT(t.getPreferred(Seq{0}) == std::nullopt); - BEAST_EXPECT(t.getPreferred(Seq{2}) == std::nullopt); - } - // Genesis support is NOT empty - { - LedgerTrie t; - LedgerHistoryHelper h; - Ledger const genesis = h[""]; - t.insert(genesis); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{0})->id == genesis.id()); - BEAST_EXPECT(t.remove(genesis)); - BEAST_EXPECT(t.getPreferred(Seq{0}) == std::nullopt); - BEAST_EXPECT(!t.remove(genesis)); - } - // Single node no children - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - } - // Single node smaller child support - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcd"]); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id()); - } - // Single node larger child - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcd"], 2); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcd"].id()); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abcd"].id()); - } - // Single node smaller children support - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcd"]); - t.insert(h["abce"]); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id()); - - t.insert(h["abc"]); - - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - // Single node larger children - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcd"], 2); - t.insert(h["abce"]); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id()); - - t.insert(h["abcd"]); - - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcd"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abcd"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - // Tie-breaker by id - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abcd"], 2); - t.insert(h["abce"], 2); - - BEAST_EXPECT(h["abce"].id() > h["abcd"].id()); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abce"].id()); - - t.insert(h["abcd"]); - BEAST_EXPECT(h["abce"].id() > h["abcd"].id()); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abcd"].id()); - } - - // Tie-breaker not needed - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcd"]); - t.insert(h["abce"], 2); - // abce only has a margin of 1, but it owns the tie-breaker - BEAST_EXPECT(h["abce"].id() > h["abcd"].id()); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abce"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abce"].id()); - - // Switch support from abce to abcd, tie-breaker now needed - t.remove(h["abce"]); - t.insert(h["abcd"]); - - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - - // Single node larger grand child - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcd"], 2); - t.insert(h["abcde"], 4); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcde"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abcde"].id()); - BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["abcde"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - - // Too much uncommitted support from competing branches - { - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["abc"]); - t.insert(h["abcde"], 2); - t.insert(h["abcfg"], 2); - // 'de' and 'fg' are tied without 'abc' vote - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abc"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abc"].id()); - BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["abc"].id()); - - t.remove(h["abc"]); - t.insert(h["abcd"]); - - // 'de' branch has 3 votes to 2, so earlier sequences see it as preferred - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abcde"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["abcde"].id()); - - // However, if you validated a ledger with Seq 5, potentially on - // a different branch, you do not yet know if they chose abcd - // or abcf because of you, so abc remains preferred - BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["abc"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - - // Changing largestSeq perspective changes preferred branch - { - /** - * Build the tree below with initial tip support annotated - * A - * / \ - * B(1) C(1) - * / | | - * H D F(1) - * | - * E(2) - * | - * G - */ - LedgerTrie t; - LedgerHistoryHelper h; - t.insert(h["ab"]); - t.insert(h["ac"]); - t.insert(h["acf"]); - t.insert(h["abde"], 2); - - // B has more branch support - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["ab"].id()); - - // But if you last validated D,F or E, you do not yet know - // if someone used that validation to commit to B or C - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["a"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["a"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - - /** - * One of E advancing to G doesn't change anything - * A - * / \ - * B(1) C(1) - * / | | - * H D F(1) - * | - * E(1) - * | - * G(1) - */ - t.remove(h["abde"]); - t.insert(h["abdeg"]); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["a"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["a"].id()); - BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["a"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - - /** - * C advancing to H does advance the seq 3 preferred ledger - * A - * / \ - * B(1) C - * / | | - * H(1)D F(1) - * | - * E(1) - * | - * G(1) - */ - t.remove(h["ac"]); - t.insert(h["abh"]); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["a"].id()); - BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["a"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - - /** - * F advancing to E also moves the preferred ledger forward - * A - * / \ - * B(1) C - * / | | - * H(1)D F - * | - * E(2) - * | - * G(1) - */ - t.remove(h["acf"]); - t.insert(h["abde"]); - - // NOLINTBEGIN(bugprone-unchecked-optional-access) - BEAST_EXPECT(t.getPreferred(Seq{1})->id == h["abde"].id()); - BEAST_EXPECT(t.getPreferred(Seq{2})->id == h["abde"].id()); - BEAST_EXPECT(t.getPreferred(Seq{3})->id == h["abde"].id()); - BEAST_EXPECT(t.getPreferred(Seq{4})->id == h["ab"].id()); - BEAST_EXPECT(t.getPreferred(Seq{5})->id == h["ab"].id()); - // NOLINTEND(bugprone-unchecked-optional-access) - } - } - - void - testRootRelated() - { - using namespace csf; - // Since the root is a special node that breaks the no-single child - // invariant, do some tests that exercise it. - - LedgerTrie t; - LedgerHistoryHelper h; - BEAST_EXPECT(!t.remove(h[""])); - BEAST_EXPECT(t.branchSupport(h[""]) == 0); - BEAST_EXPECT(t.tipSupport(h[""]) == 0); - - t.insert(h["a"]); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.branchSupport(h[""]) == 1); - BEAST_EXPECT(t.tipSupport(h[""]) == 0); - - t.insert(h["e"]); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.branchSupport(h[""]) == 2); - BEAST_EXPECT(t.tipSupport(h[""]) == 0); - - BEAST_EXPECT(t.remove(h["e"])); - BEAST_EXPECT(t.checkInvariants()); - BEAST_EXPECT(t.branchSupport(h[""]) == 1); - BEAST_EXPECT(t.tipSupport(h[""]) == 0); - } - - void - testStress() - { - using namespace csf; - LedgerTrie t; - LedgerHistoryHelper h; - - // Test quasi-randomly add/remove supporting for different ledgers - // from a branching history. - - // Ledgers have sequence 1,2,3,4 - std::uint32_t const depthConst = 4; - // Each ledger has 4 possible children - std::uint32_t const width = 4; - - std::uint32_t const iterations = 10000; - - // Use explicit seed to have same results for CI - // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test - std::mt19937 gen{42}; - std::uniform_int_distribution<> depthDist(0, depthConst - 1); - std::uniform_int_distribution<> widthDist(0, width - 1); - std::uniform_int_distribution<> flip(0, 1); - for (std::uint32_t i = 0; i < iterations; ++i) - { - // pick a random ledger history - std::string curr; - char const depth = depthDist(gen); - char offset = 0; - for (char d = 0; d < depth; ++d) - { - char const a = offset + widthDist(gen); - curr += a; - offset = (a + 1) * width; - } - - // 50-50 to add remove - if (flip(gen) == 0) - { - t.insert(h[curr]); - } - else - { - t.remove(h[curr]); - } - if (!BEAST_EXPECT(t.checkInvariants())) - return; - } - } - - void - run() override - { - testInsert(); - testRemove(); - testEmpty(); - testSupport(); - testGetPreferred(); - testRootRelated(); - testStress(); - } -}; - -BEAST_DEFINE_TESTSUITE(LedgerTrie, consensus, xrpl); -} // namespace xrpl::test diff --git a/src/test/consensus/RCLCensorshipDetector_test.cpp b/src/test/consensus/RCLCensorshipDetector_test.cpp deleted file mode 100644 index 722a34f937..0000000000 --- a/src/test/consensus/RCLCensorshipDetector_test.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include - -#include - -#include -#include -#include - -namespace xrpl::test { - -class RCLCensorshipDetector_test : public beast::unit_test::Suite -{ - void - test( - RCLCensorshipDetector& cdet, - int round, - std::vector proposed, - std::vector accepted, - std::vector remain, - std::vector remove) - { - // Begin tracking what we're proposing this round - RCLCensorshipDetector::TxIDSeqVec proposal; - for (auto const& i : proposed) - proposal.emplace_back(i, round); - cdet.propose(std::move(proposal)); - - // Finalize the round, by processing what we accepted; then - // remove anything that needs to be removed and ensure that - // what remains is correct. - cdet.check(std::move(accepted), [&remove, &remain](auto id, auto seq) { - // If the item is supposed to be removed from the censorship - // detector internal tracker manually, do it now: - if (std::ranges::find(remove, id) != remove.end()) - return true; - - // If the item is supposed to still remain in the censorship - // detector internal tracker; remove it from the vector. - auto it = std::ranges::find(remain, id); - if (it != remain.end()) - remain.erase(it); - return false; - }); - - // On entry, this set contained all the elements that should be tracked - // by the detector after we process this round. We removed all the items - // that actually were in the tracker, so this should now be empty: - BEAST_EXPECT(remain.empty()); - } - -public: - void - run() override - { - testcase("Censorship Detector"); - - RCLCensorshipDetector cdet; - int round = 0; - // proposed accepted remain remove - test(cdet, ++round, {}, {}, {}, {}); - test(cdet, ++round, {10, 11, 12, 13}, {11, 2}, {10, 13}, {}); - test(cdet, ++round, {10, 13, 14, 15}, {14}, {10, 13, 15}, {}); - test(cdet, ++round, {10, 13, 15, 16}, {15, 16}, {10, 13}, {}); - test(cdet, ++round, {10, 13}, {17, 18}, {10, 13}, {}); - test(cdet, ++round, {10, 19}, {}, {10, 19}, {}); - test(cdet, ++round, {10, 19, 20}, {20}, {10}, {19}); - test(cdet, ++round, {21}, {21}, {}, {}); - test(cdet, ++round, {}, {22}, {}, {}); - test(cdet, ++round, {23, 24, 25, 26}, {25, 27}, {23, 26}, {24}); - test(cdet, ++round, {23, 26, 28}, {26, 28}, {23}, {}); - - for (int i = 0; i != 10; ++i) - test(cdet, ++round, {23}, {}, {23}, {}); - - test(cdet, ++round, {23, 29}, {29}, {23}, {}); - test(cdet, ++round, {30, 31}, {31}, {30}, {}); - test(cdet, ++round, {30}, {30}, {}, {}); - test(cdet, ++round, {}, {}, {}, {}); - } -}; - -BEAST_DEFINE_TESTSUITE(RCLCensorshipDetector, consensus, xrpl); -} // namespace xrpl::test diff --git a/src/test/consensus/ScaleFreeSim_test.cpp b/src/test/consensus/ScaleFreeSim_test.cpp deleted file mode 100644 index e533e09eb0..0000000000 --- a/src/test/consensus/ScaleFreeSim_test.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include - -namespace xrpl::test { - -class ScaleFreeSim_test : public beast::unit_test::Suite -{ - void - run() override - { - using namespace std::chrono; - using namespace csf; - - // Generate a quasi-random scale free network and simulate consensus - // as we vary transaction submission rates - - int const n = 100; // Peers - - int const numUNLs = 15; // UNL lists - int const minUNLSize = n / 4, maxUNLSize = n / 2; - - ConsensusParms const parms{}; - Sim sim; - PeerGroup network = sim.createGroup(n); - - // generate trust ranks - std::vector const ranks = - sample(network.size(), PowerLawDistribution{1, 3}, sim.rng); - - // generate scale-free trust graph - randomRankedTrust( - network, - ranks, - numUNLs, - std::uniform_int_distribution<>{minUNLSize, maxUNLSize}, - sim.rng); - - // nodes with a trust line in either direction are network-connected - network.connectFromTrust(round(0.2 * parms.ledgerGRANULARITY)); - - // Initialize collectors to track statistics to report - TxCollector txCollector; - LedgerCollector ledgerCollector; - auto colls = makeCollectors(txCollector, ledgerCollector); - sim.collectors.add(colls); - - // Initial round to set prior state - sim.run(1); - - // Initialize timers - HeartbeatTimer heart(sim.scheduler, seconds(10s)); - - // Run for 10 minutes, submitting 100 tx/second - std::chrono::nanoseconds const simDuration = 10min; - std::chrono::nanoseconds const quiet = 10s; - Rate const rate{.count = 100, .duration = 1000ms}; - - // txs, start/stop/step, target - auto peerSelector = makeSelector(network.begin(), network.end(), ranks, sim.rng); - auto txSubmitter = makeSubmitter( - ConstantDistribution{rate.inv()}, - sim.scheduler.now() + quiet, - sim.scheduler.now() + (simDuration - quiet), - peerSelector, - sim.scheduler, - sim.rng); - - // run simulation for given duration - heart.start(); - sim.run(simDuration); - - BEAST_EXPECT(sim.branches() == 1); - BEAST_EXPECT(sim.synchronized()); - - // TODO: Clean up this formatting mess!! - - log << "Peers: " << network.size() << std::endl; - log << "Simulated Duration: " << duration_cast(simDuration).count() << " ms" - << std::endl; - log << "Branches: " << sim.branches() << std::endl; - log << "Synchronized: " << (sim.synchronized() ? "Y" : "N") << std::endl; - log << std::endl; - - txCollector.report(simDuration, log); - ledgerCollector.report(simDuration, log); - // Print summary? - // # forks? # of LCLs? - // # peers - // # tx submitted - // # ledgers/sec etc.? - } -}; - -BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(ScaleFreeSim, consensus, xrpl, 80); - -} // namespace xrpl::test diff --git a/src/test/consensus/Validations_test.cpp b/src/test/consensus/Validations_test.cpp deleted file mode 100644 index 606c6f2824..0000000000 --- a/src/test/consensus/Validations_test.cpp +++ /dev/null @@ -1,1059 +0,0 @@ -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test::csf { -class Validations_test : public beast::unit_test::Suite -{ - using clock_type = beast::AbstractClock const; - - // Helper to convert steady_clock to a reasonable NetClock - // This allows a single manual clock in the unit tests - static NetClock::time_point - toNetClock(clock_type const& c) - { - // We don't care about the actual epochs, but do want the - // generated NetClock time to be well past its epoch to ensure - // any subtractions are positive - using namespace std::chrono; - return NetClock::time_point( - duration_cast(c.now().time_since_epoch() + 86400s)); - } - - // Represents a node that can issue validations - class Node - { - clock_type const& c_; - PeerID nodeID_; - bool trusted_ = true; - std::size_t signIdx_{1}; - std::optional loadFee_; - - public: - Node(PeerID nodeID, clock_type const& c) : c_(c), nodeID_(nodeID) - { - } - - void - untrust() - { - trusted_ = false; - } - - void - trust() - { - trusted_ = true; - } - - void - setLoadFee(std::uint32_t fee) - { - loadFee_ = fee; - } - - [[nodiscard]] PeerID - nodeID() const - { - return nodeID_; - } - - void - advanceKey() - { - signIdx_++; - } - - [[nodiscard]] PeerKey - currKey() const - { - return std::make_pair(nodeID_, signIdx_); - } - - [[nodiscard]] PeerKey - masterKey() const - { - return std::make_pair(nodeID_, 0); - } - [[nodiscard]] NetClock::time_point - now() const - { - return toNetClock(c_); - } - - // Issue a new validation with given sequence number and id and - // with signing and seen times offset from the common clock - [[nodiscard]] Validation - validate( - Ledger::ID id, - Ledger::Seq seq, - NetClock::duration signOffset, - NetClock::duration seenOffset, - bool full) const - { - Validation v{ - id, - seq, - now() + signOffset, - now() + seenOffset, - currKey(), - nodeID_, - full, - loadFee_}; - if (trusted_) - v.setTrusted(); - return v; - } - - [[nodiscard]] Validation - validate(Ledger ledger, NetClock::duration signOffset, NetClock::duration seenOffset) const - { - return validate(ledger.id(), ledger.seq(), signOffset, seenOffset, true); - } - - [[nodiscard]] Validation - validate(Ledger ledger) const - { - return validate( - ledger.id(), ledger.seq(), NetClock::duration{0}, NetClock::duration{0}, true); - } - - [[nodiscard]] Validation - partial(Ledger ledger) const - { - return validate( - ledger.id(), ledger.seq(), NetClock::duration{0}, NetClock::duration{0}, false); - } - }; - - // Generic Validations adaptor - class Adaptor - { - clock_type& c_; - LedgerOracle& oracle_; - - public: - // Non-locking mutex to avoid locks in generic Validations - struct Mutex - { - void - lock() - { - } - - void - unlock() - { - } - }; - - using Validation = csf::Validation; - using Ledger = csf::Ledger; - - Adaptor(clock_type& c, LedgerOracle& o) : c_{c}, oracle_{o} - { - } - - [[nodiscard]] NetClock::time_point - now() const - { - return toNetClock(c_); - } - - std::optional - acquire(Ledger::ID const& id) - { - return oracle_.lookup(id); - } - }; - - // Specialize generic Validations using the above types - using TestValidations = Validations; - - // Gather the dependencies of TestValidations in a single class and provide - // accessors for simplifying test logic - class TestHarness - { - ValidationParms p_; - beast::ManualClock clock_; - TestValidations tv_; - PeerID nextNodeId_{0}; - - public: - explicit TestHarness(LedgerOracle& o) : tv_(p_, clock_, clock_, o) - { - } - - ValStatus - add(Validation const& v) - { - return tv_.add(v.nodeID(), v); - } - - TestValidations& - vals() - { - return tv_; - } - - Node - makeNode() - { - return Node(nextNodeId_++, clock_); - } - - ValidationParms - parms() const - { - return p_; - } - - auto& - clock() - { - return clock_; - } - }; - - Ledger const genesisLedger_{Ledger::MakeGenesis{}}; - - void - testAddValidation() - { - using namespace std::chrono_literals; - - testcase("Add validation"); - LedgerHistoryHelper h; - Ledger const ledgerA = h["a"]; - Ledger ledgerAB = h["ab"]; - Ledger ledgerAZ = h["az"]; - Ledger ledgerABC = h["abc"]; - Ledger const ledgerABCD = h["abcd"]; - Ledger const ledgerABCDE = h["abcde"]; - - { - TestHarness harness(h.oracle); - Node n = harness.makeNode(); - - auto const v = n.validate(ledgerA); - - // Add a current validation - BEAST_EXPECT(ValStatus::Current == harness.add(v)); - - // Re-adding violates the increasing seq requirement for full - // validations - BEAST_EXPECT(ValStatus::BadSeq == harness.add(v)); - - harness.clock().advance(1s); - - BEAST_EXPECT(ValStatus::Current == harness.add(n.validate(ledgerAB))); - - // Test the node changing signing key - - // Confirm old ledger on hand, but not new ledger - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerAB.id()) == 1); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerABC.id()) == 0); - - // Rotate signing keys - n.advanceKey(); - - harness.clock().advance(1s); - - // Cannot re-do the same full validation sequence - BEAST_EXPECT(ValStatus::Conflicting == harness.add(n.validate(ledgerAB))); - // Cannot send the same partial validation sequence - BEAST_EXPECT(ValStatus::Conflicting == harness.add(n.partial(ledgerAB))); - - // Now trusts the newest ledger too - harness.clock().advance(1s); - BEAST_EXPECT(ValStatus::Current == harness.add(n.validate(ledgerABC))); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerAB.id()) == 1); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerABC.id()) == 1); - - // Processing validations out of order should ignore the older - // validation - harness.clock().advance(2s); - auto const valABCDE = n.validate(ledgerABCDE); - - harness.clock().advance(4s); - auto const valABCD = n.validate(ledgerABCD); - - BEAST_EXPECT(ValStatus::Current == harness.add(valABCD)); - - BEAST_EXPECT(ValStatus::Stale == harness.add(valABCDE)); - } - - { - // Process validations out of order with shifted times - - TestHarness harness(h.oracle); - Node const n = harness.makeNode(); - - // Establish a new current validation - BEAST_EXPECT(ValStatus::Current == harness.add(n.validate(ledgerA))); - - // Process a validation that has "later" seq but early sign time - BEAST_EXPECT(ValStatus::Stale == harness.add(n.validate(ledgerAB, -1s, -1s))); - - // Process a validation that has a later seq and later sign - // time - BEAST_EXPECT(ValStatus::Current == harness.add(n.validate(ledgerABC, 1s, 1s))); - } - - { - // Test stale on arrival validations - TestHarness harness(h.oracle); - Node const n = harness.makeNode(); - - BEAST_EXPECT( - ValStatus::Stale == - harness.add(n.validate(ledgerA, -harness.parms().validationCurrentEarly, 0s))); - - BEAST_EXPECT( - ValStatus::Stale == - harness.add(n.validate(ledgerA, harness.parms().validationCurrentWall, 0s))); - - BEAST_EXPECT( - ValStatus::Stale == - harness.add(n.validate(ledgerA, 0s, harness.parms().validationCurrentLocal))); - } - - { - // Test that full or partials cannot be sent for older sequence - // numbers, unless time-out has happened - for (bool doFull : {true, false}) - { - TestHarness harness(h.oracle); - Node n = harness.makeNode(); - - auto process = [&](Ledger& lgr) { - if (doFull) - return harness.add(n.validate(lgr)); - return harness.add(n.partial(lgr)); - }; - - BEAST_EXPECT(ValStatus::Current == process(ledgerABC)); - harness.clock().advance(1s); - BEAST_EXPECT(ledgerAB.seq() < ledgerABC.seq()); - BEAST_EXPECT(ValStatus::BadSeq == process(ledgerAB)); - - // If we advance far enough for AB to expire, we can fully - // validate or partially validate that sequence number again - BEAST_EXPECT(ValStatus::Conflicting == process(ledgerAZ)); - harness.clock().advance(harness.parms().validationSetExpires + 1ms); - BEAST_EXPECT(ValStatus::Current == process(ledgerAZ)); - } - } - } - - void - testOnStale() - { - testcase("Stale validation"); - // Verify validation becomes stale based solely on time passing, but - // use different functions to trigger the check for staleness - - LedgerHistoryHelper h; - Ledger ledgerA = h["a"]; - Ledger const ledgerAB = h["ab"]; - - using Trigger = std::function; - - std::vector const triggers = { - [&](TestValidations& vals) { vals.currentTrusted(); }, - [&](TestValidations& vals) { vals.getCurrentNodeIDs(); }, - [&](TestValidations& vals) { vals.getPreferred(genesisLedger_); }, - [&](TestValidations& vals) { vals.getNodesAfter(ledgerA, ledgerA.id()); }}; - for (Trigger const& trigger : triggers) - { - TestHarness harness(h.oracle); - Node const n = harness.makeNode(); - - BEAST_EXPECT(ValStatus::Current == harness.add(n.validate(ledgerAB))); - trigger(harness.vals()); - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 1); - BEAST_EXPECT( - harness.vals().getPreferred(genesisLedger_) == - std::make_pair(ledgerAB.seq(), ledgerAB.id())); - harness.clock().advance(harness.parms().validationCurrentLocal); - - // trigger check for stale - trigger(harness.vals()); - - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 0); - BEAST_EXPECT(harness.vals().getPreferred(genesisLedger_) == std::nullopt); - } - } - - void - testGetNodesAfter() - { - // Test getting number of nodes working on a validation descending - // a prescribed one. This count should only be for trusted nodes, but - // includes partial and full validations - - using namespace std::chrono_literals; - testcase("Get nodes after"); - - LedgerHistoryHelper h; - Ledger const ledgerA = h["a"]; - Ledger const ledgerAB = h["ab"]; - Ledger const ledgerABC = h["abc"]; - Ledger const ledgerAD = h["ad"]; - - TestHarness harness(h.oracle); - Node const trustedNode1 = harness.makeNode(); - Node const trustedNode2 = harness.makeNode(); - Node const trustedNode3 = harness.makeNode(); - - Node notTrustedNode = harness.makeNode(); - notTrustedNode.untrust(); - - // first round a,b,c agree, d has is partial - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode1.validate(ledgerA))); - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode2.validate(ledgerA))); - BEAST_EXPECT(ValStatus::Current == harness.add(notTrustedNode.validate(ledgerA))); - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode3.partial(ledgerA))); - - for (Ledger const& ledger : {ledgerA, ledgerAB, ledgerABC, ledgerAD}) - BEAST_EXPECT(harness.vals().getNodesAfter(ledger, ledger.id()) == 0); - - harness.clock().advance(5s); - - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode1.validate(ledgerAB))); - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode2.validate(ledgerABC))); - BEAST_EXPECT(ValStatus::Current == harness.add(notTrustedNode.validate(ledgerAB))); - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode3.partial(ledgerABC))); - - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 3); - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerAB, ledgerAB.id()) == 2); - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerABC, ledgerABC.id()) == 0); - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerAD, ledgerAD.id()) == 0); - - // If given a ledger inconsistent with the id, is still able to check using slower method - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerAD, ledgerA.id()) == 1); - BEAST_EXPECT(harness.vals().getNodesAfter(ledgerAD, ledgerAB.id()) == 2); - } - - void - testCurrentTrusted() - { - using namespace std::chrono_literals; - testcase("Current trusted validations"); - - LedgerHistoryHelper h; - Ledger const ledgerA = h["a"]; - Ledger const ledgerB = h["b"]; - Ledger const ledgerAC = h["ac"]; - - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - Node b = harness.makeNode(); - b.untrust(); - - BEAST_EXPECT(ValStatus::Current == harness.add(a.validate(ledgerA))); - BEAST_EXPECT(ValStatus::Current == harness.add(b.validate(ledgerB))); - - // Only a is trusted - BEAST_EXPECT(harness.vals().currentTrusted().size() == 1); - BEAST_EXPECT(harness.vals().currentTrusted()[0].ledgerID() == ledgerA.id()); - BEAST_EXPECT(harness.vals().currentTrusted()[0].seq() == ledgerA.seq()); - - harness.clock().advance(3s); - - for (auto const& node : {a, b}) - BEAST_EXPECT(ValStatus::Current == harness.add(node.validate(ledgerAC))); - - // New validation for a - BEAST_EXPECT(harness.vals().currentTrusted().size() == 1); - BEAST_EXPECT(harness.vals().currentTrusted()[0].ledgerID() == ledgerAC.id()); - BEAST_EXPECT(harness.vals().currentTrusted()[0].seq() == ledgerAC.seq()); - - // Pass enough time for it to go stale - harness.clock().advance(harness.parms().validationCurrentLocal); - BEAST_EXPECT(harness.vals().currentTrusted().empty()); - } - - void - testGetCurrentPublicKeys() - { - using namespace std::chrono_literals; - testcase("Current public keys"); - - LedgerHistoryHelper h; - Ledger const ledgerA = h["a"]; - Ledger const ledgerAC = h["ac"]; - - TestHarness harness(h.oracle); - Node a = harness.makeNode(), b = harness.makeNode(); - b.untrust(); - - for (auto const& node : {a, b}) - BEAST_EXPECT(ValStatus::Current == harness.add(node.validate(ledgerA))); - - { - hash_set const expectedKeys = {a.nodeID(), b.nodeID()}; - BEAST_EXPECT(harness.vals().getCurrentNodeIDs() == expectedKeys); - } - - harness.clock().advance(3s); - - // Change keys and issue partials - a.advanceKey(); - b.advanceKey(); - - for (auto const& node : {a, b}) - BEAST_EXPECT(ValStatus::Current == harness.add(node.partial(ledgerAC))); - - { - hash_set const expectedKeys = {a.nodeID(), b.nodeID()}; - BEAST_EXPECT(harness.vals().getCurrentNodeIDs() == expectedKeys); - } - - // Pass enough time for them to go stale - harness.clock().advance(harness.parms().validationCurrentLocal); - BEAST_EXPECT(harness.vals().getCurrentNodeIDs().empty()); - } - - void - testTrustedByLedgerFunctions() - { - // Test the Validations functions that calculate a value by ledger ID - using namespace std::chrono_literals; - testcase("By ledger functions"); - - // Several Validations functions return a set of values associated - // with trusted ledgers sharing the same ledger ID. The tests below - // exercise this logic by saving the set of trusted Validations, and - // verifying that the Validations member functions all calculate the - // proper transformation of the available ledgers. - - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - - Node a = harness.makeNode(), b = harness.makeNode(), c = harness.makeNode(), - d = harness.makeNode(), e = harness.makeNode(); - - c.untrust(); - // Mix of load fees - a.setLoadFee(12); - b.setLoadFee(1); - c.setLoadFee(12); - e.setLoadFee(12); - - hash_map, std::vector> trustedValidations; - - //---------------------------------------------------------------------- - // checkers - auto sorted = [](auto vec) { - std::sort(vec.begin(), vec.end()); - return vec; - }; - auto compare = [&]() { - for (auto& it : trustedValidations) - { - auto const& id = it.first.first; - auto const& seq = it.first.second; - auto const& expectedValidations = it.second; - - BEAST_EXPECT(harness.vals().numTrustedForLedger(id) == expectedValidations.size()); - BEAST_EXPECT( - sorted(harness.vals().getTrustedForLedger(id, seq)) == - sorted(expectedValidations)); - - std::uint32_t const baseFee = 0; - std::vector expectedFees; - expectedFees.reserve(expectedValidations.size()); - for (auto const& val : expectedValidations) - { - expectedFees.push_back(val.loadFee().value_or(baseFee)); - } - - BEAST_EXPECT(sorted(harness.vals().fees(id, baseFee)) == sorted(expectedFees)); - } - }; - - //---------------------------------------------------------------------- - Ledger const ledgerA = h["a"]; - Ledger const ledgerB = h["b"]; - Ledger const ledgerAC = h["ac"]; - - // Add a dummy ID to cover unknown ledger identifiers - trustedValidations[{Ledger::ID{100}, Ledger::Seq{100}}] = {}; - - // first round a,b,c agree - for (auto const& node : {a, b, c}) - { - auto const val = node.validate(ledgerA); - BEAST_EXPECT(ValStatus::Current == harness.add(val)); - if (val.trusted()) - trustedValidations[{val.ledgerID(), val.seq()}].emplace_back(val); - } - // d disagrees - { - auto const val = d.validate(ledgerB); - BEAST_EXPECT(ValStatus::Current == harness.add(val)); - trustedValidations[{val.ledgerID(), val.seq()}].emplace_back(val); - } - // e only issues partials - { - BEAST_EXPECT(ValStatus::Current == harness.add(e.partial(ledgerA))); - } - - harness.clock().advance(5s); - // second round, a,b,c move to ledger 2 - for (auto const& node : {a, b, c}) - { - auto const val = node.validate(ledgerAC); - BEAST_EXPECT(ValStatus::Current == harness.add(val)); - if (val.trusted()) - trustedValidations[{val.ledgerID(), val.seq()}].emplace_back(val); - } - // d now thinks ledger 1, but cannot re-issue a previously used seq - // and attempting it should generate a conflict. - { - BEAST_EXPECT(ValStatus::Conflicting == harness.add(d.partial(ledgerA))); - } - // e only issues partials - { - BEAST_EXPECT(ValStatus::Current == harness.add(e.partial(ledgerAC))); - } - - compare(); - } - - void - testExpire() - { - // Verify expiring clears out validations stored by ledger - testcase("Expire validations"); - SuiteJournal j("Validations_test", *this); - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - constexpr Ledger::Seq kOne(1); - constexpr Ledger::Seq kTwo(2); - - // simple cases - Ledger const ledgerA = h["a"]; - BEAST_EXPECT(ValStatus::Current == harness.add(a.validate(ledgerA))); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); - harness.vals().expire(j); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); - harness.clock().advance(harness.parms().validationSetExpires); - harness.vals().expire(j); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 0); - - // use setSeqToKeep to keep the validation from expire - Ledger const ledgerB = h["ab"]; - BEAST_EXPECT(ValStatus::Current == harness.add(a.validate(ledgerB))); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerB.id()) == 1); - harness.vals().setSeqToKeep(ledgerB.seq(), ledgerB.seq() + kOne); - harness.clock().advance(harness.parms().validationSetExpires); - harness.vals().expire(j); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerB.id()) == 1); - // change toKeep - harness.vals().setSeqToKeep(ledgerB.seq() + kOne, ledgerB.seq() + kTwo); - // advance clock slowly - int const loops = - harness.parms().validationSetExpires / harness.parms().validationFRESHNESS + 1; - for (int i = 0; i < loops; ++i) - { - harness.clock().advance(harness.parms().validationFRESHNESS); - harness.vals().expire(j); - } - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerB.id()) == 0); - - // Allow the validation with high seq to expire - Ledger const ledgerC = h["abc"]; - BEAST_EXPECT(ValStatus::Current == harness.add(a.validate(ledgerC))); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerC.id()) == 1); - harness.vals().setSeqToKeep(ledgerC.seq() - kOne, ledgerC.seq()); - harness.clock().advance(harness.parms().validationSetExpires); - harness.vals().expire(j); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerC.id()) == 0); - } - - void - testFlush() - { - // Test final flush of validations - using namespace std::chrono_literals; - testcase("Flush validations"); - - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const trustedNode1 = harness.makeNode(); - Node const trustedNode2 = harness.makeNode(); - Node notTrustedNode = harness.makeNode(); - notTrustedNode.untrust(); - - Ledger const ledgerA = h["a"]; - Ledger const ledgerAB = h["ab"]; - - hash_map expected; - for (auto const& node : {trustedNode1, trustedNode2, notTrustedNode}) - { - auto const val = node.validate(ledgerA); - BEAST_EXPECT(ValStatus::Current == harness.add(val)); - expected.emplace(node.nodeID(), val); - } - - // Send in a new validation for a, saving the new one into the expected - // map after setting the proper prior ledger ID it replaced - harness.clock().advance(1s); - auto newVal = trustedNode1.validate(ledgerAB); - BEAST_EXPECT(ValStatus::Current == harness.add(newVal)); - expected.find(trustedNode1.nodeID())->second = newVal; - } - - void - testGetPreferredLedger() - { - using namespace std::chrono_literals; - testcase("Preferred Ledger"); - - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const trustedNode1 = harness.makeNode(); - Node const trustedNode2 = harness.makeNode(); - Node const trustedNode3 = harness.makeNode(); - - Node notTrustedNode = harness.makeNode(); - notTrustedNode.untrust(); - - Ledger const ledgerA = h["a"]; - Ledger const ledgerB = h["b"]; - Ledger const ledgerAC = h["ac"]; - Ledger const ledgerACD = h["acd"]; - - using Seq = Ledger::Seq; - - auto pref = [](Ledger ledger) { return std::make_pair(ledger.seq(), ledger.id()); }; - - // Empty (no ledgers) - BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == std::nullopt); - - // Single ledger - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode1.validate(ledgerB))); - BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerB)); - BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerB)); - - // Minimum valid sequence - BEAST_EXPECT(harness.vals().getPreferred(ledgerA, Seq{10}) == ledgerA.id()); - - // Untrusted doesn't impact preferred ledger - // (ledgerB has tie-break over ledgerA) - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode2.validate(ledgerA))); - BEAST_EXPECT(ValStatus::Current == harness.add(notTrustedNode.validate(ledgerA))); - BEAST_EXPECT(ledgerB.id() > ledgerA.id()); - BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerB)); - BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerB)); - - // Partial does break ties - BEAST_EXPECT(ValStatus::Current == harness.add(trustedNode3.partial(ledgerA))); - BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerA)); - BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerA)); - - harness.clock().advance(5s); - - // Parent of preferred-> stick with ledger - for (auto const& node : {trustedNode1, trustedNode2, notTrustedNode, trustedNode3}) - BEAST_EXPECT(ValStatus::Current == harness.add(node.validate(ledgerAC))); - // Parent of preferred stays put - BEAST_EXPECT(harness.vals().getPreferred(ledgerA) == pref(ledgerA)); - // Earlier different chain, switch - BEAST_EXPECT(harness.vals().getPreferred(ledgerB) == pref(ledgerAC)); - // Later on chain, stays where it is - BEAST_EXPECT(harness.vals().getPreferred(ledgerACD) == pref(ledgerACD)); - - // Any later grandchild or different chain is preferred - harness.clock().advance(5s); - for (auto const& node : {trustedNode1, trustedNode2, notTrustedNode, trustedNode3}) - BEAST_EXPECT(ValStatus::Current == harness.add(node.validate(ledgerACD))); - for (auto const& ledger : {ledgerA, ledgerB, ledgerACD}) - BEAST_EXPECT(harness.vals().getPreferred(ledger) == pref(ledgerACD)); - } - - void - testGetPreferredLCL() - { - using namespace std::chrono_literals; - testcase("Get preferred LCL"); - - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - - Ledger const ledgerA = h["a"]; - Ledger const ledgerB = h["b"]; - Ledger const ledgerC = h["c"]; - - using ID = Ledger::ID; - using Seq = Ledger::Seq; - - hash_map peerCounts; - - // No trusted validations or counts sticks with current ledger - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerA.id()); - - ++peerCounts[ledgerB.id()]; - - // No trusted validations, rely on peer counts - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerB.id()); - - ++peerCounts[ledgerC.id()]; - // No trusted validations, tied peers goes with larger ID - BEAST_EXPECT(ledgerC.id() > ledgerB.id()); - - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerC.id()); - - peerCounts[ledgerC.id()] += 1000; - - // Single trusted always wins over peer counts - BEAST_EXPECT(ValStatus::Current == harness.add(a.validate(ledgerA))); - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerA.id()); - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerB, Seq{0}, peerCounts) == ledgerA.id()); - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerC, Seq{0}, peerCounts) == ledgerA.id()); - - // Stick with current ledger if trusted validation ledger has too old - // of a sequence - BEAST_EXPECT(harness.vals().getPreferredLCL(ledgerB, Seq{2}, peerCounts) == ledgerB.id()); - } - - void - testAcquireValidatedLedger() - { - using namespace std::chrono_literals; - testcase("Acquire validated ledger"); - - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - Node const b = harness.makeNode(); - - using ID = Ledger::ID; - using Seq = Ledger::Seq; - - // Validate the ledger before it is actually available - Validation const val = a.validate(ID{2}, Seq{2}, 0s, 0s, true); - - BEAST_EXPECT(ValStatus::Current == harness.add(val)); - // Validation is available - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{2}) == 1); - // but ledger based data is not - BEAST_EXPECT(harness.vals().getNodesAfter(genesisLedger_, ID{0}) == 0); - // Initial preferred branch falls back to the ledger we are trying to - // acquire - BEAST_EXPECT(harness.vals().getPreferred(genesisLedger_) == std::make_pair(Seq{2}, ID{2})); - - // After adding another unavailable validation, the preferred ledger - // breaks ties via higher ID - BEAST_EXPECT(ValStatus::Current == harness.add(b.validate(ID{3}, Seq{2}, 0s, 0s, true))); - BEAST_EXPECT(harness.vals().getPreferred(genesisLedger_) == std::make_pair(Seq{2}, ID{3})); - - // Create the ledger - Ledger const ledgerAB = h["ab"]; - // Now it should be available - BEAST_EXPECT(harness.vals().getNodesAfter(genesisLedger_, ID{0}) == 1); - - // Create a validation that is not available - harness.clock().advance(5s); - Validation const val2 = a.validate(ID{4}, Seq{4}, 0s, 0s, true); - BEAST_EXPECT(ValStatus::Current == harness.add(val2)); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{4}) == 1); - BEAST_EXPECT( - harness.vals().getPreferred(genesisLedger_) == - std::make_pair(ledgerAB.seq(), ledgerAB.id())); - - // Another node requesting that ledger still doesn't change things - Validation const val3 = b.validate(ID{4}, Seq{4}, 0s, 0s, true); - BEAST_EXPECT(ValStatus::Current == harness.add(val3)); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ID{4}) == 2); - BEAST_EXPECT( - harness.vals().getPreferred(genesisLedger_) == - std::make_pair(ledgerAB.seq(), ledgerAB.id())); - - // Switch to validation that is available - harness.clock().advance(5s); - Ledger const ledgerABCDE = h["abcde"]; - BEAST_EXPECT(ValStatus::Current == harness.add(a.partial(ledgerABCDE))); - BEAST_EXPECT(ValStatus::Current == harness.add(b.partial(ledgerABCDE))); - BEAST_EXPECT( - harness.vals().getPreferred(genesisLedger_) == - std::make_pair(ledgerABCDE.seq(), ledgerABCDE.id())); - } - - void - testNumTrustedForLedger() - { - testcase("NumTrustedForLedger"); - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - Node const b = harness.makeNode(); - Ledger const ledgerA = h["a"]; - - BEAST_EXPECT(ValStatus::Current == harness.add(a.partial(ledgerA))); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 0); - - BEAST_EXPECT(ValStatus::Current == harness.add(b.validate(ledgerA))); - BEAST_EXPECT(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); - } - - void - testSeqEnforcer() - { - testcase("SeqEnforcer"); - using Seq = Ledger::Seq; - using namespace std::chrono; - - beast::ManualClock clock; - SeqEnforcer enforcer; - - ValidationParms const p; - - BEAST_EXPECT(enforcer(clock.now(), Seq{1}, p)); - BEAST_EXPECT(enforcer(clock.now(), Seq{10}, p)); - BEAST_EXPECT(!enforcer(clock.now(), Seq{5}, p)); - BEAST_EXPECT(!enforcer(clock.now(), Seq{9}, p)); - clock.advance(p.validationSetExpires - 1ms); - BEAST_EXPECT(!enforcer(clock.now(), Seq{1}, p)); - clock.advance(2ms); - BEAST_EXPECT(enforcer(clock.now(), Seq{1}, p)); - } - - void - testTrustChanged() - { - testcase("TrustChanged"); - using namespace std::chrono; - - auto checker = [this]( - TestValidations& vals, - hash_set const& listed, - std::vector const& trustedVals) { - Ledger::ID const testID = - trustedVals.empty() ? this->genesisLedger_.id() : trustedVals[0].ledgerID(); - Ledger::Seq const testSeq = - trustedVals.empty() ? this->genesisLedger_.seq() : trustedVals[0].seq(); - BEAST_EXPECT(vals.currentTrusted() == trustedVals); - BEAST_EXPECT(vals.getCurrentNodeIDs() == listed); - BEAST_EXPECT( - vals.getNodesAfter(this->genesisLedger_, genesisLedger_.id()) == - trustedVals.size()); - if (trustedVals.empty()) - { - BEAST_EXPECT(vals.getPreferred(this->genesisLedger_) == std::nullopt); - } - else - { - BEAST_EXPECT(vals.getPreferred(this->genesisLedger_)->second == testID); - } - BEAST_EXPECT(vals.getTrustedForLedger(testID, testSeq) == trustedVals); - BEAST_EXPECT(vals.numTrustedForLedger(testID) == trustedVals.size()); - }; - - { - // Trusted to untrusted - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - Ledger const ledgerAB = h["ab"]; - Validation const v = a.validate(ledgerAB); - BEAST_EXPECT(ValStatus::Current == harness.add(v)); - - hash_set const listed({a.nodeID()}); - std::vector trustedVals({v}); - checker(harness.vals(), listed, trustedVals); - - trustedVals.clear(); - harness.vals().trustChanged({}, {a.nodeID()}); - checker(harness.vals(), listed, trustedVals); - } - - { - // Untrusted to trusted - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node a = harness.makeNode(); - a.untrust(); - Ledger const ledgerAB = h["ab"]; - Validation const v = a.validate(ledgerAB); - BEAST_EXPECT(ValStatus::Current == harness.add(v)); - - hash_set const listed({a.nodeID()}); - std::vector trustedVals; - checker(harness.vals(), listed, trustedVals); - - trustedVals.push_back(v); - harness.vals().trustChanged({a.nodeID()}, {}); - checker(harness.vals(), listed, trustedVals); - } - - { - // Trusted but not acquired -> untrusted - LedgerHistoryHelper h; - TestHarness harness(h.oracle); - Node const a = harness.makeNode(); - Validation const v = a.validate(Ledger::ID{2}, Ledger::Seq{2}, 0s, 0s, true); - BEAST_EXPECT(ValStatus::Current == harness.add(v)); - - hash_set const listed({a.nodeID()}); - std::vector trustedVals({v}); - auto& vals = harness.vals(); - BEAST_EXPECT(vals.currentTrusted() == trustedVals); - - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - BEAST_EXPECT(vals.getPreferred(genesisLedger_)->second == v.ledgerID()); - BEAST_EXPECT(vals.getNodesAfter(genesisLedger_, genesisLedger_.id()) == 0); - - trustedVals.clear(); - harness.vals().trustChanged({}, {a.nodeID()}); - // make acquiring ledger available - h["ab"]; - BEAST_EXPECT(vals.currentTrusted() == trustedVals); - BEAST_EXPECT(vals.getPreferred(genesisLedger_) == std::nullopt); - BEAST_EXPECT(vals.getNodesAfter(genesisLedger_, genesisLedger_.id()) == 0); - } - } - - void - run() override - { - testAddValidation(); - testOnStale(); - testGetNodesAfter(); - testCurrentTrusted(); - testGetCurrentPublicKeys(); - testTrustedByLedgerFunctions(); - testExpire(); - testFlush(); - testGetPreferredLedger(); - testGetPreferredLCL(); - testAcquireValidatedLedger(); - testNumTrustedForLedger(); - testSeqEnforcer(); - testTrustChanged(); - } -}; - -BEAST_DEFINE_TESTSUITE(Validations, consensus, xrpl); -} // namespace xrpl::test::csf diff --git a/src/test/csf/BasicNetwork_test.cpp b/src/test/csf/BasicNetwork_test.cpp deleted file mode 100644 index 22f52a1b63..0000000000 --- a/src/test/csf/BasicNetwork_test.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include -#include - -#include - -#include -#include - -namespace xrpl::test { - -class BasicNetwork_test : public beast::unit_test::Suite -{ -public: - struct Peer - { - int id; - std::set set; - - Peer(Peer const&) = default; - Peer(Peer&&) = default; - - explicit Peer(int id) : id(id) - { - } - - template - void - start(csf::Scheduler& scheduler, Net& net) - { - using namespace std::chrono_literals; - auto t = scheduler.in(1s, [&] { set.insert(0); }); - if (id == 0) - { - for (auto const link : net.links(this)) - { - net.send( - this, link.target, [&, to = link.target] { to->receive(net, this, 1); }); - } - } - else - { - scheduler.cancel(t); - } - } - - template - void - receive(Net& net, Peer* from, int m) - { - set.insert(m); - ++m; - if (m < 5) - { - for (auto const link : net.links(this)) - { - net.send(this, link.target, [&, mm = m, to = link.target] { - to->receive(net, this, mm); - }); - } - } - } - }; - - void - testNetwork() - { - using namespace std::chrono_literals; - std::vector pv; - pv.emplace_back(0); - pv.emplace_back(1); - pv.emplace_back(2); - csf::Scheduler scheduler; - csf::BasicNetwork net(scheduler); - BEAST_EXPECT(!net.connect(&pv[0], &pv[0])); - BEAST_EXPECT(net.connect(&pv[0], &pv[1], 1s)); - BEAST_EXPECT(net.connect(&pv[1], &pv[2], 1s)); - BEAST_EXPECT(!net.connect(&pv[0], &pv[1])); - for (auto& peer : pv) - peer.start(scheduler, net); - BEAST_EXPECT(scheduler.stepFor(0s)); - BEAST_EXPECT(scheduler.stepFor(1s)); - BEAST_EXPECT(scheduler.step()); - BEAST_EXPECT(!scheduler.step()); - BEAST_EXPECT(!scheduler.stepFor(1s)); - net.send(&pv[0], &pv[1], [] {}); - net.send(&pv[1], &pv[0], [] {}); - BEAST_EXPECT(net.disconnect(&pv[0], &pv[1])); - BEAST_EXPECT(!net.disconnect(&pv[0], &pv[1])); - for (;;) - { - auto const links = net.links(&pv[1]); - if (links.empty()) - break; - BEAST_EXPECT(net.disconnect(&pv[1], links[0].target)); - } - BEAST_EXPECT(pv[0].set == std::set({0, 2, 4})); - BEAST_EXPECT(pv[1].set == std::set({1, 3})); - BEAST_EXPECT(pv[2].set == std::set({2, 4})); - } - - void - testDisconnect() - { - using namespace std::chrono_literals; - csf::Scheduler scheduler; - csf::BasicNetwork net(scheduler); - BEAST_EXPECT(net.connect(0, 1, 1s)); - BEAST_EXPECT(net.connect(0, 2, 2s)); - - std::set delivered; - net.send(0, 1, [&]() { delivered.insert(1); }); - net.send(0, 2, [&]() { delivered.insert(2); }); - - scheduler.in(1000ms, [&]() { BEAST_EXPECT(net.disconnect(0, 2)); }); - scheduler.in(1100ms, [&]() { BEAST_EXPECT(net.connect(0, 2)); }); - - scheduler.step(); - - // only the first message is delivered because the disconnect at 1 s - // purges all pending messages from 0 to 2 - BEAST_EXPECT(delivered == std::set({1})); - } - - void - run() override - { - testNetwork(); - testDisconnect(); - } -}; - -BEAST_DEFINE_TESTSUITE(BasicNetwork, csf, xrpl); - -} // namespace xrpl::test diff --git a/src/test/csf/Digraph_test.cpp b/src/test/csf/Digraph_test.cpp deleted file mode 100644 index 40bfafde9c..0000000000 --- a/src/test/csf/Digraph_test.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include - -#include - -#include -#include -#include -#include - -namespace xrpl::test { - -class Digraph_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace csf; - using Graph = Digraph; - Graph graph; - - BEAST_EXPECT(!graph.connected('a', 'b')); - BEAST_EXPECT(!graph.edge('a', 'b')); - BEAST_EXPECT(!graph.disconnect('a', 'b')); - - BEAST_EXPECT(graph.connect('a', 'b', "foobar")); - BEAST_EXPECT(graph.connected('a', 'b')); - BEAST_EXPECT( - *graph.edge('a', 'b') == "foobar"); // NOLINT(bugprone-unchecked-optional-access) - - BEAST_EXPECT(!graph.connect('a', 'b', "repeat")); - BEAST_EXPECT(graph.disconnect('a', 'b')); - BEAST_EXPECT(graph.connect('a', 'b', "repeat")); - BEAST_EXPECT(graph.connected('a', 'b')); - BEAST_EXPECT( - *graph.edge('a', 'b') == "repeat"); // NOLINT(bugprone-unchecked-optional-access) - - BEAST_EXPECT(graph.connect('a', 'c', "tree")); - - { - std::vector> edges; - - for (auto const& edge : graph.outEdges('a')) - { - edges.emplace_back(edge.source, edge.target, edge.data); - } - - std::vector> expected; - expected.emplace_back('a', 'b', "repeat"); - expected.emplace_back('a', 'c', "tree"); - BEAST_EXPECT(edges == expected); - BEAST_EXPECT(graph.outDegree('a') == expected.size()); - } - - BEAST_EXPECT(graph.outEdges('r').size() == 0); - BEAST_EXPECT(graph.outDegree('r') == 0); - BEAST_EXPECT(graph.outDegree('c') == 0); - - // only 'a' has out edges - BEAST_EXPECT(graph.outVertices().size() == 1); - std::vector const expected = {'b', 'c'}; - - BEAST_EXPECT((graph.outVertices('a') == expected)); - BEAST_EXPECT(graph.outVertices('b').size() == 0); - BEAST_EXPECT(graph.outVertices('c').size() == 0); - BEAST_EXPECT(graph.outVertices('r').size() == 0); - - std::stringstream ss; - graph.saveDot(ss, [](char v) { return v; }); - std::string const expectedDot = - "digraph {\n" - "a -> b;\n" - "a -> c;\n" - "}\n"; - BEAST_EXPECT(ss.str() == expectedDot); - } -}; - -BEAST_DEFINE_TESTSUITE(Digraph, csf, xrpl); - -} // namespace xrpl::test diff --git a/src/test/csf/Histogram_test.cpp b/src/test/csf/Histogram_test.cpp deleted file mode 100644 index 65edb14e5d..0000000000 --- a/src/test/csf/Histogram_test.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include - -#include - -namespace xrpl::test { - -class Histogram_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace csf; - Histogram hist; - - BEAST_EXPECT(hist.size() == 0); - BEAST_EXPECT(hist.numBins() == 0); - BEAST_EXPECT(hist.minValue() == 0); - BEAST_EXPECT(hist.maxValue() == 0); - BEAST_EXPECT(hist.avg() == 0); - BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue()); - BEAST_EXPECT(hist.percentile(0.5f) == 0); - BEAST_EXPECT(hist.percentile(0.9f) == 0); - BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue()); - - hist.insert(1); - - BEAST_EXPECT(hist.size() == 1); - BEAST_EXPECT(hist.numBins() == 1); - BEAST_EXPECT(hist.minValue() == 1); - BEAST_EXPECT(hist.maxValue() == 1); - BEAST_EXPECT(hist.avg() == 1); - BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue()); - BEAST_EXPECT(hist.percentile(0.5f) == 1); - BEAST_EXPECT(hist.percentile(0.9f) == 1); - BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue()); - - hist.insert(9); - - BEAST_EXPECT(hist.size() == 2); - BEAST_EXPECT(hist.numBins() == 2); - BEAST_EXPECT(hist.minValue() == 1); - BEAST_EXPECT(hist.maxValue() == 9); - BEAST_EXPECT(hist.avg() == 5); - BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue()); - BEAST_EXPECT(hist.percentile(0.5f) == 1); - BEAST_EXPECT(hist.percentile(0.9f) == 9); - BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue()); - - hist.insert(1); - - BEAST_EXPECT(hist.size() == 3); - BEAST_EXPECT(hist.numBins() == 2); - BEAST_EXPECT(hist.minValue() == 1); - BEAST_EXPECT(hist.maxValue() == 9); - BEAST_EXPECT(hist.avg() == 11 / 3); - BEAST_EXPECT(hist.percentile(0.0f) == hist.minValue()); - BEAST_EXPECT(hist.percentile(0.5f) == 1); - BEAST_EXPECT(hist.percentile(0.9f) == 9); - BEAST_EXPECT(hist.percentile(1.0f) == hist.maxValue()); - } -}; - -BEAST_DEFINE_TESTSUITE(Histogram, csf, xrpl); - -} // namespace xrpl::test diff --git a/src/test/csf/Scheduler_test.cpp b/src/test/csf/Scheduler_test.cpp deleted file mode 100644 index 6f4ac051c4..0000000000 --- a/src/test/csf/Scheduler_test.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include - -#include - -#include - -namespace xrpl::test { - -class Scheduler_test : public beast::unit_test::Suite -{ -public: - void - run() override - { - using namespace std::chrono_literals; - csf::Scheduler scheduler; - std::set seen; - - scheduler.in(1s, [&] { seen.insert(1); }); - scheduler.in(2s, [&] { seen.insert(2); }); - auto token = scheduler.in(3s, [&] { seen.insert(3); }); - scheduler.at(scheduler.now() + 4s, [&] { seen.insert(4); }); - scheduler.at(scheduler.now() + 8s, [&] { seen.insert(8); }); - - auto start = scheduler.now(); - - // Process first event - BEAST_EXPECT(seen.empty()); - BEAST_EXPECT(scheduler.stepOne()); - BEAST_EXPECT(seen == std::set({1})); - BEAST_EXPECT(scheduler.now() == (start + 1s)); - - // No processing if stepping until current time - BEAST_EXPECT(scheduler.stepUntil(scheduler.now())); - BEAST_EXPECT(seen == std::set({1})); - BEAST_EXPECT(scheduler.now() == (start + 1s)); - - // Process next event - BEAST_EXPECT(scheduler.stepFor(1s)); - BEAST_EXPECT(seen == std::set({1, 2})); - BEAST_EXPECT(scheduler.now() == (start + 2s)); - - // Don't process cancelled event, but advance clock - scheduler.cancel(token); - BEAST_EXPECT(scheduler.stepFor(1s)); - BEAST_EXPECT(seen == std::set({1, 2})); - BEAST_EXPECT(scheduler.now() == (start + 3s)); - - // Process until 3 seen ints - BEAST_EXPECT(scheduler.stepWhile([&]() { return seen.size() < 3; })); - BEAST_EXPECT(seen == std::set({1, 2, 4})); - BEAST_EXPECT(scheduler.now() == (start + 4s)); - - // Process the rest - BEAST_EXPECT(scheduler.step()); - BEAST_EXPECT(seen == std::set({1, 2, 4, 8})); - BEAST_EXPECT(scheduler.now() == (start + 8s)); - - // Process the rest again doesn't advance - BEAST_EXPECT(!scheduler.step()); - BEAST_EXPECT(seen == std::set({1, 2, 4, 8})); - BEAST_EXPECT(scheduler.now() == (start + 8s)); - } -}; - -BEAST_DEFINE_TESTSUITE(Scheduler, csf, xrpl); - -} // namespace xrpl::test diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 2e131b895e..4828e03815 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -27,6 +27,7 @@ target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl) # supported on Windows. set(test_modules basics + consensus crypto json peerfinder @@ -58,6 +59,16 @@ foreach(module IN LISTS test_modules) ) endforeach() +# The consensus tests use the CSF (Consensus Simulation Framework) helpers, so +# compile the CSF sources into the test binary. The consensus engine itself now +# lives in libxrpl, so no xrpld sources or include paths are needed here. +file( + GLOB_RECURSE csf_sources + CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/csf/*.cpp" +) +target_sources(xrpl_tests PRIVATE ${csf_sources}) + # The test helpers and per-module test headers are not built with add_module, # so verify them against the test binary's own compile environment. if(verify_headers) diff --git a/src/tests/libxrpl/basics/base_uint_test.cpp b/src/tests/libxrpl/basics/base_uint.cpp similarity index 99% rename from src/tests/libxrpl/basics/base_uint_test.cpp rename to src/tests/libxrpl/basics/base_uint.cpp index c9bfc35c94..174cc33aa0 100644 --- a/src/tests/libxrpl/basics/base_uint_test.cpp +++ b/src/tests/libxrpl/basics/base_uint.cpp @@ -1,5 +1,6 @@ -#include #include + +#include #include #include diff --git a/src/tests/libxrpl/consensus/ByzantineFailureSim.cpp b/src/tests/libxrpl/consensus/ByzantineFailureSim.cpp new file mode 100644 index 0000000000..e712817121 --- /dev/null +++ b/src/tests/libxrpl/consensus/ByzantineFailureSim.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +TEST(ByzantineFailureSimTest, DISABLED_byzantine_failure_sim) +{ + using namespace csf; + using namespace std::chrono; + + // This test simulates a specific topology with nodes generating + // different ledgers due to a simulated byzantine failure (injecting + // an extra non-consensus transaction). + + Sim sim; + ConsensusParms const parms{}; + + SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); + PeerGroup a = sim.createGroup(1); + PeerGroup b = sim.createGroup(1); + PeerGroup c = sim.createGroup(1); + PeerGroup d = sim.createGroup(1); + PeerGroup e = sim.createGroup(1); + PeerGroup f = sim.createGroup(1); + PeerGroup g = sim.createGroup(1); + + a.trustAndConnect(a + b + c + g, delay); + b.trustAndConnect(b + a + c + d + e, delay); + c.trustAndConnect(c + a + b + d + e, delay); + d.trustAndConnect(d + b + c + e + f, delay); + e.trustAndConnect(e + b + c + d + f, delay); + f.trustAndConnect(f + d + e + g, delay); + g.trustAndConnect(g + a + f, delay); + + PeerGroup const network = a + b + c + d + e + f + g; + + StreamCollector sc{std::cout}; + + sim.collectors.add(sc); + + for (TrustGraph::ForkInfo const& fi : sim.trustGraph.forkablePairs(0.8)) + { + std::cout << "Can fork " << PeerGroup{fi.unlA} << " " + << " " << PeerGroup{fi.unlB} << " overlap " << fi.overlap << " required " + << fi.required << "\n"; + }; + + // set prior state + sim.run(1); + + PeerGroup byzantineNodes = a + b + c + g; + // All peers see some TX 0 + for (Peer* peer : network) + { + peer->submit(Tx{0}); + // Peers 0,1,2,6 will close the next ledger differently by injecting + // a non-consensus approved transaction + if (byzantineNodes.contains(peer)) + { + peer->txInjections.emplace(peer->lastClosedLedger.seq(), Tx{42}); + } + } + sim.run(4); + std::cout << "Branches: " << sim.branches() << "\n"; + std::cout << "Fully synchronized: " << std::boolalpha << sim.synchronized() << "\n"; + // Not tessting anything currently. + SUCCEED(); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/CensorshipDetector.cpp b/src/tests/libxrpl/consensus/CensorshipDetector.cpp new file mode 100644 index 0000000000..aa6b2d086b --- /dev/null +++ b/src/tests/libxrpl/consensus/CensorshipDetector.cpp @@ -0,0 +1,81 @@ +#include + +#include + +#include +#include +#include + +namespace xrpl::test { + +namespace { + +void +runRound( + CensorshipDetector& cdet, + int round, + std::vector proposed, + std::vector accepted, + std::vector remain, + std::vector remove) +{ + // Begin tracking what we're proposing this round + CensorshipDetector::TxIDSeqVec proposal; + for (auto const& i : proposed) + proposal.emplace_back(i, round); + cdet.propose(std::move(proposal)); + + // Finalize the round, by processing what we accepted; then + // remove anything that needs to be removed and ensure that + // what remains is correct. + cdet.check(std::move(accepted), [&remove, &remain](auto id, auto seq) { + // If the item is supposed to be removed from the censorship + // detector internal tracker manually, do it now: + if (std::ranges::find(remove, id) != remove.end()) + return true; + + // If the item is supposed to still remain in the censorship + // detector internal tracker; remove it from the vector. + auto it = std::ranges::find(remain, id); + if (it != remain.end()) + remain.erase(it); + return false; + }); + + // On entry, this set contained all the elements that should be tracked + // by the detector after we process this round. We removed all the items + // that actually were in the tracker, so this should now be empty: + EXPECT_TRUE(remain.empty()); +} + +} // namespace + +TEST(CensorshipDetectorTest, censorship_detector) +{ + SCOPED_TRACE("Censorship Detector"); + + CensorshipDetector cdet; + int round = 0; + // proposed accepted remain remove + runRound(cdet, ++round, {}, {}, {}, {}); + runRound(cdet, ++round, {10, 11, 12, 13}, {11, 2}, {10, 13}, {}); + runRound(cdet, ++round, {10, 13, 14, 15}, {14}, {10, 13, 15}, {}); + runRound(cdet, ++round, {10, 13, 15, 16}, {15, 16}, {10, 13}, {}); + runRound(cdet, ++round, {10, 13}, {17, 18}, {10, 13}, {}); + runRound(cdet, ++round, {10, 19}, {}, {10, 19}, {}); + runRound(cdet, ++round, {10, 19, 20}, {20}, {10}, {19}); + runRound(cdet, ++round, {21}, {21}, {}, {}); + runRound(cdet, ++round, {}, {22}, {}, {}); + runRound(cdet, ++round, {23, 24, 25, 26}, {25, 27}, {23, 26}, {24}); + runRound(cdet, ++round, {23, 26, 28}, {26, 28}, {23}, {}); + + for (int i = 0; i != 10; ++i) + runRound(cdet, ++round, {23}, {}, {23}, {}); + + runRound(cdet, ++round, {23, 29}, {29}, {23}, {}); + runRound(cdet, ++round, {30, 31}, {31}, {30}, {}); + runRound(cdet, ++round, {30}, {30}, {}, {}); + runRound(cdet, ++round, {}, {}, {}, {}); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/Consensus.cpp b/src/tests/libxrpl/consensus/Consensus.cpp new file mode 100644 index 0000000000..d303d28e89 --- /dev/null +++ b/src/tests/libxrpl/consensus/Consensus.cpp @@ -0,0 +1,1455 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +namespace { + +beast::Journal +journal() +{ + return beast::Journal{TestSink::instance()}; +} + +bool +shouldCloseLedger( + bool anyTransactions, + std::size_t prevProposers, + std::size_t proposersClosed, + std::size_t proposersValidated, + std::chrono::milliseconds prevRoundTime, + std::chrono::milliseconds timeSincePrevClose, + std::chrono::milliseconds openTime, + std::chrono::milliseconds idleInterval, + ConsensusParms const& parms, + std::unique_ptr const& clog = {}) +{ + return xrpl::shouldCloseLedger( + anyTransactions, + prevProposers, + proposersClosed, + proposersValidated, + prevRoundTime, + timeSincePrevClose, + openTime, + idleInterval, + parms, + journal(), + clog); +} + +ConsensusState +checkConsensus( + std::size_t prevProposers, + std::size_t currentProposers, + std::size_t currentAgree, + std::size_t currentFinished, + std::chrono::milliseconds previousAgreeTime, + std::chrono::milliseconds currentAgreeTime, + bool stalled, + ConsensusParms const& parms, + bool proposing, + std::unique_ptr const& clog = {}) +{ + return xrpl::checkConsensus( + prevProposers, + currentProposers, + currentAgree, + currentFinished, + previousAgreeTime, + currentAgreeTime, + stalled, + parms, + proposing, + journal(), + clog); +} + +using CsfDisputedTx = DisputedTx; + +CsfDisputedTx +makeDisputedTx(csf::Tx tx, bool ourVote, std::size_t numPeers) +{ + return CsfDisputedTx{tx, ourVote, numPeers, journal()}; +} + +bool +isStalled( + CsfDisputedTx const& dispute, + ConsensusParms const& parms, + bool proposing, + int peersUnchanged, + std::unique_ptr const& clog) +{ + return dispute.stalled(parms, proposing, peersUnchanged, journal(), clog); +} + +// Helper collector for testPreferredByBranch +// Invasively disconnects network at bad times to cause splits +struct Disruptor +{ + csf::PeerGroup& network; + csf::PeerGroup& groupCfast; + csf::PeerGroup& groupCsplit; + csf::SimDuration delay; + bool reconnected = false; + + Disruptor(csf::PeerGroup& net, csf::PeerGroup& c, csf::PeerGroup& split, csf::SimDuration d) + : network(net), groupCfast(c), groupCsplit(split), delay(d) + { + } + + template + void + on(csf::PeerID, csf::SimTime, E const&) + { + } + + void + on(csf::PeerID who, csf::SimTime, csf::FullyValidateLedger const& e) + { + using namespace std::chrono; + // As soon as the fastC node fully validates C, disconnect + // ALL c nodes from the network. The fast C node needs to disconnect + // as well to prevent it from relaying the validations it did see + if (who == groupCfast[0]->id && e.ledger.seq() == csf::Ledger::Seq{2}) + { + network.disconnect(groupCsplit); + network.disconnect(groupCfast); + } + } + + void + on(csf::PeerID who, csf::SimTime, csf::AcceptLedger const& e) + { + // As soon as anyone generates a child of B or C, reconnect the + // network so those validations make it through + if (!reconnected && e.ledger.seq() == csf::Ledger::Seq{3}) + { + reconnected = true; + network.connect(groupCsplit, delay); + } + } +}; + +// Helper collector for testPauseForLaggards +// This will remove the ledgerAccept delay used to +// initially create the slow vs. fast validator groups. +struct UndoDelay +{ + csf::PeerGroup& g; + + UndoDelay(csf::PeerGroup& a) : g(a) + { + } + + template + void + on(csf::PeerID, csf::SimTime, E const&) + { + } + + void + on(csf::PeerID who, csf::SimTime, csf::AcceptLedger const& e) + { + for (csf::Peer* p : g) + { + if (p->id == who) + p->delays.ledgerAccept = std::chrono::seconds{0}; + } + } +}; + +} // namespace + +TEST(ConsensusTest, should_close_ledger) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("should close ledger"); + + // Use default parameters + ConsensusParms const p{}; + + // Bizarre times forcibly close + EXPECT_TRUE(shouldCloseLedger(true, 10, 10, 10, -10s, 10s, 1s, 1s, p)); + EXPECT_TRUE(shouldCloseLedger(true, 10, 10, 10, 100h, 10s, 1s, 1s, p)); + EXPECT_TRUE(shouldCloseLedger(true, 10, 10, 10, 10s, 100h, 1s, 1s, p)); + + // Rest of network has closed + EXPECT_TRUE(shouldCloseLedger(true, 10, 3, 5, 10s, 10s, 10s, 10s, p)); + + // No transactions means wait until end of internval + EXPECT_TRUE(!shouldCloseLedger(false, 10, 0, 0, 1s, 1s, 1s, 10s, p)); + EXPECT_TRUE(shouldCloseLedger(false, 10, 0, 0, 1s, 10s, 1s, 10s, p)); + + // Enforce minimum ledger open time + EXPECT_TRUE(!shouldCloseLedger(true, 10, 0, 0, 10s, 10s, 1s, 10s, p)); + + // Don't go too much faster than last time + EXPECT_TRUE(!shouldCloseLedger(true, 10, 0, 0, 10s, 10s, 3s, 10s, p)); + + EXPECT_TRUE(shouldCloseLedger(true, 10, 0, 0, 10s, 10s, 10s, 10s, p)); +} + +TEST(ConsensusTest, check_consensus) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("check consensus"); + + // Use default parameters + ConsensusParms const p{}; + + /////////////// + // Disputes still in doubt + // + // Not enough time has elapsed + EXPECT_TRUE(ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 2s, false, p, true)); + + // If not enough peers have proposed, ensure + // more time for proposals + EXPECT_TRUE(ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true)); + + // Enough time has elapsed and we all agree + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true)); + + // Enough time has elapsed and we don't yet agree + EXPECT_TRUE(ConsensusState::No == checkConsensus(10, 2, 1, 0, 3s, 10s, false, p, true)); + + // Our peers have moved on + // Enough time has elapsed and we all agree + EXPECT_TRUE(ConsensusState::MovedOn == checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true)); + + // If no peers, don't agree until time has passed. + EXPECT_TRUE(ConsensusState::No == checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true)); + + // Agree if no peers and enough time has passed. + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(0, 0, 0, 0, 3s, 16s, false, p, true)); + + // Expire if too much time has passed without agreement + EXPECT_TRUE(ConsensusState::Expired == checkConsensus(10, 8, 1, 0, 1s, 19s, false, p, true)); + + /////////////// + // Stalled + // + // Not enough time has elapsed + EXPECT_TRUE(ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 2s, true, p, true)); + + // If not enough peers have proposed, ensure + // more time for proposals + EXPECT_TRUE(ConsensusState::No == checkConsensus(10, 2, 2, 0, 3s, 4s, true, p, true)); + + // Enough time has elapsed and we all agree + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(10, 2, 2, 0, 3s, 10s, true, p, true)); + + // Enough time has elapsed and we don't yet agree, but there's nothing + // left to dispute + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(10, 2, 1, 0, 3s, 10s, true, p, true)); + + // Our peers have moved on + // Enough time has elapsed and we all agree, nothing left to dispute + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(10, 2, 1, 8, 3s, 10s, true, p, true)); + + // If no peers, don't agree until time has passed. + EXPECT_TRUE(ConsensusState::No == checkConsensus(0, 0, 0, 0, 3s, 10s, true, p, true)); + + // Agree if no peers and enough time has passed. + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(0, 0, 0, 0, 3s, 16s, true, p, true)); + + // We are done if there's nothing left to dispute, no matter how much + // time has passed + EXPECT_TRUE(ConsensusState::Yes == checkConsensus(10, 8, 1, 0, 1s, 19s, true, p, true)); +} + +TEST(ConsensusTest, standalone) +{ + using namespace std::chrono_literals; + using namespace csf; + SCOPED_TRACE("standalone"); + + Sim s; + PeerGroup const peers = s.createGroup(1); + Peer* peer = peers[0]; + peer->targetLedgers = 1; + peer->start(); + peer->submit(Tx{1}); + + s.scheduler.step(); + + // Inspect that the proper ledger was created + auto const& lcl = peer->lastClosedLedger; + EXPECT_TRUE(peer->prevLedgerID() == lcl.id()); + EXPECT_TRUE(lcl.seq() == Ledger::Seq{1}); + EXPECT_TRUE(lcl.txs().size() == 1); + EXPECT_TRUE(lcl.txs().contains(Tx{1})); + EXPECT_TRUE(peer->prevProposers == 0); +} + +TEST(ConsensusTest, peers_agree) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("peers agree"); + + ConsensusParms const parms{}; + Sim sim; + PeerGroup peers = sim.createGroup(5); + + // Connected trust and network graphs with single fixed delay + peers.trustAndConnect(peers, round(0.2 * parms.ledgerGRANULARITY)); + + // everyone submits their own ID as a TX + for (Peer* p : peers) + p->submit(Tx(static_cast(p->id))); + + sim.run(1); + + // All peers are in sync + EXPECT_TRUE(sim.synchronized()); + if (sim.synchronized()) + { + for (Peer const* peer : peers) + { + auto const& lcl = peer->lastClosedLedger; + EXPECT_TRUE(lcl.id() == peer->prevLedgerID()); + EXPECT_TRUE(lcl.seq() == Ledger::Seq{1}); + // All peers proposed + EXPECT_TRUE(peer->prevProposers == peers.size() - 1); + // All transactions were accepted + for (std::uint32_t i = 0; i < peers.size(); ++i) + EXPECT_TRUE(lcl.txs().contains(Tx{i})); + } + } +} + +TEST(ConsensusTest, slow_peers) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("slow peers"); + + // Several tests of a complete trust graph with a subset of peers + // that have significantly longer network delays to the rest of the + // network + + // Test when a slow peer doesn't delay a consensus quorum (4/5 agree) + { + ConsensusParms const parms{}; + Sim sim; + PeerGroup slow = sim.createGroup(1); + PeerGroup fast = sim.createGroup(4); + PeerGroup network = fast + slow; + + // Fully connected trust graph + network.trust(network); + + // Fast and slow network connections + fast.connect(fast, round(0.2 * parms.ledgerGRANULARITY)); + + slow.connect(network, round(1.1 * parms.ledgerGRANULARITY)); + + // All peers submit their own ID as a transaction + for (Peer* peer : network) + peer->submit(Tx{static_cast(peer->id)}); + + sim.run(1); + + // Verify all peers have same LCL but are missing transaction 0 + // All peers are in sync even with a slower peer 0 + EXPECT_TRUE(sim.synchronized()); + if (sim.synchronized()) + { + for (Peer const* peer : network) + { + auto const& lcl = peer->lastClosedLedger; + EXPECT_TRUE(lcl.id() == peer->prevLedgerID()); + EXPECT_TRUE(lcl.seq() == Ledger::Seq{1}); + + EXPECT_TRUE(peer->prevProposers == network.size() - 1); + EXPECT_TRUE(peer->prevRoundTime == network[0]->prevRoundTime); + + EXPECT_TRUE(not lcl.txs().contains(Tx{0})); + for (std::uint32_t i = 2; i < network.size(); ++i) + EXPECT_TRUE(lcl.txs().contains(Tx{i})); + + // Tx 0 didn't make it + EXPECT_TRUE(peer->openTxs.contains(Tx{0})); + } + } + } + + // Test when the slow peers delay a consensus quorum (4/6 agree) + { + // Run two tests + // 1. The slow peers are participating in consensus + // 2. The slow peers are just observing + + for (auto isParticipant : {true, false}) + { + ConsensusParms const parms{}; + + Sim sim; + PeerGroup slow = sim.createGroup(2); + PeerGroup fast = sim.createGroup(4); + PeerGroup network = fast + slow; + + // Connected trust graph + network.trust(network); + + // Fast and slow network connections + fast.connect(fast, round(0.2 * parms.ledgerGRANULARITY)); + + slow.connect(network, round(1.1 * parms.ledgerGRANULARITY)); + + for (Peer* peer : slow) + peer->runAsValidator = isParticipant; + + // All peers submit their own ID as a transaction and relay it + // to peers + for (Peer* peer : network) + peer->submit(Tx{static_cast(peer->id)}); + + sim.run(1); + + EXPECT_TRUE(sim.synchronized()); + if (sim.synchronized()) + { + // Verify all peers have same LCL but are missing + // transaction 0,1 which was not received by all peers + // before the ledger closed + for (Peer const* peer : network) + { + // Closed ledger has all but transaction 0,1 + auto const& lcl = peer->lastClosedLedger; + EXPECT_TRUE(lcl.seq() == Ledger::Seq{1}); + EXPECT_TRUE(not lcl.txs().contains(Tx{0})); + EXPECT_TRUE(not lcl.txs().contains(Tx{1})); + for (std::uint32_t i = slow.size(); i < network.size(); ++i) + EXPECT_TRUE(lcl.txs().contains(Tx{i})); + + // Tx 0-1 didn't make it + EXPECT_TRUE(peer->openTxs.contains(Tx{0})); + EXPECT_TRUE(peer->openTxs.contains(Tx{1})); + } + + Peer const* slowPeer = slow[0]; + if (isParticipant) + { + EXPECT_TRUE(slowPeer->prevProposers == network.size() - 1); + } + else + { + EXPECT_TRUE(slowPeer->prevProposers == fast.size()); + } + + for (Peer const* peer : fast) + { + // Due to the network link delay settings + // Peer 0 initially proposes {0} + // Peer 1 initially proposes {1} + // Peers 2-5 initially propose {2,3,4,5} + // Since peers 2-5 agree, 4/6 > the initial 50% needed + // to include a disputed transaction, so Peer 0/1 switch + // to agree with those peers. Peer 0/1 then closes with + // an 80% quorum of agreeing positions (5/6) match. + // + // Peers 2-5 do not change position, since tx 0 or tx 1 + // have less than the 50% initial threshold. They also + // cannot declare consensus, since 4/6 agreeing + // positions are < 80% threshold. They therefore need an + // additional timerEntry call to see the updated + // positions from Peer 0 & 1. + + if (isParticipant) + { + EXPECT_TRUE(peer->prevProposers == network.size() - 1); + EXPECT_TRUE(peer->prevRoundTime > slowPeer->prevRoundTime); + } + else + { + EXPECT_TRUE(peer->prevProposers == fast.size() - 1); + // so all peers should have closed together + EXPECT_TRUE(peer->prevRoundTime == slowPeer->prevRoundTime); + } + } + } + } + } +} + +TEST(ConsensusTest, close_time_disagree) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("close time disagree"); + + // This is a very specialized test to get ledgers to disagree on + // the close time. It unfortunately assumes knowledge about current + // timing constants. This is a necessary evil to get coverage up + // pending more extensive refactorings of timing constants. + + // In order to agree-to-disagree on the close time, there must be no + // clear majority of nodes agreeing on a close time. This test + // sets a relative offset to the peers internal clocks so that they + // send proposals with differing times. + + // However, agreement is on the effective close time, not the + // exact close time. The minimum closeTimeResolution is given by + // ledgerPossibleTimeResolutions[0], which is currently 10s. This means + // the skews need to be at least 10 seconds to have different effective + // close times. + + // Complicating this matter is that nodes will ignore proposals + // with times more than proposeFRESHNESS =20s in the past. So at + // the minimum granularity, we have at most 3 types of skews + // (0s,10s,20s). + + // This test therefore has 6 nodes, with 2 nodes having each type of + // skew. Then no majority (1/3 < 1/2) of nodes will agree on an + // actual close time. + + ConsensusParms const parms{}; + Sim sim; + + PeerGroup groupA = sim.createGroup(2); + PeerGroup const groupB = sim.createGroup(2); + PeerGroup const groupC = sim.createGroup(2); + PeerGroup network = groupA + groupB + groupC; + + network.trust(network); + network.connect(network, round(0.2 * parms.ledgerGRANULARITY)); + + // Run consensus without skew until we have a short close time + // resolution + Peer const* firstPeer = *groupA.begin(); + while (firstPeer->lastClosedLedger.closeTimeResolution() >= parms.proposeFRESHNESS) + sim.run(1); + + // Introduce a shift on the time of 2/3 of peers + for (Peer* peer : groupA) + peer->clockSkew = parms.proposeFRESHNESS / 2; + for (Peer* peer : groupB) + peer->clockSkew = parms.proposeFRESHNESS; + + sim.run(1); + + // All nodes agreed to disagree on the close time + EXPECT_TRUE(sim.synchronized()); + if (sim.synchronized()) + { + for (Peer const* peer : network) + EXPECT_TRUE(!peer->lastClosedLedger.closeAgree()); + } +} + +TEST(ConsensusTest, wrong_lcl) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("wrong LCL"); + + // Specialized test to exercise a temporary fork in which some peers + // are working on an incorrect prior ledger. + + ConsensusParms const parms{}; + + // Vary the time it takes to process validations to exercise detecting + // the wrong LCL at different phases of consensus + for (auto validationDelay : {0ms, parms.ledgerMinClose}) + { + // Consider 10 peers: + // 0 1 2 3 4 5 6 7 8 9 + // minority majorityA majorityB + // + // Nodes 0-1 trust nodes 0-4 + // Nodes 2-9 trust nodes 2-9 + // + // By submitting tx 0 to nodes 0-4 and tx 1 to nodes 5-9, + // nodes 0-1 will generate the wrong LCL (with tx 0). The remaining + // nodes will instead accept the ledger with tx 1. + + // Nodes 0-1 will detect this mismatch during a subsequent round + // since nodes 2-4 will validate a different ledger. + + // Nodes 0-1 will acquire the proper ledger from the network and + // resume consensus and eventually generate the dominant network + // ledger. + + // This topology can potentially fork with the above trust relations + // but that is intended for this test. + + Sim sim; + + PeerGroup minority = sim.createGroup(2); + PeerGroup const majorityA = sim.createGroup(3); + PeerGroup const majorityB = sim.createGroup(5); + + PeerGroup majority = majorityA + majorityB; + PeerGroup const network = minority + majority; + + SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); + minority.trustAndConnect(minority + majorityA, delay); + majority.trustAndConnect(majority, delay); + + CollectByNode jumps; + sim.collectors.add(jumps); + + EXPECT_TRUE(sim.trustGraph.canFork(parms.minConsensusPct / 100.)); + + // initial round to set prior state + sim.run(1); + + // Nodes in smaller UNL have seen tx 0, nodes in other unl have seen + // tx 1 + for (Peer* peer : network) + peer->delays.recvValidation = validationDelay; + for (Peer* peer : (minority + majorityA)) + peer->openTxs.insert(Tx{0}); + for (Peer* peer : majorityB) + peer->openTxs.insert(Tx{1}); + + // Run for additional rounds + // With no validation delay, only 2 more rounds are needed. + // 1. Round to generate different ledgers + // 2. Round to detect different prior ledgers (but still generate + // wrong ones) and recover within that round since wrong LCL + // is detected before we close + // + // With a validation delay of ledgerMinClose, we need 3 more + // rounds. + // 1. Round to generate different ledgers + // 2. Round to detect different prior ledgers (but still generate + // wrong ones) but end up declaring consensus on wrong LCL (but + // with the right transaction set!). This is because we detect + // the wrong LCL after we have closed the ledger, so we declare + // consensus based solely on our peer proposals. But we haven't + // had time to acquire the right ledger. + // 3. Round to correct + sim.run(3); + + // The network never actually forks, since node 0-1 never see a + // quorum of validations to fully validate the incorrect chain. + + // However, for a non zero-validation delay, the network is not + // synchronized because nodes 0 and 1 are running one ledger behind + EXPECT_TRUE(sim.branches() == 1); + if (sim.branches() == 1) + { + for (Peer const* peer : majority) + { + // No jumps for majority nodes + EXPECT_TRUE(jumps[peer->id].closeJumps.empty()); + EXPECT_TRUE(jumps[peer->id].fullyValidatedJumps.empty()); + } + for (Peer const* peer : minority) + { + auto& peerJumps = jumps[peer->id]; + // last closed ledger jump between chains + { + EXPECT_TRUE(peerJumps.closeJumps.size() == 1); + if (peerJumps.closeJumps.size() == 1) + { + JumpCollector::Jump const& jump = peerJumps.closeJumps.front(); + // Jump is to a different chain + EXPECT_TRUE(jump.from.seq() <= jump.to.seq()); + EXPECT_TRUE(!jump.to.isAncestor(jump.from)); + } + } + // fully validated jump forward in same chain + { + EXPECT_TRUE(peerJumps.fullyValidatedJumps.size() == 1); + if (peerJumps.fullyValidatedJumps.size() == 1) + { + JumpCollector::Jump const& jump = peerJumps.fullyValidatedJumps.front(); + // Jump is to a different chain with same seq + EXPECT_TRUE(jump.from.seq() < jump.to.seq()); + EXPECT_TRUE(jump.to.isAncestor(jump.from)); + } + } + } + } + } + + { + // Additional test engineered to switch LCL during the establish + // phase. This was added to trigger a scenario that previously + // crashed, in which switchLCL switched from establish to open + // phase, but still processed the establish phase logic. + + // Loner node will accept an initial ledger A, but all other nodes + // accept ledger B a bit later. By delaying the time it takes + // to process a validation, loner node will detect the wrongLCL + // after it is already in the establish phase of the next round. + + Sim sim; + PeerGroup loner = sim.createGroup(1); + PeerGroup const friends = sim.createGroup(3); + loner.trust(loner + friends); + + PeerGroup const others = sim.createGroup(6); + PeerGroup clique = friends + others; + clique.trust(clique); + + PeerGroup network = loner + clique; + network.connect(network, round(0.2 * parms.ledgerGRANULARITY)); + + // initial round to set prior state + sim.run(1); + for (Peer* peer : (loner + friends)) + peer->openTxs.insert(Tx(0)); + for (Peer* peer : others) + peer->openTxs.insert(Tx(1)); + + // Delay validation processing + for (Peer* peer : network) + peer->delays.recvValidation = parms.ledgerGRANULARITY; + + // additional rounds to generate wrongLCL and recover + sim.run(2); + + // Check all peers recovered + for (Peer const* p : network) + EXPECT_TRUE(p->prevLedgerID() == network[0]->prevLedgerID()); + } +} + +TEST(ConsensusTest, consensus_close_time_rounding) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("consensus close time rounding"); + + // This is a specialized test engineered to yield ledgers with different + // close times even though the peers believe they had close time + // consensus on the ledger. + ConsensusParms const parms; + + Sim sim; + + // This requires a group of 4 fast and 2 slow peers to create a + // situation in which a subset of peers requires seeing additional + // proposals to declare consensus. + PeerGroup slow = sim.createGroup(2); + PeerGroup fast = sim.createGroup(4); + PeerGroup network = fast + slow; + + // Connected trust graph + network.trust(network); + + // Fast and slow network connections + fast.connect(fast, round(0.2 * parms.ledgerGRANULARITY)); + slow.connect(network, round(1.1 * parms.ledgerGRANULARITY)); + + // Run to the ledger *prior* to decreasing the resolution + sim.run(kIncreaseLedgerTimeResolutionEvery - 2); + + // In order to create the discrepancy, we want a case where if + // X = effCloseTime(closeTime, resolution, parentCloseTime) + // X != effCloseTime(X, resolution, parentCloseTime) + // + // That is, the effective close time is not a fixed point. This can + // happen if X = parentCloseTime + 1, but a subsequent rounding goes + // to the next highest multiple of resolution. + + // So we want to find an offset (now + offset) % 30s = 15 + // (now + offset) % 20s = 15 + // This way, the next ledger will close and round up Due to the + // network delay settings, the round of consensus will take 5s, so + // the next ledger's close time will + + NetClock::duration when = network[0]->now().time_since_epoch(); + + // Check we are before the 30s to 20s transition + NetClock::duration const resolution = network[0]->lastClosedLedger.closeTimeResolution(); + EXPECT_TRUE(resolution == NetClock::duration{30s}); + + while (((when % NetClock::duration{30s}) != NetClock::duration{15s}) || + ((when % NetClock::duration{20s}) != NetClock::duration{15s})) + when += 1s; + // Advance the clock without consensus running (IS THIS WHAT + // PREVENTS IT IN PRACTICE?) + sim.scheduler.stepFor(NetClock::time_point{when} - network[0]->now()); + + // Run one more ledger with 30s resolution + sim.run(1); + EXPECT_TRUE(sim.synchronized()); + if (sim.synchronized()) + { + // close time should be ahead of clock time since we engineered + // the close time to round up + for (Peer const* peer : network) + { + EXPECT_TRUE(peer->lastClosedLedger.closeTime() > peer->now()); + EXPECT_TRUE(peer->lastClosedLedger.closeAgree()); + } + } + + // All peers submit their own ID as a transaction + for (Peer* peer : network) + peer->submit(Tx{static_cast(peer->id)}); + + // Run 1 more round, this time it will have a decreased + // resolution of 20 seconds. + + // The network delays are engineered so that the slow peers + // initially have the wrong tx hash, but they see a majority + // of agreement from their peers and declare consensus + // + // The trick is that everyone starts with a raw close time of + // 84681s + // Which has + // effCloseTime(86481s, 20s, 86490s) = 86491s + // However, when the slow peers update their position, they change + // the close time to 86451s. The fast peers declare consensus with + // the 86481s as their position still. + // + // When accepted the ledger + // - fast peers use eff(86481s) -> 86491s as the close time + // - slow peers use eff(eff(86481s)) -> eff(86491s) -> 86500s! + + sim.run(1); + + EXPECT_TRUE(sim.synchronized()); +} + +TEST(ConsensusTest, fork) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("fork"); + + std::uint32_t const numPeers = 10; + // Vary overlap between two UNLs + for (std::uint32_t overlap = 0; overlap <= numPeers; ++overlap) + { + ConsensusParms const parms{}; + Sim sim; + + std::uint32_t const numA = (numPeers - overlap) / 2; + std::uint32_t const numB = numPeers - numA - overlap; + + PeerGroup const aOnly = sim.createGroup(numA); + PeerGroup const bOnly = sim.createGroup(numB); + PeerGroup const commonOnly = sim.createGroup(overlap); + + PeerGroup a = aOnly + commonOnly; + PeerGroup b = bOnly + commonOnly; + + PeerGroup const network = a + b; + + SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); + a.trustAndConnect(a, delay); + b.trustAndConnect(b, delay); + + // Initial round to set prior state + sim.run(1); + for (Peer* peer : network) + { + // Nodes have only seen transactions from their neighbors + peer->openTxs.insert(Tx{static_cast(peer->id)}); + for (Peer const* to : sim.trustGraph.trustedPeers(peer)) + peer->openTxs.insert(Tx{static_cast(to->id)}); + } + sim.run(1); + + // Fork should not happen for 40% or greater overlap + // Since the overlapped nodes have a UNL that is the union of the + // two cliques, the maximum sized UNL list is the number of peers + if (overlap > 0.4 * numPeers) + { + EXPECT_TRUE(sim.synchronized()); + } + else + { + // Even if we do fork, there shouldn't be more than 3 ledgers + // One for cliqueA, one for cliqueB and one for nodes in both + EXPECT_TRUE(sim.branches() <= 3); + } + } +} + +TEST(ConsensusTest, hub_network) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("hub network"); + + // Simulate a set of 5 validators that aren't directly connected but + // rely on a single hub node for communication + + ConsensusParms const parms{}; + Sim sim; + PeerGroup validators = sim.createGroup(5); + PeerGroup center = sim.createGroup(1); + validators.trust(validators); + center.trust(validators); + + SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); + validators.connect(center, delay); + + center[0]->runAsValidator = false; + + // prep round to set initial state. + sim.run(1); + + // everyone submits their own ID as a TX and relay it to peers + for (Peer* p : validators) + p->submit(Tx(static_cast(p->id))); + + sim.run(1); + + // All peers are in sync + EXPECT_TRUE(sim.synchronized()); +} + +TEST(ConsensusTest, preferred_by_branch) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("preferred by branch"); + + // Simulate network splits that are prevented from forking when using + // preferred ledger by trie. This is a contrived example that involves + // excessive network splits, but demonstrates the safety improvement + // from the preferred ledger by trie approach. + + // Consider 10 validating nodes that comprise a single common UNL + // Ledger history: + // 1: A + // _/ \_ + // 2: B C + // _/ _/ \_ + // 3: D C' |||||||| (8 different ledgers) + + // - All nodes generate the common ledger A + // - 2 nodes generate B and 8 nodes generate C + // - Only 1 of the C nodes sees all the C validations and fully + // validates C. The rest of the C nodes split at just the right time + // such that they never see any C validations but their own. + // - The C nodes continue and generate 8 different child ledgers. + // - Meanwhile, the D nodes only saw 1 validation for C and 2 + // validations + // for B. + // - The network reconnects and the validations for generation 3 ledgers + // are observed (D and the 8 C's) + // - In the old approach, 2 votes for D outweighs 1 vote for each C' + // so the network would avalanche towards D and fully validate it + // EVEN though C was fully validated by one node + // - In the new approach, 2 votes for D are not enough to outweight the + // 8 implicit votes for C, so nodes will avalanche to C instead + + ConsensusParms const parms{}; + Sim sim; + + // Goes A->B->D + PeerGroup const groupABD = sim.createGroup(2); + // Single node that initially fully validates C before the split + PeerGroup groupCfast = sim.createGroup(1); + // Generates C, but fails to fully validate before the split + PeerGroup groupCsplit = sim.createGroup(7); + + PeerGroup groupNotFastC = groupABD + groupCsplit; + PeerGroup network = groupABD + groupCsplit + groupCfast; + + SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); + SimDuration const fDelay = round(0.1 * parms.ledgerGRANULARITY); + + network.trust(network); + // C must have a shorter delay to see all the validations before the + // other nodes + network.connect(groupCfast, fDelay); + // The rest of the network is connected at the same speed + groupNotFastC.connect(groupNotFastC, delay); + + Disruptor dc(network, groupCfast, groupCsplit, delay); + sim.collectors.add(dc); + + // Consensus round to generate ledger A + sim.run(1); + EXPECT_TRUE(sim.synchronized()); + + // Next round generates B and C + // To force B, we inject an extra transaction in to those nodes + for (Peer* peer : groupABD) + { + peer->txInjections.emplace(peer->lastClosedLedger.seq(), Tx{42}); + } + // The Disruptor will ensure that nodes disconnect before the C + // validations make it to all but the fastC node + sim.run(1); + + // We are no longer in sync, but have not yet forked: + // 9 nodes consider A the last fully validated ledger and fastC sees C + EXPECT_TRUE(!sim.synchronized()); + EXPECT_TRUE(sim.branches() == 1); + + // Run another round to generate the 8 different C' ledgers + for (Peer* p : network) + p->submit(Tx(static_cast(p->id))); + sim.run(1); + + // Still not forked + EXPECT_TRUE(!sim.synchronized()); + EXPECT_TRUE(sim.branches() == 1); + + // Disruptor will reconnect all but the fastC node + sim.run(1); + + EXPECT_TRUE(sim.branches() == 1); + if (sim.branches() == 1) + { + EXPECT_TRUE(sim.synchronized()); + } + else // old approach caused a fork + { + EXPECT_TRUE(sim.branches(groupNotFastC) == 1); + EXPECT_TRUE(sim.synchronized(groupNotFastC) == 1); + } +} + +TEST(ConsensusTest, pause_for_laggards) +{ + using namespace csf; + using namespace std::chrono; + SCOPED_TRACE("pause for laggards"); + + // Test that validators that jump ahead of the network slow + // down. + + // We engineer the following validated ledger history scenario: + // + // / --> B1 --> C1 --> ... -> G1 "ahead" + // A + // \ --> B2 --> C2 "behind" + // + // After validating a common ledger A, a set of "behind" validators + // briefly run slower and validate the lower chain of ledgers. + // The "ahead" validators run normal speed and run ahead validating the + // upper chain of ledgers. + // + // Due to the uncommitted support definition of the preferred branch + // protocol, even if the "behind" validators are a majority, the "ahead" + // validators cannot jump to the proper branch until the "behind" + // validators catch up to the same sequence number. For this test to + // succeed, the ahead validators need to briefly slow down consensus. + + ConsensusParms const parms{}; + Sim sim; + SimDuration const delay = round(0.2 * parms.ledgerGRANULARITY); + + PeerGroup behind = sim.createGroup(3); + PeerGroup const ahead = sim.createGroup(2); + PeerGroup network = ahead + behind; + + hash_set trustedKeys; + for (Peer const* p : network) + trustedKeys.insert(p->key); + for (Peer* p : network) + p->trustedKeys = trustedKeys; + + network.trustAndConnect(network, delay); + + // Initial seed round to set prior state + sim.run(1); + + // Have the "behind" group initially take a really long time to + // accept a ledger after ending deliberation + for (Peer* p : behind) + p->delays.ledgerAccept = 20s; + + // Use the collector to revert the delay after the single + // slow ledger is generated + UndoDelay undoDelay{behind}; + sim.collectors.add(undoDelay); + + // Run the simulation for 100 seconds of simulation time with + std::chrono::nanoseconds const simDuration = 100s; + + // Simulate clients submitting 1 tx every 5 seconds to a random + // validator + Rate const rate{.count = 1, .duration = 5s}; + auto peerSelector = makeSelector( + network.begin(), network.end(), std::vector(network.size(), 1.), sim.rng); + auto txSubmitter = makeSubmitter( + ConstantDistribution{rate.inv()}, + sim.scheduler.now(), + sim.scheduler.now() + simDuration, + peerSelector, + sim.scheduler, + sim.rng); + + // Run simulation + sim.run(simDuration); + + // Verify that the network recovered + EXPECT_TRUE(sim.synchronized()); +} + +TEST(ConsensusTest, disputes) +{ + SCOPED_TRACE("disputes"); + + using namespace csf; + + // Test dispute objects directly + using Dispute = CsfDisputedTx; + + Tx const txTrue{99}; + Tx const txFalse{98}; + Tx const txFollowingTrue{97}; + Tx const txFollowingFalse{96}; + int const numPeers = 100; + ConsensusParms const p; + std::size_t peersUnchanged = 0; + + auto clog = std::make_unique(); + + // Three cases: + // 1 proposing, initial vote yes + // 2 proposing, initial vote no + // 3 not proposing, initial vote doesn't matter after the first update, + // use yes + { + Dispute proposingTrue = makeDisputedTx(txTrue, true, numPeers); + Dispute proposingFalse = makeDisputedTx(txFalse, false, numPeers); + Dispute followingTrue = makeDisputedTx(txFollowingTrue, true, numPeers); + Dispute followingFalse = makeDisputedTx(txFollowingFalse, false, numPeers); + EXPECT_TRUE(proposingTrue.id() == 99); + EXPECT_TRUE(proposingFalse.id() == 98); + EXPECT_TRUE(followingTrue.id() == 97); + EXPECT_TRUE(followingFalse.id() == 96); + + // Create an even split in the peer votes + for (int i = 0; i < numPeers; ++i) + { + EXPECT_TRUE(proposingTrue.setVote(PeerID(i), i < 50)); + EXPECT_TRUE(proposingFalse.setVote(PeerID(i), i < 50)); + EXPECT_TRUE(followingTrue.setVote(PeerID(i), i < 50)); + EXPECT_TRUE(followingFalse.setVote(PeerID(i), i < 50)); + } + // Switch the middle vote to match mine + EXPECT_TRUE(proposingTrue.setVote(PeerID(50), true)); + EXPECT_TRUE(proposingFalse.setVote(PeerID(49), false)); + EXPECT_TRUE(followingTrue.setVote(PeerID(50), true)); + EXPECT_TRUE(followingFalse.setVote(PeerID(49), false)); + + // no changes yet + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + EXPECT_TRUE(!isStalled(proposingTrue, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(proposingFalse, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingTrue, p, false, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingFalse, p, false, peersUnchanged, clog)); + EXPECT_TRUE(clog->str().empty()); + + // I'm in the majority, my vote should not change + EXPECT_TRUE(!proposingTrue.updateVote(5, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(5, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(5, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(5, false, p)); + + EXPECT_TRUE(!proposingTrue.updateVote(10, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(10, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(10, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(10, false, p)); + + peersUnchanged = 2; + EXPECT_TRUE(!isStalled(proposingTrue, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(proposingFalse, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingTrue, p, false, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingFalse, p, false, peersUnchanged, clog)); + EXPECT_TRUE(clog->str().empty()); + + // Right now, the vote is 51%. The requirement is about to jump to + // 65% + EXPECT_TRUE(proposingTrue.updateVote(55, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(55, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(55, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(55, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == false); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + // 16 validators change their vote to match my original vote + for (int i = 0; i < 16; ++i) + { + auto pTrue = PeerID(numPeers - i - 1); + auto pFalse = PeerID(i); + EXPECT_TRUE(proposingTrue.setVote(pTrue, true)); + EXPECT_TRUE(proposingFalse.setVote(pFalse, false)); + EXPECT_TRUE(followingTrue.setVote(pTrue, true)); + EXPECT_TRUE(followingFalse.setVote(pFalse, false)); + } + // The vote should now be 66%, threshold is 65% + EXPECT_TRUE(proposingTrue.updateVote(60, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(60, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(60, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(60, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // Threshold jumps to 70% + EXPECT_TRUE(proposingTrue.updateVote(86, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(86, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(86, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(86, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == false); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // 5 more validators change their vote to match my original vote + for (int i = 16; i < 21; ++i) + { + auto pTrue = PeerID(numPeers - i - 1); + auto pFalse = PeerID(i); + EXPECT_TRUE(proposingTrue.setVote(pTrue, true)); + EXPECT_TRUE(proposingFalse.setVote(pFalse, false)); + EXPECT_TRUE(followingTrue.setVote(pTrue, true)); + EXPECT_TRUE(followingFalse.setVote(pFalse, false)); + } + + // The vote should now be 71%, threshold is 70% + EXPECT_TRUE(proposingTrue.updateVote(90, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(90, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(90, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(90, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // The vote should now be 71%, threshold is 70% + EXPECT_TRUE(!proposingTrue.updateVote(150, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(150, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(150, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(150, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // The vote should now be 71%, threshold is 70% + EXPECT_TRUE(!proposingTrue.updateVote(190, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(190, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(190, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(190, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + peersUnchanged = 3; + EXPECT_TRUE(!isStalled(proposingTrue, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(proposingFalse, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingTrue, p, false, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingFalse, p, false, peersUnchanged, clog)); + EXPECT_TRUE(clog->str().empty()); + + // Threshold jumps to 95% + EXPECT_TRUE(proposingTrue.updateVote(220, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(220, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(220, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(220, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == false); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // 25 more validators change their vote to match my original vote + for (int i = 21; i < 46; ++i) + { + auto pTrue = PeerID(numPeers - i - 1); + auto pFalse = PeerID(i); + EXPECT_TRUE(proposingTrue.setVote(pTrue, true)); + EXPECT_TRUE(proposingFalse.setVote(pFalse, false)); + EXPECT_TRUE(followingTrue.setVote(pTrue, true)); + EXPECT_TRUE(followingFalse.setVote(pFalse, false)); + } + + // The vote should now be 96%, threshold is 95% + EXPECT_TRUE(proposingTrue.updateVote(250, true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(250, true, p)); + EXPECT_TRUE(!followingTrue.updateVote(250, false, p)); + EXPECT_TRUE(!followingFalse.updateVote(250, false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + for (peersUnchanged = 0; peersUnchanged < 6; ++peersUnchanged) + { + EXPECT_TRUE(!isStalled(proposingTrue, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(proposingFalse, p, true, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingTrue, p, false, peersUnchanged, clog)); + EXPECT_TRUE(!isStalled(followingFalse, p, false, peersUnchanged, clog)); + EXPECT_TRUE(clog->str().empty()); + } + + auto expectStalled = [&clog]( + int txid, + bool ourVote, + int ourTime, + int peerTime, + int support, + std::uint32_t line) { + using namespace std::string_literals; + + auto const s = clog->str(); + SCOPED_TRACE(::testing::Message() << __FILE__ << ":" << line); + EXPECT_NE(s.find("stalled"), s.npos) << s; + EXPECT_TRUE(s.starts_with("Transaction "s + std::to_string(txid))) << s; + EXPECT_NE(s.find("voting "s + (ourVote ? "YES" : "NO")), s.npos) << s; + EXPECT_NE(s.find("for "s + std::to_string(ourTime) + " rounds."s), s.npos) << s; + EXPECT_NE(s.find("votes in "s + std::to_string(peerTime) + " rounds."), s.npos) << s; + EXPECT_TRUE(s.ends_with("has "s + std::to_string(support) + "% support. "s)) << s; + clog = std::make_unique(); + }; + + for (int i = 0; i < 1; ++i) + { + EXPECT_TRUE(!proposingTrue.updateVote(250 + (10 * i), true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(250 + (10 * i), true, p)); + EXPECT_TRUE(!followingTrue.updateVote(250 + (10 * i), false, p)); + EXPECT_TRUE(!followingFalse.updateVote(250 + (10 * i), false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // true vote has changed recently, so not stalled + EXPECT_TRUE(!isStalled(proposingTrue, p, true, 0, clog)); + EXPECT_TRUE(clog->str().empty()); + // remaining votes have been unchanged in so long that we only + // need to hit the second round at 95% to be stalled, regardless + // of peers + EXPECT_TRUE(isStalled(proposingFalse, p, true, 0, clog)); + expectStalled(98, false, 11, 0, 2, __LINE__); + EXPECT_TRUE(isStalled(followingTrue, p, false, 0, clog)); + expectStalled(97, true, 11, 0, 97, __LINE__); + EXPECT_TRUE(isStalled(followingFalse, p, false, 0, clog)); + expectStalled(96, false, 11, 0, 3, __LINE__); + + // true vote has changed recently, so not stalled + EXPECT_TRUE(!isStalled(proposingTrue, p, true, peersUnchanged, clog)); + EXPECT_TRUE(clog->str().empty()) << clog->str(); + // remaining votes have been unchanged in so long that we only + // need to hit the second round at 95% to be stalled, regardless + // of peers + EXPECT_TRUE(isStalled(proposingFalse, p, true, peersUnchanged, clog)); + expectStalled(98, false, 11, 6, 2, __LINE__); + EXPECT_TRUE(isStalled(followingTrue, p, false, peersUnchanged, clog)); + expectStalled(97, true, 11, 6, 97, __LINE__); + EXPECT_TRUE(isStalled(followingFalse, p, false, peersUnchanged, clog)); + expectStalled(96, false, 11, 6, 3, __LINE__); + } + for (int i = 1; i < 3; ++i) + { + EXPECT_TRUE(!proposingTrue.updateVote(250 + (10 * i), true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(250 + (10 * i), true, p)); + EXPECT_TRUE(!followingTrue.updateVote(250 + (10 * i), false, p)); + EXPECT_TRUE(!followingFalse.updateVote(250 + (10 * i), false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + // true vote changed 2 rounds ago, and peers are changing, so + // not stalled + EXPECT_TRUE(!isStalled(proposingTrue, p, true, 0, clog)); + EXPECT_TRUE(clog->str().empty()) << clog->str(); + // still stalled + EXPECT_TRUE(isStalled(proposingFalse, p, true, 0, clog)); + expectStalled(98, false, 11 + i, 0, 2, __LINE__); + EXPECT_TRUE(isStalled(followingTrue, p, false, 0, clog)); + expectStalled(97, true, 11 + i, 0, 97, __LINE__); + EXPECT_TRUE(isStalled(followingFalse, p, false, 0, clog)); + expectStalled(96, false, 11 + i, 0, 3, __LINE__); + + // true vote changed 2 rounds ago, and peers are NOT changing, + // so stalled + EXPECT_TRUE(isStalled(proposingTrue, p, true, peersUnchanged, clog)); + expectStalled(99, true, 1 + i, 6, 97, __LINE__); + // still stalled + EXPECT_TRUE(isStalled(proposingFalse, p, true, peersUnchanged, clog)); + expectStalled(98, false, 11 + i, 6, 2, __LINE__); + EXPECT_TRUE(isStalled(followingTrue, p, false, peersUnchanged, clog)); + expectStalled(97, true, 11 + i, 6, 97, __LINE__); + EXPECT_TRUE(isStalled(followingFalse, p, false, peersUnchanged, clog)); + expectStalled(96, false, 11 + i, 6, 3, __LINE__); + } + for (int i = 3; i < 5; ++i) + { + EXPECT_TRUE(!proposingTrue.updateVote(250 + (10 * i), true, p)); + EXPECT_TRUE(!proposingFalse.updateVote(250 + (10 * i), true, p)); + EXPECT_TRUE(!followingTrue.updateVote(250 + (10 * i), false, p)); + EXPECT_TRUE(!followingFalse.updateVote(250 + (10 * i), false, p)); + + EXPECT_TRUE(proposingTrue.getOurVote() == true); + EXPECT_TRUE(proposingFalse.getOurVote() == false); + EXPECT_TRUE(followingTrue.getOurVote() == true); + EXPECT_TRUE(followingFalse.getOurVote() == false); + + EXPECT_TRUE(isStalled(proposingTrue, p, true, 0, clog)); + expectStalled(99, true, 1 + i, 0, 97, __LINE__); + EXPECT_TRUE(isStalled(proposingFalse, p, true, 0, clog)); + expectStalled(98, false, 11 + i, 0, 2, __LINE__); + EXPECT_TRUE(isStalled(followingTrue, p, false, 0, clog)); + expectStalled(97, true, 11 + i, 0, 97, __LINE__); + EXPECT_TRUE(isStalled(followingFalse, p, false, 0, clog)); + expectStalled(96, false, 11 + i, 0, 3, __LINE__); + + EXPECT_TRUE(isStalled(proposingTrue, p, true, peersUnchanged, clog)); + expectStalled(99, true, 1 + i, 6, 97, __LINE__); + EXPECT_TRUE(isStalled(proposingFalse, p, true, peersUnchanged, clog)); + expectStalled(98, false, 11 + i, 6, 2, __LINE__); + EXPECT_TRUE(isStalled(followingTrue, p, false, peersUnchanged, clog)); + expectStalled(97, true, 11 + i, 6, 97, __LINE__); + EXPECT_TRUE(isStalled(followingFalse, p, false, peersUnchanged, clog)); + expectStalled(96, false, 11 + i, 6, 3, __LINE__); + } + } +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/DistributedValidatorsSim.cpp b/src/tests/libxrpl/consensus/DistributedValidatorsSim.cpp new file mode 100644 index 0000000000..f5ba508cbc --- /dev/null +++ b/src/tests/libxrpl/consensus/DistributedValidatorsSim.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +namespace { + +[[nodiscard]] std::string const& +arg() +{ + static std::string const kEMPTY; + return kEMPTY; +} + +void +completeTrustCompleteConnectFixedDelay( + std::size_t numPeers, + std::chrono::milliseconds delay = std::chrono::milliseconds(200), + bool printHeaders = false) +{ + using namespace csf; + using namespace std::chrono; + + // Initialize persistent collector logs specific to this method + std::string const prefix = + "DistributedValidators_" + "completeTrustCompleteConnectFixedDelay"; + std::fstream txLog(prefix + "_tx.csv", std::ofstream::app), + ledgerLog(prefix + "_ledger.csv", std::ofstream::app); + + // title + std::cout << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl; + + // number of peers, UNLs, connections + EXPECT_TRUE(numPeers >= 1); + + Sim sim; + PeerGroup peers = sim.createGroup(numPeers); + + // complete trust graph + peers.trust(peers); + + // complete connect graph with fixed delay + peers.connect(peers, delay); + + // Initialize collectors to track statistics to report + TxCollector txCollector; + LedgerCollector ledgerCollector; + auto colls = makeCollectors(txCollector, ledgerCollector); + sim.collectors.add(colls); + + // Initial round to set prior state + sim.run(1); + + // Run for 10 minutes, submitting 100 tx/second + std::chrono::nanoseconds const simDuration = 10min; + std::chrono::nanoseconds const quiet = 10s; + Rate const rate{.count = 100, .duration = 1000ms}; + + // Initialize timers + HeartbeatTimer heart(sim.scheduler); + + // txs, start/stop/step, target + auto peerSelector = + makeSelector(peers.begin(), peers.end(), std::vector(numPeers, 1.), sim.rng); + auto txSubmitter = makeSubmitter( + ConstantDistribution{rate.inv()}, + sim.scheduler.now() + quiet, + sim.scheduler.now() + simDuration - quiet, + peerSelector, + sim.scheduler, + sim.rng); + + // run simulation for given duration + heart.start(); + sim.run(simDuration); + + // EXPECT_TRUE(sim.branches() == 1); + // EXPECT_TRUE(sim.synchronized()); + + std::cout << std::right; + std::cout << "| Peers: " << std::setw(2) << peers.size(); + std::cout << " | Duration: " << std::setw(6) << duration_cast(simDuration).count() + << " ms"; + std::cout << " | Branches: " << std::setw(1) << sim.branches(); + std::cout << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N"); + std::cout << " |" << std::endl; + + txCollector.report(simDuration, std::cout, true); + ledgerCollector.report(simDuration, std::cout, false); + + std::string const tag = std::to_string(numPeers); + txCollector.csv(simDuration, txLog, tag, printHeaders); + ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders); + + std::cout << std::endl; +} + +void +completeTrustScaleFreeConnectFixedDelay( + std::size_t numPeers, + std::chrono::milliseconds delay = std::chrono::milliseconds(200), + bool printHeaders = false) +{ + using namespace csf; + using namespace std::chrono; + + // Initialize persistent collector logs specific to this method + std::string const prefix = + "DistributedValidators__" + "completeTrustScaleFreeConnectFixedDelay"; + std::fstream txLog(prefix + "_tx.csv", std::ofstream::app), + ledgerLog(prefix + "_ledger.csv", std::ofstream::app); + + // title + std::cout << prefix << "(" << numPeers << "," << delay.count() << ")" << std::endl; + + // number of peers, UNLs, connections + int const numCNLs = std::max(int(1.00 * numPeers), 1); + int const minCNLSize = std::max(int(0.25 * numCNLs), 1); + int const maxCNLSize = std::max(int(0.50 * numCNLs), 1); + EXPECT_TRUE(numPeers >= 1); + EXPECT_TRUE(numCNLs >= 1); + EXPECT_TRUE(1 <= minCNLSize && minCNLSize <= maxCNLSize && maxCNLSize <= numPeers); + + Sim sim; + PeerGroup peers = sim.createGroup(numPeers); + + // complete trust graph + peers.trust(peers); + + // scale-free connect graph with fixed delay + std::vector const ranks = sample(peers.size(), PowerLawDistribution{1, 3}, sim.rng); + randomRankedConnect( + peers, + ranks, + numCNLs, + std::uniform_int_distribution<>{minCNLSize, maxCNLSize}, + sim.rng, + delay); + + // Initialize collectors to track statistics to report + TxCollector txCollector; + LedgerCollector ledgerCollector; + auto colls = makeCollectors(txCollector, ledgerCollector); + sim.collectors.add(colls); + + // Initial round to set prior state + sim.run(1); + + // Run for 10 minutes, submitting 100 tx/second + std::chrono::nanoseconds const simDuration = 10min; + std::chrono::nanoseconds const quiet = 10s; + Rate const rate{.count = 100, .duration = 1000ms}; + + // Initialize timers + HeartbeatTimer heart(sim.scheduler); + + // txs, start/stop/step, target + auto peerSelector = + makeSelector(peers.begin(), peers.end(), std::vector(numPeers, 1.), sim.rng); + auto txSubmitter = makeSubmitter( + ConstantDistribution{rate.inv()}, + sim.scheduler.now() + quiet, + sim.scheduler.now() + simDuration - quiet, + peerSelector, + sim.scheduler, + sim.rng); + + // run simulation for given duration + heart.start(); + sim.run(simDuration); + + // EXPECT_TRUE(sim.branches() == 1); + // EXPECT_TRUE(sim.synchronized()); + + std::cout << std::right; + std::cout << "| Peers: " << std::setw(2) << peers.size(); + std::cout << " | Duration: " << std::setw(6) << duration_cast(simDuration).count() + << " ms"; + std::cout << " | Branches: " << std::setw(1) << sim.branches(); + std::cout << " | Synchronized: " << std::setw(1) << (sim.synchronized() ? "Y" : "N"); + std::cout << " |" << std::endl; + + txCollector.report(simDuration, std::cout, true); + ledgerCollector.report(simDuration, std::cout, false); + + std::string const tag = std::to_string(numPeers); + txCollector.csv(simDuration, txLog, tag, printHeaders); + ledgerCollector.csv(simDuration, ledgerLog, tag, printHeaders); + + std::cout << std::endl; +} + +} // namespace + +// In progress simulations for diversifying and distributing validators +TEST(DistributedValidatorsTest, DISABLED_distributed_validators) +{ + std::string const defaultArgs = "5 200"; + std::string const args = arg().empty() ? defaultArgs : arg(); + std::stringstream argStream(args); + + int maxNumValidators = 0; + int delayCount(200); + argStream >> maxNumValidators; + argStream >> delayCount; + + std::chrono::milliseconds const delay(delayCount); + + std::cout << "DistributedValidators: 1 to " << maxNumValidators << " Peers" << std::endl; + + // Simulate with N = 1 to N + // - complete trust graph is complete + // - complete network connectivity + // - fixed delay for network links + completeTrustCompleteConnectFixedDelay(1, delay, true); + for (int i = 2; i <= maxNumValidators; i++) + { + completeTrustCompleteConnectFixedDelay(i, delay); + } + + // Simulate with N = 1 to N + // - complete trust graph is complete + // - scale-free network connectivity + // - fixed delay for network links + completeTrustScaleFreeConnectFixedDelay(1, delay, true); + for (int i = 2; i <= maxNumValidators; i++) + { + completeTrustScaleFreeConnectFixedDelay(i, delay); + } +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/LedgerTiming.cpp b/src/tests/libxrpl/consensus/LedgerTiming.cpp new file mode 100644 index 0000000000..0cab895fda --- /dev/null +++ b/src/tests/libxrpl/consensus/LedgerTiming.cpp @@ -0,0 +1,105 @@ +#include + +#include + +#include + +#include +#include +#include + +namespace xrpl::test { + +TEST(LedgerTimingTest, get_next_ledger_time_resolution) +{ + // helper to iteratively call into getNextLedgerTimeResolution + struct TestRes + { + std::uint32_t decrease = 0; + std::uint32_t equal = 0; + std::uint32_t increase = 0; + + static TestRes + run(bool previousAgree, std::uint32_t rounds) + { + TestRes res; + auto closeResolution = kLedgerDefaultTimeResolution; + auto nextCloseResolution = closeResolution; + std::uint32_t round = 0; + do + { + nextCloseResolution = + getNextLedgerTimeResolution(closeResolution, previousAgree, ++round); + if (nextCloseResolution < closeResolution) + { + ++res.decrease; + } + else if (nextCloseResolution > closeResolution) + { + ++res.increase; + } + else + { + ++res.equal; + } + std::swap(nextCloseResolution, closeResolution); + } while (round < rounds); + return res; + } + }; + + // If we never agree on close time, only can increase resolution + // until hit the max + auto decreases = TestRes::run(false, 10); + EXPECT_TRUE(decreases.increase == 3); + EXPECT_TRUE(decreases.decrease == 0); + EXPECT_TRUE(decreases.equal == 7); + + // If we always agree on close time, only can decrease resolution + // until hit the min + auto increases = TestRes::run(false, 100); + EXPECT_TRUE(increases.increase == 3); + EXPECT_TRUE(increases.decrease == 0); + EXPECT_TRUE(increases.equal == 97); +} + +TEST(LedgerTimingTest, round_close_time) +{ + using namespace std::chrono_literals; + // A closeTime equal to the epoch is not modified + using tp = NetClock::time_point; + tp const def; + EXPECT_TRUE(def == roundCloseTime(def, 30s)); + + // Otherwise, the closeTime is rounded to the nearest + // rounding up on ties + EXPECT_TRUE(tp{0s} == roundCloseTime(tp{29s}, 60s)); + EXPECT_TRUE(tp{30s} == roundCloseTime(tp{30s}, 1s)); + EXPECT_TRUE(tp{60s} == roundCloseTime(tp{31s}, 60s)); + EXPECT_TRUE(tp{60s} == roundCloseTime(tp{30s}, 60s)); + EXPECT_TRUE(tp{60s} == roundCloseTime(tp{59s}, 60s)); + EXPECT_TRUE(tp{60s} == roundCloseTime(tp{60s}, 60s)); + EXPECT_TRUE(tp{60s} == roundCloseTime(tp{61s}, 60s)); +} + +TEST(LedgerTimingTest, eff_close_time) +{ + using namespace std::chrono_literals; + using tp = NetClock::time_point; + tp close = effCloseTime(tp{10s}, 30s, tp{0s}); + EXPECT_TRUE(close == tp{1s}); + + close = effCloseTime(tp{16s}, 30s, tp{0s}); + EXPECT_TRUE(close == tp{30s}); + + close = effCloseTime(tp{16s}, 30s, tp{30s}); + EXPECT_TRUE(close == tp{31s}); + + close = effCloseTime(tp{16s}, 30s, tp{60s}); + EXPECT_TRUE(close == tp{61s}); + + close = effCloseTime(tp{31s}, 30s, tp{0s}); + EXPECT_TRUE(close == tp{30s}); +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/LedgerTrie.cpp b/src/tests/libxrpl/consensus/LedgerTrie.cpp new file mode 100644 index 0000000000..1259a5049a --- /dev/null +++ b/src/tests/libxrpl/consensus/LedgerTrie.cpp @@ -0,0 +1,693 @@ +#include + +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +TEST(LedgerTrieTest, insert) +{ + using namespace csf; + // Single entry by itself + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 1); + + t.insert(h["abc"]); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 2); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + } + // Suffix of existing (extending tree) + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + EXPECT_TRUE(t.checkInvariants()); + // extend with no siblings + t.insert(h["abcd"]); + EXPECT_TRUE(t.checkInvariants()); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1); + + // extend with existing sibling + t.insert(h["abce"]); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 3); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abce"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abce"]) == 1); + } + // uncommitted of existing node + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + EXPECT_TRUE(t.checkInvariants()); + // uncommitted with no siblings + t.insert(h["abcdf"]); + EXPECT_TRUE(t.checkInvariants()); + + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abcdf"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcdf"]) == 1); + + // uncommitted with existing child + t.insert(h["abc"]); + EXPECT_TRUE(t.checkInvariants()); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 3); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abcdf"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcdf"]) == 1); + } + // Suffix + uncommitted of existing node + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + EXPECT_TRUE(t.checkInvariants()); + t.insert(h["abce"]); + EXPECT_TRUE(t.checkInvariants()); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abce"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abce"]) == 1); + } + // Suffix + uncommitted with existing child + { + // abcd : abcde, abcf + + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + EXPECT_TRUE(t.checkInvariants()); + t.insert(h["abcde"]); + EXPECT_TRUE(t.checkInvariants()); + t.insert(h["abcf"]); + EXPECT_TRUE(t.checkInvariants()); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 3); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abcf"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcf"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abcde"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcde"]) == 1); + } + + // Multiple counts + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"], 4); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 4); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 4); + EXPECT_TRUE(t.tipSupport(h["a"]) == 0); + EXPECT_TRUE(t.branchSupport(h["a"]) == 4); + + t.insert(h["abc"], 2); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 2); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 4); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 6); + EXPECT_TRUE(t.tipSupport(h["a"]) == 0); + EXPECT_TRUE(t.branchSupport(h["a"]) == 6); + } +} + +TEST(LedgerTrieTest, remove) +{ + using namespace csf; + // Not in trie + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + + EXPECT_TRUE(!t.remove(h["ab"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(!t.remove(h["a"])); + EXPECT_TRUE(t.checkInvariants()); + } + // In trie but with 0 tip support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"]); + t.insert(h["abce"]); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + EXPECT_TRUE(!t.remove(h["abc"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + } + // In trie with > 1 tip support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"], 2); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 2); + EXPECT_TRUE(t.remove(h["abc"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + + t.insert(h["abc"], 1); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 2); + EXPECT_TRUE(t.remove(h["abc"], 2)); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + + t.insert(h["abc"], 3); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 3); + EXPECT_TRUE(t.remove(h["abc"], 300)); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + } + // In trie with = 1 tip support, no children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + + EXPECT_TRUE(t.tipSupport(h["ab"]) == 1); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 1); + + EXPECT_TRUE(t.remove(h["abc"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 1); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 0); + } + // In trie with = 1 tip support, 1 child + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + t.insert(h["abcd"]); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1); + + EXPECT_TRUE(t.remove(h["abc"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 1); + } + // In trie with = 1 tip support, > 1 children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + t.insert(h["abcd"]); + t.insert(h["abce"]); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 3); + + EXPECT_TRUE(t.remove(h["abc"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 2); + } + + // In trie with = 1 tip support, parent compaction + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["abc"]); + t.insert(h["abd"]); + EXPECT_TRUE(t.checkInvariants()); + t.remove(h["ab"]); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abd"]) == 1); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 0); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 2); + + t.remove(h["abd"]); + EXPECT_TRUE(t.checkInvariants()); + + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 1); + } +} + +TEST(LedgerTrieTest, empty) +{ + using namespace csf; + LedgerTrie t; + LedgerHistoryHelper h; + EXPECT_TRUE(t.empty()); + + Ledger const genesis = h[""]; + t.insert(genesis); + EXPECT_TRUE(!t.empty()); + t.remove(genesis); + EXPECT_TRUE(t.empty()); + + t.insert(h["abc"]); + EXPECT_TRUE(!t.empty()); + t.remove(h["abc"]); + EXPECT_TRUE(t.empty()); +} + +TEST(LedgerTrieTest, support) +{ + using namespace csf; + + LedgerTrie t; + LedgerHistoryHelper h; + EXPECT_TRUE(t.tipSupport(h["a"]) == 0); + EXPECT_TRUE(t.tipSupport(h["axy"]) == 0); + + EXPECT_TRUE(t.branchSupport(h["a"]) == 0); + EXPECT_TRUE(t.branchSupport(h["axy"]) == 0); + + t.insert(h["abc"]); + EXPECT_TRUE(t.tipSupport(h["a"]) == 0); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 0); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abcd"]) == 0); + + EXPECT_TRUE(t.branchSupport(h["a"]) == 1); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abcd"]) == 0); + + t.insert(h["abe"]); + EXPECT_TRUE(t.tipSupport(h["a"]) == 0); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 0); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 1); + EXPECT_TRUE(t.tipSupport(h["abe"]) == 1); + + EXPECT_TRUE(t.branchSupport(h["a"]) == 2); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 2); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abe"]) == 1); + + t.remove(h["abc"]); + EXPECT_TRUE(t.tipSupport(h["a"]) == 0); + EXPECT_TRUE(t.tipSupport(h["ab"]) == 0); + EXPECT_TRUE(t.tipSupport(h["abc"]) == 0); + EXPECT_TRUE(t.tipSupport(h["abe"]) == 1); + + EXPECT_TRUE(t.branchSupport(h["a"]) == 1); + EXPECT_TRUE(t.branchSupport(h["ab"]) == 1); + EXPECT_TRUE(t.branchSupport(h["abc"]) == 0); + EXPECT_TRUE(t.branchSupport(h["abe"]) == 1); +} + +TEST(LedgerTrieTest, get_preferred) +{ + using namespace csf; + using Seq = Ledger::Seq; + // Empty + { + LedgerTrie const t; + EXPECT_TRUE(t.getPreferred(Seq{0}) == std::nullopt); + EXPECT_TRUE(t.getPreferred(Seq{2}) == std::nullopt); + } + // Genesis support is NOT empty + { + LedgerTrie t; + LedgerHistoryHelper h; + Ledger const genesis = h[""]; + t.insert(genesis); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{0})->id == genesis.id()); + EXPECT_TRUE(t.remove(genesis)); + EXPECT_TRUE(t.getPreferred(Seq{0}) == std::nullopt); + EXPECT_TRUE(!t.remove(genesis)); + } + // Single node no children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + } + // Single node smaller child support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"]); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id()); + } + // Single node larger child + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"], 2); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcd"].id()); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcd"].id()); + } + // Single node smaller children support + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"]); + t.insert(h["abce"]); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id()); + + t.insert(h["abc"]); + + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + // Single node larger children + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"], 2); + t.insert(h["abce"]); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id()); + + t.insert(h["abcd"]); + + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcd"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcd"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + // Tie-breaker by id + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abcd"], 2); + t.insert(h["abce"], 2); + + EXPECT_TRUE(h["abce"].id() > h["abcd"].id()); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abce"].id()); + + t.insert(h["abcd"]); + EXPECT_TRUE(h["abce"].id() > h["abcd"].id()); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcd"].id()); + } + + // Tie-breaker not needed + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"]); + t.insert(h["abce"], 2); + // abce only has a margin of 1, but it owns the tie-breaker + EXPECT_TRUE(h["abce"].id() > h["abcd"].id()); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abce"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abce"].id()); + + // Switch support from abce to abcd, tie-breaker now needed + t.remove(h["abce"]); + t.insert(h["abcd"]); + + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + + // Single node larger grand child + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcd"], 2); + t.insert(h["abcde"], 4); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcde"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcde"].id()); + EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["abcde"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + + // Too much uncommitted support from competing branches + { + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["abc"]); + t.insert(h["abcde"], 2); + t.insert(h["abcfg"], 2); + // 'de' and 'fg' are tied without 'abc' vote + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abc"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abc"].id()); + EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["abc"].id()); + + t.remove(h["abc"]); + t.insert(h["abcd"]); + + // 'de' branch has 3 votes to 2, so earlier sequences see it as preferred + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abcde"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["abcde"].id()); + + // However, if you validated a ledger with Seq 5, potentially on + // a different branch, you do not yet know if they chose abcd + // or abcf because of you, so abc remains preferred + EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["abc"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + + // Changing largestSeq perspective changes preferred branch + { + /** + * Build the tree below with initial tip support annotated + * A + * / \ + * B(1) C(1) + * / | | + * H D F(1) + * | + * E(2) + * | + * G + */ + LedgerTrie t; + LedgerHistoryHelper h; + t.insert(h["ab"]); + t.insert(h["ac"]); + t.insert(h["acf"]); + t.insert(h["abde"], 2); + + // B has more branch support + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["ab"].id()); + + // But if you last validated D,F or E, you do not yet know + // if someone used that validation to commit to B or C + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["a"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["a"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + + /** + * One of E advancing to G doesn't change anything + * A + * / \ + * B(1) C(1) + * / | | + * H D F(1) + * | + * E(1) + * | + * G(1) + */ + t.remove(h["abde"]); + t.insert(h["abdeg"]); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["a"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["a"].id()); + EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["a"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + + /** + * C advancing to H does advance the seq 3 preferred ledger + * A + * / \ + * B(1) C + * / | | + * H(1)D F(1) + * | + * E(1) + * | + * G(1) + */ + t.remove(h["ac"]); + t.insert(h["abh"]); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["a"].id()); + EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["a"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + + /** + * F advancing to E also moves the preferred ledger forward + * A + * / \ + * B(1) C + * / | | + * H(1)D F + * | + * E(2) + * | + * G(1) + */ + t.remove(h["acf"]); + t.insert(h["abde"]); + + // NOLINTBEGIN(bugprone-unchecked-optional-access) + EXPECT_TRUE(t.getPreferred(Seq{1})->id == h["abde"].id()); + EXPECT_TRUE(t.getPreferred(Seq{2})->id == h["abde"].id()); + EXPECT_TRUE(t.getPreferred(Seq{3})->id == h["abde"].id()); + EXPECT_TRUE(t.getPreferred(Seq{4})->id == h["ab"].id()); + EXPECT_TRUE(t.getPreferred(Seq{5})->id == h["ab"].id()); + // NOLINTEND(bugprone-unchecked-optional-access) + } +} + +TEST(LedgerTrieTest, root_related) +{ + using namespace csf; + // Since the root is a special node that breaks the no-single child + // invariant, do some tests that exercise it. + + LedgerTrie t; + LedgerHistoryHelper h; + EXPECT_TRUE(!t.remove(h[""])); + EXPECT_TRUE(t.branchSupport(h[""]) == 0); + EXPECT_TRUE(t.tipSupport(h[""]) == 0); + + t.insert(h["a"]); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.branchSupport(h[""]) == 1); + EXPECT_TRUE(t.tipSupport(h[""]) == 0); + + t.insert(h["e"]); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.branchSupport(h[""]) == 2); + EXPECT_TRUE(t.tipSupport(h[""]) == 0); + + EXPECT_TRUE(t.remove(h["e"])); + EXPECT_TRUE(t.checkInvariants()); + EXPECT_TRUE(t.branchSupport(h[""]) == 1); + EXPECT_TRUE(t.tipSupport(h[""]) == 0); +} + +TEST(LedgerTrieTest, stress) +{ + using namespace csf; + LedgerTrie t; + LedgerHistoryHelper h; + + // Test quasi-randomly add/remove supporting for different ledgers + // from a branching history. + + // Ledgers have sequence 1,2,3,4 + std::uint32_t const depthConst = 4; + // Each ledger has 4 possible children + std::uint32_t const width = 4; + + std::uint32_t const iterations = 10000; + + // Use explicit seed to have same results for CI + // NOLINTNEXTLINE(bugprone-random-generator-seed): fixed seed for reproducible test + std::mt19937 gen{42}; + std::uniform_int_distribution<> depthDist(0, depthConst - 1); + std::uniform_int_distribution<> widthDist(0, width - 1); + std::uniform_int_distribution<> flip(0, 1); + for (std::uint32_t i = 0; i < iterations; ++i) + { + // pick a random ledger history + std::string curr; + char const depth = depthDist(gen); + char offset = 0; + for (char d = 0; d < depth; ++d) + { + char const a = offset + widthDist(gen); + curr += a; + offset = (a + 1) * width; + } + + // 50-50 to add remove + if (flip(gen) == 0) + { + t.insert(h[curr]); + } + else + { + t.remove(h[curr]); + } + EXPECT_TRUE(t.checkInvariants()); + if (!(t.checkInvariants())) + return; + } +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/ScaleFreeSim.cpp b/src/tests/libxrpl/consensus/ScaleFreeSim.cpp new file mode 100644 index 0000000000..2b07b29900 --- /dev/null +++ b/src/tests/libxrpl/consensus/ScaleFreeSim.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { + +TEST(ScaleFreeSimTest, DISABLED_scale_free_sim) +{ + using namespace std::chrono; + using namespace csf; + + std::ostream& log = std::cout; + + // Generate a quasi-random scale free network and simulate consensus + // as we vary transaction submission rates + + int const n = 100; // Peers + + int const numUNLs = 15; // UNL lists + int const minUNLSize = n / 4, maxUNLSize = n / 2; + + ConsensusParms const parms{}; + Sim sim; + PeerGroup network = sim.createGroup(n); + + // generate trust ranks + std::vector const ranks = sample(network.size(), PowerLawDistribution{1, 3}, sim.rng); + + // generate scale-free trust graph + randomRankedTrust( + network, ranks, numUNLs, std::uniform_int_distribution<>{minUNLSize, maxUNLSize}, sim.rng); + + // nodes with a trust line in either direction are network-connected + network.connectFromTrust(round(0.2 * parms.ledgerGRANULARITY)); + + // Initialize collectors to track statistics to report + TxCollector txCollector; + LedgerCollector ledgerCollector; + auto colls = makeCollectors(txCollector, ledgerCollector); + sim.collectors.add(colls); + + // Initial round to set prior state + sim.run(1); + + // Initialize timers + HeartbeatTimer heart(sim.scheduler, seconds(10s)); + + // Run for 10 minutes, submitting 100 tx/second + std::chrono::nanoseconds const simDuration = 10min; + std::chrono::nanoseconds const quiet = 10s; + Rate const rate{.count = 100, .duration = 1000ms}; + + // txs, start/stop/step, target + auto peerSelector = makeSelector(network.begin(), network.end(), ranks, sim.rng); + auto txSubmitter = makeSubmitter( + ConstantDistribution{rate.inv()}, + sim.scheduler.now() + quiet, + sim.scheduler.now() + (simDuration - quiet), + peerSelector, + sim.scheduler, + sim.rng); + + // run simulation for given duration + heart.start(); + sim.run(simDuration); + + EXPECT_TRUE(sim.branches() == 1); + EXPECT_TRUE(sim.synchronized()); + + // TODO: Clean up this formatting mess!! + + log << "Peers: " << network.size() << std::endl; + log << "Simulated Duration: " << duration_cast(simDuration).count() << " ms" + << std::endl; + log << "Branches: " << sim.branches() << std::endl; + log << "Synchronized: " << (sim.synchronized() ? "Y" : "N") << std::endl; + log << std::endl; + + txCollector.report(simDuration, log); + ledgerCollector.report(simDuration, log); + // Print summary? + // # forks? # of LCLs? + // # peers + // # tx submitted + // # ledgers/sec etc.? +} + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/consensus/Validations.cpp b/src/tests/libxrpl/consensus/Validations.cpp new file mode 100644 index 0000000000..56964f56a6 --- /dev/null +++ b/src/tests/libxrpl/consensus/Validations.cpp @@ -0,0 +1,1030 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test::csf { + +namespace { + +beast::Journal +journal() +{ + return beast::Journal{TestSink::instance()}; +} + +template +void +expireValidations(ValidationStore& validations) +{ + auto j = journal(); + validations.expire(j); +} + +using clock_type = beast::AbstractClock const; + +// Helper to convert steady_clock to a reasonable NetClock +// This allows a single manual clock in the unit tests +NetClock::time_point +toNetClock(clock_type const& c) +{ + // We don't care about the actual epochs, but do want the + // generated NetClock time to be well past its epoch to ensure + // any subtractions are positive + using namespace std::chrono; + return NetClock::time_point( + duration_cast(c.now().time_since_epoch() + 86400s)); +} + +// Represents a node that can issue validations +class Node +{ + clock_type const& c_; + PeerID nodeID_; + bool trusted_ = true; + std::size_t signIdx_{1}; + std::optional loadFee_; + +public: + Node(PeerID nodeID, clock_type const& c) : c_(c), nodeID_(nodeID) + { + } + + void + untrust() + { + trusted_ = false; + } + + void + trust() + { + trusted_ = true; + } + + void + setLoadFee(std::uint32_t fee) + { + loadFee_ = fee; + } + + [[nodiscard]] PeerID + nodeID() const + { + return nodeID_; + } + + void + advanceKey() + { + signIdx_++; + } + + [[nodiscard]] PeerKey + currKey() const + { + return std::make_pair(nodeID_, signIdx_); + } + + [[nodiscard]] PeerKey + masterKey() const + { + return std::make_pair(nodeID_, 0); + } + [[nodiscard]] NetClock::time_point + now() const + { + return toNetClock(c_); + } + + // Issue a new validation with given sequence number and id and + // with signing and seen times offset from the common clock + [[nodiscard]] Validation + validate( + Ledger::ID id, + Ledger::Seq seq, + NetClock::duration signOffset, + NetClock::duration seenOffset, + bool full) const + { + Validation v{ + id, seq, now() + signOffset, now() + seenOffset, currKey(), nodeID_, full, loadFee_}; + if (trusted_) + v.setTrusted(); + return v; + } + + [[nodiscard]] Validation + validate(Ledger ledger, NetClock::duration signOffset, NetClock::duration seenOffset) const + { + return validate(ledger.id(), ledger.seq(), signOffset, seenOffset, true); + } + + [[nodiscard]] Validation + validate(Ledger ledger) const + { + return validate( + ledger.id(), ledger.seq(), NetClock::duration{0}, NetClock::duration{0}, true); + } + + [[nodiscard]] Validation + partial(Ledger ledger) const + { + return validate( + ledger.id(), ledger.seq(), NetClock::duration{0}, NetClock::duration{0}, false); + } +}; + +// Generic Validations adaptor +class Adaptor +{ + clock_type& c_; + LedgerOracle& oracle_; + +public: + // Non-locking mutex to avoid locks in generic Validations + struct Mutex + { + void + lock() + { + } + + void + unlock() + { + } + }; + + using Validation = csf::Validation; + using Ledger = csf::Ledger; + + Adaptor(clock_type& c, LedgerOracle& o) : c_{c}, oracle_{o} + { + } + + [[nodiscard]] NetClock::time_point + now() const + { + return toNetClock(c_); + } + + std::optional + acquire(Ledger::ID const& id) + { + return oracle_.lookup(id); + } +}; + +// Specialize generic Validations using the above types +using TestValidations = Validations; + +// Gather the dependencies of TestValidations in a single class and provide +// accessors for simplifying test logic +class TestHarness +{ + ValidationParms p_; + beast::ManualClock clock_; + TestValidations tv_; + PeerID nextNodeId_{0}; + +public: + explicit TestHarness(LedgerOracle& o) : tv_(p_, clock_, clock_, o) + { + } + + ValStatus + add(Validation const& v) + { + return tv_.add(v.nodeID(), v); + } + + TestValidations& + vals() + { + return tv_; + } + + Node + makeNode() + { + return Node(nextNodeId_++, clock_); + } + + ValidationParms + parms() const + { + return p_; + } + + auto& + clock() + { + return clock_; + } +}; + +Ledger const kGenesisLedger{Ledger::MakeGenesis{}}; + +} // namespace + +TEST(ValidationsTest, add_validation) +{ + using namespace std::chrono_literals; + + SCOPED_TRACE("Add validation"); + LedgerHistoryHelper h; + Ledger const ledgerA = h["a"]; + Ledger ledgerAB = h["ab"]; + Ledger ledgerAZ = h["az"]; + Ledger ledgerABC = h["abc"]; + Ledger const ledgerABCD = h["abcd"]; + Ledger const ledgerABCDE = h["abcde"]; + + { + TestHarness harness(h.oracle); + Node n = harness.makeNode(); + + auto const v = n.validate(ledgerA); + + // Add a current validation + EXPECT_TRUE(ValStatus::Current == harness.add(v)); + + // Re-adding violates the increasing seq requirement for full + // validations + EXPECT_TRUE(ValStatus::BadSeq == harness.add(v)); + + harness.clock().advance(1s); + + EXPECT_TRUE(ValStatus::Current == harness.add(n.validate(ledgerAB))); + + // Test the node changing signing key + + // Confirm old ledger on hand, but not new ledger + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerAB.id()) == 1); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerABC.id()) == 0); + + // Rotate signing keys + n.advanceKey(); + + harness.clock().advance(1s); + + // Cannot re-do the same full validation sequence + EXPECT_TRUE(ValStatus::Conflicting == harness.add(n.validate(ledgerAB))); + // Cannot send the same partial validation sequence + EXPECT_TRUE(ValStatus::Conflicting == harness.add(n.partial(ledgerAB))); + + // Now trusts the newest ledger too + harness.clock().advance(1s); + EXPECT_TRUE(ValStatus::Current == harness.add(n.validate(ledgerABC))); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerAB.id()) == 1); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerABC.id()) == 1); + + // Processing validations out of order should ignore the older + // validation + harness.clock().advance(2s); + auto const valABCDE = n.validate(ledgerABCDE); + + harness.clock().advance(4s); + auto const valABCD = n.validate(ledgerABCD); + + EXPECT_TRUE(ValStatus::Current == harness.add(valABCD)); + + EXPECT_TRUE(ValStatus::Stale == harness.add(valABCDE)); + } + + { + // Process validations out of order with shifted times + + TestHarness harness(h.oracle); + Node const n = harness.makeNode(); + + // Establish a new current validation + EXPECT_TRUE(ValStatus::Current == harness.add(n.validate(ledgerA))); + + // Process a validation that has "later" seq but early sign time + EXPECT_TRUE(ValStatus::Stale == harness.add(n.validate(ledgerAB, -1s, -1s))); + + // Process a validation that has a later seq and later sign + // time + EXPECT_TRUE(ValStatus::Current == harness.add(n.validate(ledgerABC, 1s, 1s))); + } + + { + // Test stale on arrival validations + TestHarness harness(h.oracle); + Node const n = harness.makeNode(); + + EXPECT_TRUE( + ValStatus::Stale == + harness.add(n.validate(ledgerA, -harness.parms().validationCurrentEarly, 0s))); + + EXPECT_TRUE( + ValStatus::Stale == + harness.add(n.validate(ledgerA, harness.parms().validationCurrentWall, 0s))); + + EXPECT_TRUE( + ValStatus::Stale == + harness.add(n.validate(ledgerA, 0s, harness.parms().validationCurrentLocal))); + } + + { + // Test that full or partials cannot be sent for older sequence + // numbers, unless time-out has happened + for (bool doFull : {true, false}) + { + TestHarness harness(h.oracle); + Node n = harness.makeNode(); + + auto process = [&](Ledger& lgr) { + if (doFull) + return harness.add(n.validate(lgr)); + return harness.add(n.partial(lgr)); + }; + + EXPECT_TRUE(ValStatus::Current == process(ledgerABC)); + harness.clock().advance(1s); + EXPECT_TRUE(ledgerAB.seq() < ledgerABC.seq()); + EXPECT_TRUE(ValStatus::BadSeq == process(ledgerAB)); + + // If we advance far enough for AB to expire, we can fully + // validate or partially validate that sequence number again + EXPECT_TRUE(ValStatus::Conflicting == process(ledgerAZ)); + harness.clock().advance(harness.parms().validationSetExpires + 1ms); + EXPECT_TRUE(ValStatus::Current == process(ledgerAZ)); + } + } +} + +TEST(ValidationsTest, on_stale) +{ + SCOPED_TRACE("Stale validation"); + // Verify validation becomes stale based solely on time passing, but + // use different functions to trigger the check for staleness + + LedgerHistoryHelper h; + Ledger ledgerA = h["a"]; + Ledger const ledgerAB = h["ab"]; + + using Trigger = std::function; + + std::vector const triggers = { + [&](TestValidations& vals) { vals.currentTrusted(); }, + [&](TestValidations& vals) { vals.getCurrentNodeIDs(); }, + [&](TestValidations& vals) { vals.getPreferred(kGenesisLedger); }, + [&](TestValidations& vals) { vals.getNodesAfter(ledgerA, ledgerA.id()); }}; + for (Trigger const& trigger : triggers) + { + TestHarness harness(h.oracle); + Node const n = harness.makeNode(); + + EXPECT_TRUE(ValStatus::Current == harness.add(n.validate(ledgerAB))); + trigger(harness.vals()); + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 1); + EXPECT_TRUE( + harness.vals().getPreferred(kGenesisLedger) == + std::make_pair(ledgerAB.seq(), ledgerAB.id())); + harness.clock().advance(harness.parms().validationCurrentLocal); + + // trigger check for stale + trigger(harness.vals()); + + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 0); + EXPECT_TRUE(harness.vals().getPreferred(kGenesisLedger) == std::nullopt); + } +} + +TEST(ValidationsTest, get_nodes_after) +{ + // Test getting number of nodes working on a validation descending + // a prescribed one. This count should only be for trusted nodes, but + // includes partial and full validations + + using namespace std::chrono_literals; + SCOPED_TRACE("Get nodes after"); + + LedgerHistoryHelper h; + Ledger const ledgerA = h["a"]; + Ledger const ledgerAB = h["ab"]; + Ledger const ledgerABC = h["abc"]; + Ledger const ledgerAD = h["ad"]; + + TestHarness harness(h.oracle); + Node const trustedNode1 = harness.makeNode(); + Node const trustedNode2 = harness.makeNode(); + Node const trustedNode3 = harness.makeNode(); + + Node notTrustedNode = harness.makeNode(); + notTrustedNode.untrust(); + + // first round a,b,c agree, d has is partial + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode1.validate(ledgerA))); + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode2.validate(ledgerA))); + EXPECT_TRUE(ValStatus::Current == harness.add(notTrustedNode.validate(ledgerA))); + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode3.partial(ledgerA))); + + for (Ledger const& ledger : {ledgerA, ledgerAB, ledgerABC, ledgerAD}) + EXPECT_TRUE(harness.vals().getNodesAfter(ledger, ledger.id()) == 0); + + harness.clock().advance(5s); + + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode1.validate(ledgerAB))); + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode2.validate(ledgerABC))); + EXPECT_TRUE(ValStatus::Current == harness.add(notTrustedNode.validate(ledgerAB))); + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode3.partial(ledgerABC))); + + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerA, ledgerA.id()) == 3); + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerAB, ledgerAB.id()) == 2); + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerABC, ledgerABC.id()) == 0); + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerAD, ledgerAD.id()) == 0); + + // If given a ledger inconsistent with the id, is still able to check using slower method + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerAD, ledgerA.id()) == 1); + EXPECT_TRUE(harness.vals().getNodesAfter(ledgerAD, ledgerAB.id()) == 2); +} + +TEST(ValidationsTest, current_trusted) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("Current trusted validations"); + + LedgerHistoryHelper h; + Ledger const ledgerA = h["a"]; + Ledger const ledgerB = h["b"]; + Ledger const ledgerAC = h["ac"]; + + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + Node b = harness.makeNode(); + b.untrust(); + + EXPECT_TRUE(ValStatus::Current == harness.add(a.validate(ledgerA))); + EXPECT_TRUE(ValStatus::Current == harness.add(b.validate(ledgerB))); + + // Only a is trusted + EXPECT_TRUE(harness.vals().currentTrusted().size() == 1); + EXPECT_TRUE(harness.vals().currentTrusted()[0].ledgerID() == ledgerA.id()); + EXPECT_TRUE(harness.vals().currentTrusted()[0].seq() == ledgerA.seq()); + + harness.clock().advance(3s); + + for (auto const& node : {a, b}) + EXPECT_TRUE(ValStatus::Current == harness.add(node.validate(ledgerAC))); + + // New validation for a + EXPECT_TRUE(harness.vals().currentTrusted().size() == 1); + EXPECT_TRUE(harness.vals().currentTrusted()[0].ledgerID() == ledgerAC.id()); + EXPECT_TRUE(harness.vals().currentTrusted()[0].seq() == ledgerAC.seq()); + + // Pass enough time for it to go stale + harness.clock().advance(harness.parms().validationCurrentLocal); + EXPECT_TRUE(harness.vals().currentTrusted().empty()); +} + +TEST(ValidationsTest, get_current_public_keys) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("Current public keys"); + + LedgerHistoryHelper h; + Ledger const ledgerA = h["a"]; + Ledger const ledgerAC = h["ac"]; + + TestHarness harness(h.oracle); + Node a = harness.makeNode(), b = harness.makeNode(); + b.untrust(); + + for (auto const& node : {a, b}) + EXPECT_TRUE(ValStatus::Current == harness.add(node.validate(ledgerA))); + + { + hash_set const expectedKeys = {a.nodeID(), b.nodeID()}; + EXPECT_TRUE(harness.vals().getCurrentNodeIDs() == expectedKeys); + } + + harness.clock().advance(3s); + + // Change keys and issue partials + a.advanceKey(); + b.advanceKey(); + + for (auto const& node : {a, b}) + EXPECT_TRUE(ValStatus::Current == harness.add(node.partial(ledgerAC))); + + { + hash_set const expectedKeys = {a.nodeID(), b.nodeID()}; + EXPECT_TRUE(harness.vals().getCurrentNodeIDs() == expectedKeys); + } + + // Pass enough time for them to go stale + harness.clock().advance(harness.parms().validationCurrentLocal); + EXPECT_TRUE(harness.vals().getCurrentNodeIDs().empty()); +} + +TEST(ValidationsTest, trusted_by_ledger_functions) +{ + // Test the Validations functions that calculate a value by ledger ID + using namespace std::chrono_literals; + SCOPED_TRACE("By ledger functions"); + + // Several Validations functions return a set of values associated + // with trusted ledgers sharing the same ledger ID. The tests below + // exercise this logic by saving the set of trusted Validations, and + // verifying that the Validations member functions all calculate the + // proper transformation of the available ledgers. + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + + Node a = harness.makeNode(), b = harness.makeNode(), c = harness.makeNode(), + d = harness.makeNode(), e = harness.makeNode(); + + c.untrust(); + // Mix of load fees + a.setLoadFee(12); + b.setLoadFee(1); + c.setLoadFee(12); + e.setLoadFee(12); + + hash_map, std::vector> trustedValidations; + + //---------------------------------------------------------------------- + // checkers + auto sorted = [](auto vec) { + std::sort(vec.begin(), vec.end()); + return vec; + }; + auto compare = [&]() { + for (auto& it : trustedValidations) + { + auto const& id = it.first.first; + auto const& seq = it.first.second; + auto const& expectedValidations = it.second; + + EXPECT_TRUE(harness.vals().numTrustedForLedger(id) == expectedValidations.size()); + EXPECT_TRUE( + sorted(harness.vals().getTrustedForLedger(id, seq)) == sorted(expectedValidations)); + + std::uint32_t const baseFee = 0; + std::vector expectedFees; + expectedFees.reserve(expectedValidations.size()); + for (auto const& val : expectedValidations) + { + expectedFees.push_back(val.loadFee().value_or(baseFee)); + } + + EXPECT_TRUE(sorted(harness.vals().fees(id, baseFee)) == sorted(expectedFees)); + } + }; + + //---------------------------------------------------------------------- + Ledger const ledgerA = h["a"]; + Ledger const ledgerB = h["b"]; + Ledger const ledgerAC = h["ac"]; + + // Add a dummy ID to cover unknown ledger identifiers + trustedValidations[{Ledger::ID{100}, Ledger::Seq{100}}] = {}; + + // first round a,b,c agree + for (auto const& node : {a, b, c}) + { + auto const val = node.validate(ledgerA); + EXPECT_TRUE(ValStatus::Current == harness.add(val)); + if (val.trusted()) + trustedValidations[{val.ledgerID(), val.seq()}].emplace_back(val); + } + // d disagrees + { + auto const val = d.validate(ledgerB); + EXPECT_TRUE(ValStatus::Current == harness.add(val)); + trustedValidations[{val.ledgerID(), val.seq()}].emplace_back(val); + } + // e only issues partials + { + EXPECT_TRUE(ValStatus::Current == harness.add(e.partial(ledgerA))); + } + + harness.clock().advance(5s); + // second round, a,b,c move to ledger 2 + for (auto const& node : {a, b, c}) + { + auto const val = node.validate(ledgerAC); + EXPECT_TRUE(ValStatus::Current == harness.add(val)); + if (val.trusted()) + trustedValidations[{val.ledgerID(), val.seq()}].emplace_back(val); + } + // d now thinks ledger 1, but cannot re-issue a previously used seq + // and attempting it should generate a conflict. + { + EXPECT_TRUE(ValStatus::Conflicting == harness.add(d.partial(ledgerA))); + } + // e only issues partials + { + EXPECT_TRUE(ValStatus::Current == harness.add(e.partial(ledgerAC))); + } + + compare(); +} + +TEST(ValidationsTest, expire) +{ + // Verify expiring clears out validations stored by ledger + SCOPED_TRACE("Expire validations"); + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + constexpr Ledger::Seq kOne(1); + constexpr Ledger::Seq kTwo(2); + + // simple cases + Ledger const ledgerA = h["a"]; + EXPECT_TRUE(ValStatus::Current == harness.add(a.validate(ledgerA))); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); + expireValidations(harness.vals()); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); + harness.clock().advance(harness.parms().validationSetExpires); + expireValidations(harness.vals()); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerA.id()) == 0); + + // use setSeqToKeep to keep the validation from expire + Ledger const ledgerB = h["ab"]; + EXPECT_TRUE(ValStatus::Current == harness.add(a.validate(ledgerB))); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerB.id()) == 1); + harness.vals().setSeqToKeep(ledgerB.seq(), ledgerB.seq() + kOne); + harness.clock().advance(harness.parms().validationSetExpires); + expireValidations(harness.vals()); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerB.id()) == 1); + // change toKeep + harness.vals().setSeqToKeep(ledgerB.seq() + kOne, ledgerB.seq() + kTwo); + // advance clock slowly + int const loops = + harness.parms().validationSetExpires / harness.parms().validationFRESHNESS + 1; + for (int i = 0; i < loops; ++i) + { + harness.clock().advance(harness.parms().validationFRESHNESS); + expireValidations(harness.vals()); + } + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerB.id()) == 0); + + // Allow the validation with high seq to expire + Ledger const ledgerC = h["abc"]; + EXPECT_TRUE(ValStatus::Current == harness.add(a.validate(ledgerC))); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerC.id()) == 1); + harness.vals().setSeqToKeep(ledgerC.seq() - kOne, ledgerC.seq()); + harness.clock().advance(harness.parms().validationSetExpires); + expireValidations(harness.vals()); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerC.id()) == 0); +} + +TEST(ValidationsTest, flush) +{ + // Test final flush of validations + using namespace std::chrono_literals; + SCOPED_TRACE("Flush validations"); + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const trustedNode1 = harness.makeNode(); + Node const trustedNode2 = harness.makeNode(); + Node notTrustedNode = harness.makeNode(); + notTrustedNode.untrust(); + + Ledger const ledgerA = h["a"]; + Ledger const ledgerAB = h["ab"]; + + hash_map expected; + for (auto const& node : {trustedNode1, trustedNode2, notTrustedNode}) + { + auto const val = node.validate(ledgerA); + EXPECT_TRUE(ValStatus::Current == harness.add(val)); + expected.emplace(node.nodeID(), val); + } + + // Send in a new validation for a, saving the new one into the expected + // map after setting the proper prior ledger ID it replaced + harness.clock().advance(1s); + auto newVal = trustedNode1.validate(ledgerAB); + EXPECT_TRUE(ValStatus::Current == harness.add(newVal)); + expected.find(trustedNode1.nodeID())->second = newVal; +} + +TEST(ValidationsTest, get_preferred_ledger) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("Preferred Ledger"); + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const trustedNode1 = harness.makeNode(); + Node const trustedNode2 = harness.makeNode(); + Node const trustedNode3 = harness.makeNode(); + + Node notTrustedNode = harness.makeNode(); + notTrustedNode.untrust(); + + Ledger const ledgerA = h["a"]; + Ledger const ledgerB = h["b"]; + Ledger const ledgerAC = h["ac"]; + Ledger const ledgerACD = h["acd"]; + + using Seq = Ledger::Seq; + + auto pref = [](Ledger ledger) { return std::make_pair(ledger.seq(), ledger.id()); }; + + // Empty (no ledgers) + EXPECT_TRUE(harness.vals().getPreferred(ledgerA) == std::nullopt); + + // Single ledger + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode1.validate(ledgerB))); + EXPECT_TRUE(harness.vals().getPreferred(ledgerA) == pref(ledgerB)); + EXPECT_TRUE(harness.vals().getPreferred(ledgerB) == pref(ledgerB)); + + // Minimum valid sequence + EXPECT_TRUE(harness.vals().getPreferred(ledgerA, Seq{10}) == ledgerA.id()); + + // Untrusted doesn't impact preferred ledger + // (ledgerB has tie-break over ledgerA) + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode2.validate(ledgerA))); + EXPECT_TRUE(ValStatus::Current == harness.add(notTrustedNode.validate(ledgerA))); + EXPECT_TRUE(ledgerB.id() > ledgerA.id()); + EXPECT_TRUE(harness.vals().getPreferred(ledgerA) == pref(ledgerB)); + EXPECT_TRUE(harness.vals().getPreferred(ledgerB) == pref(ledgerB)); + + // Partial does break ties + EXPECT_TRUE(ValStatus::Current == harness.add(trustedNode3.partial(ledgerA))); + EXPECT_TRUE(harness.vals().getPreferred(ledgerA) == pref(ledgerA)); + EXPECT_TRUE(harness.vals().getPreferred(ledgerB) == pref(ledgerA)); + + harness.clock().advance(5s); + + // Parent of preferred-> stick with ledger + for (auto const& node : {trustedNode1, trustedNode2, notTrustedNode, trustedNode3}) + EXPECT_TRUE(ValStatus::Current == harness.add(node.validate(ledgerAC))); + // Parent of preferred stays put + EXPECT_TRUE(harness.vals().getPreferred(ledgerA) == pref(ledgerA)); + // Earlier different chain, switch + EXPECT_TRUE(harness.vals().getPreferred(ledgerB) == pref(ledgerAC)); + // Later on chain, stays where it is + EXPECT_TRUE(harness.vals().getPreferred(ledgerACD) == pref(ledgerACD)); + + // Any later grandchild or different chain is preferred + harness.clock().advance(5s); + for (auto const& node : {trustedNode1, trustedNode2, notTrustedNode, trustedNode3}) + EXPECT_TRUE(ValStatus::Current == harness.add(node.validate(ledgerACD))); + for (auto const& ledger : {ledgerA, ledgerB, ledgerACD}) + EXPECT_TRUE(harness.vals().getPreferred(ledger) == pref(ledgerACD)); +} + +TEST(ValidationsTest, get_preferred_lcl) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("Get preferred LCL"); + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + + Ledger const ledgerA = h["a"]; + Ledger const ledgerB = h["b"]; + Ledger const ledgerC = h["c"]; + + using ID = Ledger::ID; + using Seq = Ledger::Seq; + + hash_map peerCounts; + + // No trusted validations or counts sticks with current ledger + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerA.id()); + + ++peerCounts[ledgerB.id()]; + + // No trusted validations, rely on peer counts + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerB.id()); + + ++peerCounts[ledgerC.id()]; + // No trusted validations, tied peers goes with larger ID + EXPECT_TRUE(ledgerC.id() > ledgerB.id()); + + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerC.id()); + + peerCounts[ledgerC.id()] += 1000; + + // Single trusted always wins over peer counts + EXPECT_TRUE(ValStatus::Current == harness.add(a.validate(ledgerA))); + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerA, Seq{0}, peerCounts) == ledgerA.id()); + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerB, Seq{0}, peerCounts) == ledgerA.id()); + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerC, Seq{0}, peerCounts) == ledgerA.id()); + + // Stick with current ledger if trusted validation ledger has too old + // of a sequence + EXPECT_TRUE(harness.vals().getPreferredLCL(ledgerB, Seq{2}, peerCounts) == ledgerB.id()); +} + +TEST(ValidationsTest, acquire_validated_ledger) +{ + using namespace std::chrono_literals; + SCOPED_TRACE("Acquire validated ledger"); + + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + Node const b = harness.makeNode(); + + using ID = Ledger::ID; + using Seq = Ledger::Seq; + + // Validate the ledger before it is actually available + Validation const val = a.validate(ID{2}, Seq{2}, 0s, 0s, true); + + EXPECT_TRUE(ValStatus::Current == harness.add(val)); + // Validation is available + EXPECT_TRUE(harness.vals().numTrustedForLedger(ID{2}) == 1); + // but ledger based data is not + EXPECT_TRUE(harness.vals().getNodesAfter(kGenesisLedger, ID{0}) == 0); + // Initial preferred branch falls back to the ledger we are trying to + // acquire + EXPECT_TRUE(harness.vals().getPreferred(kGenesisLedger) == std::make_pair(Seq{2}, ID{2})); + + // After adding another unavailable validation, the preferred ledger + // breaks ties via higher ID + EXPECT_TRUE(ValStatus::Current == harness.add(b.validate(ID{3}, Seq{2}, 0s, 0s, true))); + EXPECT_TRUE(harness.vals().getPreferred(kGenesisLedger) == std::make_pair(Seq{2}, ID{3})); + + // Create the ledger + Ledger const ledgerAB = h["ab"]; + // Now it should be available + EXPECT_TRUE(harness.vals().getNodesAfter(kGenesisLedger, ID{0}) == 1); + + // Create a validation that is not available + harness.clock().advance(5s); + Validation const val2 = a.validate(ID{4}, Seq{4}, 0s, 0s, true); + EXPECT_TRUE(ValStatus::Current == harness.add(val2)); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ID{4}) == 1); + EXPECT_TRUE( + harness.vals().getPreferred(kGenesisLedger) == + std::make_pair(ledgerAB.seq(), ledgerAB.id())); + + // Another node requesting that ledger still doesn't change things + Validation const val3 = b.validate(ID{4}, Seq{4}, 0s, 0s, true); + EXPECT_TRUE(ValStatus::Current == harness.add(val3)); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ID{4}) == 2); + EXPECT_TRUE( + harness.vals().getPreferred(kGenesisLedger) == + std::make_pair(ledgerAB.seq(), ledgerAB.id())); + + // Switch to validation that is available + harness.clock().advance(5s); + Ledger const ledgerABCDE = h["abcde"]; + EXPECT_TRUE(ValStatus::Current == harness.add(a.partial(ledgerABCDE))); + EXPECT_TRUE(ValStatus::Current == harness.add(b.partial(ledgerABCDE))); + EXPECT_TRUE( + harness.vals().getPreferred(kGenesisLedger) == + std::make_pair(ledgerABCDE.seq(), ledgerABCDE.id())); +} + +TEST(ValidationsTest, num_trusted_for_ledger) +{ + SCOPED_TRACE("NumTrustedForLedger"); + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + Node const b = harness.makeNode(); + Ledger const ledgerA = h["a"]; + + EXPECT_TRUE(ValStatus::Current == harness.add(a.partial(ledgerA))); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerA.id()) == 0); + + EXPECT_TRUE(ValStatus::Current == harness.add(b.validate(ledgerA))); + EXPECT_TRUE(harness.vals().numTrustedForLedger(ledgerA.id()) == 1); +} + +TEST(ValidationsTest, seq_enforcer) +{ + SCOPED_TRACE("SeqEnforcer"); + using Seq = Ledger::Seq; + using namespace std::chrono; + + beast::ManualClock clock; + SeqEnforcer enforcer; + + ValidationParms const p; + + EXPECT_TRUE(enforcer(clock.now(), Seq{1}, p)); + EXPECT_TRUE(enforcer(clock.now(), Seq{10}, p)); + EXPECT_TRUE(!enforcer(clock.now(), Seq{5}, p)); + EXPECT_TRUE(!enforcer(clock.now(), Seq{9}, p)); + clock.advance(p.validationSetExpires - 1ms); + EXPECT_TRUE(!enforcer(clock.now(), Seq{1}, p)); + clock.advance(2ms); + EXPECT_TRUE(enforcer(clock.now(), Seq{1}, p)); +} + +TEST(ValidationsTest, trust_changed) +{ + SCOPED_TRACE("TrustChanged"); + using namespace std::chrono; + + auto checker = [&](TestValidations& vals, + hash_set const& listed, + std::vector const& trustedVals) { + Ledger::ID const testID = + trustedVals.empty() ? kGenesisLedger.id() : trustedVals[0].ledgerID(); + Ledger::Seq const testSeq = + trustedVals.empty() ? kGenesisLedger.seq() : trustedVals[0].seq(); + EXPECT_TRUE(vals.currentTrusted() == trustedVals); + EXPECT_TRUE(vals.getCurrentNodeIDs() == listed); + EXPECT_TRUE(vals.getNodesAfter(kGenesisLedger, kGenesisLedger.id()) == trustedVals.size()); + if (trustedVals.empty()) + { + EXPECT_TRUE(vals.getPreferred(kGenesisLedger) == std::nullopt); + } + else + { + EXPECT_TRUE(vals.getPreferred(kGenesisLedger)->second == testID); + } + EXPECT_TRUE(vals.getTrustedForLedger(testID, testSeq) == trustedVals); + EXPECT_TRUE(vals.numTrustedForLedger(testID) == trustedVals.size()); + }; + + { + // Trusted to untrusted + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + Ledger const ledgerAB = h["ab"]; + Validation const v = a.validate(ledgerAB); + EXPECT_TRUE(ValStatus::Current == harness.add(v)); + + hash_set const listed({a.nodeID()}); + std::vector trustedVals({v}); + checker(harness.vals(), listed, trustedVals); + + trustedVals.clear(); + harness.vals().trustChanged({}, {a.nodeID()}); + checker(harness.vals(), listed, trustedVals); + } + + { + // Untrusted to trusted + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node a = harness.makeNode(); + a.untrust(); + Ledger const ledgerAB = h["ab"]; + Validation const v = a.validate(ledgerAB); + EXPECT_TRUE(ValStatus::Current == harness.add(v)); + + hash_set const listed({a.nodeID()}); + std::vector trustedVals; + checker(harness.vals(), listed, trustedVals); + + trustedVals.push_back(v); + harness.vals().trustChanged({a.nodeID()}, {}); + checker(harness.vals(), listed, trustedVals); + } + + { + // Trusted but not acquired -> untrusted + LedgerHistoryHelper h; + TestHarness harness(h.oracle); + Node const a = harness.makeNode(); + Validation const v = a.validate(Ledger::ID{2}, Ledger::Seq{2}, 0s, 0s, true); + EXPECT_TRUE(ValStatus::Current == harness.add(v)); + + hash_set const listed({a.nodeID()}); + std::vector trustedVals({v}); + auto& vals = harness.vals(); + EXPECT_TRUE(vals.currentTrusted() == trustedVals); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + EXPECT_TRUE(vals.getPreferred(kGenesisLedger)->second == v.ledgerID()); + EXPECT_TRUE(vals.getNodesAfter(kGenesisLedger, kGenesisLedger.id()) == 0); + + trustedVals.clear(); + harness.vals().trustChanged({}, {a.nodeID()}); + // make acquiring ledger available + h["ab"]; + EXPECT_TRUE(vals.currentTrusted() == trustedVals); + EXPECT_TRUE(vals.getPreferred(kGenesisLedger) == std::nullopt); + EXPECT_TRUE(vals.getNodesAfter(kGenesisLedger, kGenesisLedger.id()) == 0); + } +} + +} // namespace xrpl::test::csf diff --git a/src/tests/libxrpl/csf/BasicNetwork.cpp b/src/tests/libxrpl/csf/BasicNetwork.cpp new file mode 100644 index 0000000000..a6fc2d90e6 --- /dev/null +++ b/src/tests/libxrpl/csf/BasicNetwork.cpp @@ -0,0 +1,122 @@ +#include + +#include +#include + +#include +#include + +namespace xrpl::test { + +namespace { + +struct Peer +{ + int id; + std::set set; + + Peer(Peer const&) = default; + Peer(Peer&&) = default; + + explicit Peer(int id) : id(id) + { + } + + template + void + start(csf::Scheduler& scheduler, Net& net) + { + using namespace std::chrono_literals; + auto t = scheduler.in(1s, [&] { set.insert(0); }); + if (id == 0) + { + for (auto const link : net.links(this)) + { + net.send(this, link.target, [&, to = link.target] { to->receive(net, this, 1); }); + } + } + else + { + scheduler.cancel(t); + } + } + + template + void + receive(Net& net, Peer* from, int m) + { + set.insert(m); + ++m; + if (m < 5) + { + for (auto const link : net.links(this)) + { + net.send(this, link.target, [&, mm = m, to = link.target] { + to->receive(net, this, mm); + }); + } + } + } +}; + +} // namespace + +TEST(BasicNetworkTest, network) +{ + using namespace std::chrono_literals; + std::vector pv; + pv.emplace_back(0); + pv.emplace_back(1); + pv.emplace_back(2); + csf::Scheduler scheduler; + csf::BasicNetwork net(scheduler); + EXPECT_TRUE(!net.connect(&pv[0], &pv[0])); + EXPECT_TRUE(net.connect(&pv[0], &pv[1], 1s)); + EXPECT_TRUE(net.connect(&pv[1], &pv[2], 1s)); + EXPECT_TRUE(!net.connect(&pv[0], &pv[1])); + for (auto& peer : pv) + peer.start(scheduler, net); + EXPECT_TRUE(scheduler.stepFor(0s)); + EXPECT_TRUE(scheduler.stepFor(1s)); + EXPECT_TRUE(scheduler.step()); + EXPECT_TRUE(!scheduler.step()); + EXPECT_TRUE(!scheduler.stepFor(1s)); + net.send(&pv[0], &pv[1], [] {}); + net.send(&pv[1], &pv[0], [] {}); + EXPECT_TRUE(net.disconnect(&pv[0], &pv[1])); + EXPECT_TRUE(!net.disconnect(&pv[0], &pv[1])); + for (;;) + { + auto const links = net.links(&pv[1]); + if (links.empty()) + break; + EXPECT_TRUE(net.disconnect(&pv[1], links[0].target)); + } + EXPECT_TRUE(pv[0].set == std::set({0, 2, 4})); + EXPECT_TRUE(pv[1].set == std::set({1, 3})); + EXPECT_TRUE(pv[2].set == std::set({2, 4})); +} + +TEST(BasicNetworkTest, disconnect) +{ + using namespace std::chrono_literals; + csf::Scheduler scheduler; + csf::BasicNetwork net(scheduler); + EXPECT_TRUE(net.connect(0, 1, 1s)); + EXPECT_TRUE(net.connect(0, 2, 2s)); + + std::set delivered; + net.send(0, 1, [&]() { delivered.insert(1); }); + net.send(0, 2, [&]() { delivered.insert(2); }); + + scheduler.in(1000ms, [&]() { EXPECT_TRUE(net.disconnect(0, 2)); }); + scheduler.in(1100ms, [&]() { EXPECT_TRUE(net.connect(0, 2)); }); + + scheduler.step(); + + // only the first message is delivered because the disconnect at 1 s + // purges all pending messages from 0 to 2 + EXPECT_TRUE(delivered == std::set({1})); +} + +} // namespace xrpl::test diff --git a/src/test/csf/BasicNetwork.h b/src/tests/libxrpl/csf/BasicNetwork.h similarity index 99% rename from src/test/csf/BasicNetwork.h rename to src/tests/libxrpl/csf/BasicNetwork.h index 0428475504..2c68bd282d 100644 --- a/src/test/csf/BasicNetwork.h +++ b/src/tests/libxrpl/csf/BasicNetwork.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include diff --git a/src/test/csf/CollectorRef.h b/src/tests/libxrpl/csf/CollectorRef.h similarity index 97% rename from src/test/csf/CollectorRef.h rename to src/tests/libxrpl/csf/CollectorRef.h index 3aef4d617f..b1da962f3d 100644 --- a/src/test/csf/CollectorRef.h +++ b/src/tests/libxrpl/csf/CollectorRef.h @@ -1,11 +1,11 @@ #pragma once -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include diff --git a/src/tests/libxrpl/csf/Digraph.cpp b/src/tests/libxrpl/csf/Digraph.cpp new file mode 100644 index 0000000000..83c15ec06a --- /dev/null +++ b/src/tests/libxrpl/csf/Digraph.cpp @@ -0,0 +1,72 @@ +#include + +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +TEST(DigraphTest, digraph) +{ + using namespace csf; + using Graph = Digraph; + Graph graph; + + EXPECT_TRUE(!graph.connected('a', 'b')); + EXPECT_TRUE(!graph.edge('a', 'b')); + EXPECT_TRUE(!graph.disconnect('a', 'b')); + + EXPECT_TRUE(graph.connect('a', 'b', "foobar")); + EXPECT_TRUE(graph.connected('a', 'b')); + EXPECT_TRUE(*graph.edge('a', 'b') == "foobar"); // NOLINT(bugprone-unchecked-optional-access) + + EXPECT_TRUE(!graph.connect('a', 'b', "repeat")); + EXPECT_TRUE(graph.disconnect('a', 'b')); + EXPECT_TRUE(graph.connect('a', 'b', "repeat")); + EXPECT_TRUE(graph.connected('a', 'b')); + EXPECT_TRUE(*graph.edge('a', 'b') == "repeat"); // NOLINT(bugprone-unchecked-optional-access) + + EXPECT_TRUE(graph.connect('a', 'c', "tree")); + + { + std::vector> edges; + + for (auto const& edge : graph.outEdges('a')) + { + edges.emplace_back(edge.source, edge.target, edge.data); + } + + std::vector> expected; + expected.emplace_back('a', 'b', "repeat"); + expected.emplace_back('a', 'c', "tree"); + EXPECT_TRUE(edges == expected); + EXPECT_TRUE(graph.outDegree('a') == expected.size()); + } + + EXPECT_TRUE(graph.outEdges('r').size() == 0); + EXPECT_TRUE(graph.outDegree('r') == 0); + EXPECT_TRUE(graph.outDegree('c') == 0); + + // only 'a' has out edges + EXPECT_TRUE(graph.outVertices().size() == 1); + std::vector const expected = {'b', 'c'}; + + EXPECT_TRUE((graph.outVertices('a') == expected)); + EXPECT_TRUE(graph.outVertices('b').size() == 0); + EXPECT_TRUE(graph.outVertices('c').size() == 0); + EXPECT_TRUE(graph.outVertices('r').size() == 0); + + std::stringstream ss; + graph.saveDot(ss, [](char v) { return v; }); + std::string const expectedDot = + "digraph {\n" + "a -> b;\n" + "a -> c;\n" + "}\n"; + EXPECT_TRUE(ss.str() == expectedDot); +} + +} // namespace xrpl::test diff --git a/src/test/csf/Digraph.h b/src/tests/libxrpl/csf/Digraph.h similarity index 100% rename from src/test/csf/Digraph.h rename to src/tests/libxrpl/csf/Digraph.h diff --git a/src/tests/libxrpl/csf/Histogram.cpp b/src/tests/libxrpl/csf/Histogram.cpp new file mode 100644 index 0000000000..6de1cde593 --- /dev/null +++ b/src/tests/libxrpl/csf/Histogram.cpp @@ -0,0 +1,59 @@ +#include + +#include + +namespace xrpl::test { + +TEST(HistogramTest, histogram) +{ + using namespace csf; + Histogram hist; + + EXPECT_TRUE(hist.size() == 0); + EXPECT_TRUE(hist.numBins() == 0); + EXPECT_TRUE(hist.minValue() == 0); + EXPECT_TRUE(hist.maxValue() == 0); + EXPECT_TRUE(hist.avg() == 0); + EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue()); + EXPECT_TRUE(hist.percentile(0.5f) == 0); + EXPECT_TRUE(hist.percentile(0.9f) == 0); + EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue()); + + hist.insert(1); + + EXPECT_TRUE(hist.size() == 1); + EXPECT_TRUE(hist.numBins() == 1); + EXPECT_TRUE(hist.minValue() == 1); + EXPECT_TRUE(hist.maxValue() == 1); + EXPECT_TRUE(hist.avg() == 1); + EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue()); + EXPECT_TRUE(hist.percentile(0.5f) == 1); + EXPECT_TRUE(hist.percentile(0.9f) == 1); + EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue()); + + hist.insert(9); + + EXPECT_TRUE(hist.size() == 2); + EXPECT_TRUE(hist.numBins() == 2); + EXPECT_TRUE(hist.minValue() == 1); + EXPECT_TRUE(hist.maxValue() == 9); + EXPECT_TRUE(hist.avg() == 5); + EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue()); + EXPECT_TRUE(hist.percentile(0.5f) == 1); + EXPECT_TRUE(hist.percentile(0.9f) == 9); + EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue()); + + hist.insert(1); + + EXPECT_TRUE(hist.size() == 3); + EXPECT_TRUE(hist.numBins() == 2); + EXPECT_TRUE(hist.minValue() == 1); + EXPECT_TRUE(hist.maxValue() == 9); + EXPECT_TRUE(hist.avg() == 11 / 3); + EXPECT_TRUE(hist.percentile(0.0f) == hist.minValue()); + EXPECT_TRUE(hist.percentile(0.5f) == 1); + EXPECT_TRUE(hist.percentile(0.9f) == 9); + EXPECT_TRUE(hist.percentile(1.0f) == hist.maxValue()); +} + +} // namespace xrpl::test diff --git a/src/test/csf/Histogram.h b/src/tests/libxrpl/csf/Histogram.h similarity index 100% rename from src/test/csf/Histogram.h rename to src/tests/libxrpl/csf/Histogram.h diff --git a/src/test/csf/Peer.h b/src/tests/libxrpl/csf/Peer.h similarity index 98% rename from src/test/csf/Peer.h rename to src/tests/libxrpl/csf/Peer.h index 79bffec9cb..d4b6f42bdb 100644 --- a/src/test/csf/Peer.h +++ b/src/tests/libxrpl/csf/Peer.h @@ -1,33 +1,32 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - #include #include #include #include #include #include +#include +#include +#include +#include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include #include diff --git a/src/test/csf/PeerGroup.h b/src/tests/libxrpl/csf/PeerGroup.h similarity index 98% rename from src/test/csf/PeerGroup.h rename to src/tests/libxrpl/csf/PeerGroup.h index 1c31209ef3..ff99b779a3 100644 --- a/src/test/csf/PeerGroup.h +++ b/src/tests/libxrpl/csf/PeerGroup.h @@ -1,9 +1,9 @@ #pragma once -#include -#include -#include -#include +#include +#include +#include +#include #include #include diff --git a/src/test/csf/Proposal.h b/src/tests/libxrpl/csf/Proposal.h similarity index 66% rename from src/test/csf/Proposal.h rename to src/tests/libxrpl/csf/Proposal.h index ecf430ae8d..b2a97f9731 100644 --- a/src/test/csf/Proposal.h +++ b/src/tests/libxrpl/csf/Proposal.h @@ -1,10 +1,10 @@ #pragma once -#include -#include -#include +#include -#include +#include +#include +#include namespace xrpl::test::csf { /** diff --git a/src/test/csf/README.md b/src/tests/libxrpl/csf/README.md similarity index 100% rename from src/test/csf/README.md rename to src/tests/libxrpl/csf/README.md diff --git a/src/tests/libxrpl/csf/Scheduler.cpp b/src/tests/libxrpl/csf/Scheduler.cpp new file mode 100644 index 0000000000..63871e0623 --- /dev/null +++ b/src/tests/libxrpl/csf/Scheduler.cpp @@ -0,0 +1,61 @@ +#include + +#include + +#include + +namespace xrpl::test { + +TEST(SchedulerTest, scheduler) +{ + using namespace std::chrono_literals; + csf::Scheduler scheduler; + std::set seen; + + scheduler.in(1s, [&] { seen.insert(1); }); + scheduler.in(2s, [&] { seen.insert(2); }); + auto token = scheduler.in(3s, [&] { seen.insert(3); }); + scheduler.at(scheduler.now() + 4s, [&] { seen.insert(4); }); + scheduler.at(scheduler.now() + 8s, [&] { seen.insert(8); }); + + auto start = scheduler.now(); + + // Process first event + EXPECT_TRUE(seen.empty()); + EXPECT_TRUE(scheduler.stepOne()); + EXPECT_TRUE(seen == std::set({1})); + EXPECT_TRUE(scheduler.now() == (start + 1s)); + + // No processing if stepping until current time + EXPECT_TRUE(scheduler.stepUntil(scheduler.now())); + EXPECT_TRUE(seen == std::set({1})); + EXPECT_TRUE(scheduler.now() == (start + 1s)); + + // Process next event + EXPECT_TRUE(scheduler.stepFor(1s)); + EXPECT_TRUE(seen == std::set({1, 2})); + EXPECT_TRUE(scheduler.now() == (start + 2s)); + + // Don't process cancelled event, but advance clock + scheduler.cancel(token); + EXPECT_TRUE(scheduler.stepFor(1s)); + EXPECT_TRUE(seen == std::set({1, 2})); + EXPECT_TRUE(scheduler.now() == (start + 3s)); + + // Process until 3 seen ints + EXPECT_TRUE(scheduler.stepWhile([&]() { return seen.size() < 3; })); + EXPECT_TRUE(seen == std::set({1, 2, 4})); + EXPECT_TRUE(scheduler.now() == (start + 4s)); + + // Process the rest + EXPECT_TRUE(scheduler.step()); + EXPECT_TRUE(seen == std::set({1, 2, 4, 8})); + EXPECT_TRUE(scheduler.now() == (start + 8s)); + + // Process the rest again doesn't advance + EXPECT_TRUE(!scheduler.step()); + EXPECT_TRUE(seen == std::set({1, 2, 4, 8})); + EXPECT_TRUE(scheduler.now() == (start + 8s)); +} + +} // namespace xrpl::test diff --git a/src/test/csf/Scheduler.h b/src/tests/libxrpl/csf/Scheduler.h similarity index 100% rename from src/test/csf/Scheduler.h rename to src/tests/libxrpl/csf/Scheduler.h diff --git a/src/test/csf/Sim.h b/src/tests/libxrpl/csf/Sim.h similarity index 94% rename from src/test/csf/Sim.h rename to src/tests/libxrpl/csf/Sim.h index 94d26d5e06..774537138a 100644 --- a/src/test/csf/Sim.h +++ b/src/tests/libxrpl/csf/Sim.h @@ -1,16 +1,16 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include - #include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include #include diff --git a/src/test/csf/SimTime.h b/src/tests/libxrpl/csf/SimTime.h similarity index 100% rename from src/test/csf/SimTime.h rename to src/tests/libxrpl/csf/SimTime.h diff --git a/src/test/csf/TrustGraph.h b/src/tests/libxrpl/csf/TrustGraph.h similarity index 99% rename from src/test/csf/TrustGraph.h rename to src/tests/libxrpl/csf/TrustGraph.h index d46a887364..d010b954e0 100644 --- a/src/test/csf/TrustGraph.h +++ b/src/tests/libxrpl/csf/TrustGraph.h @@ -1,9 +1,9 @@ #pragma once -#include - #include +#include + #include #include #include diff --git a/src/test/csf/Tx.h b/src/tests/libxrpl/csf/Tx.h similarity index 100% rename from src/test/csf/Tx.h rename to src/tests/libxrpl/csf/Tx.h diff --git a/src/test/csf/Validation.h b/src/tests/libxrpl/csf/Validation.h similarity index 99% rename from src/test/csf/Validation.h rename to src/tests/libxrpl/csf/Validation.h index 0b9fc94890..4325f96511 100644 --- a/src/test/csf/Validation.h +++ b/src/tests/libxrpl/csf/Validation.h @@ -1,10 +1,10 @@ #pragma once -#include - #include #include +#include + #include #include #include diff --git a/src/test/csf/collectors.h b/src/tests/libxrpl/csf/collectors.h similarity index 99% rename from src/test/csf/collectors.h rename to src/tests/libxrpl/csf/collectors.h index f85854e5dd..05b63f592b 100644 --- a/src/test/csf/collectors.h +++ b/src/tests/libxrpl/csf/collectors.h @@ -1,13 +1,13 @@ #pragma once -#include -#include -#include -#include -#include - #include +#include +#include +#include +#include +#include + #include #include #include diff --git a/src/test/csf/csf_graph.png b/src/tests/libxrpl/csf/csf_graph.png similarity index 100% rename from src/test/csf/csf_graph.png rename to src/tests/libxrpl/csf/csf_graph.png diff --git a/src/test/csf/csf_overview.png b/src/tests/libxrpl/csf/csf_overview.png similarity index 100% rename from src/test/csf/csf_overview.png rename to src/tests/libxrpl/csf/csf_overview.png diff --git a/src/test/csf/events.h b/src/tests/libxrpl/csf/events.h similarity index 96% rename from src/test/csf/events.h rename to src/tests/libxrpl/csf/events.h index 2cf4fd9e9b..4ff15ec987 100644 --- a/src/test/csf/events.h +++ b/src/tests/libxrpl/csf/events.h @@ -1,8 +1,8 @@ #pragma once -#include -#include -#include +#include +#include +#include namespace xrpl::test::csf { diff --git a/src/test/csf/impl/Sim.cpp b/src/tests/libxrpl/csf/impl/Sim.cpp similarity index 93% rename from src/test/csf/impl/Sim.cpp rename to src/tests/libxrpl/csf/impl/Sim.cpp index bf4706927e..28ac5f126a 100644 --- a/src/test/csf/impl/Sim.cpp +++ b/src/tests/libxrpl/csf/impl/Sim.cpp @@ -1,7 +1,7 @@ -#include +#include -#include -#include +#include +#include #include #include diff --git a/src/test/csf/impl/ledgers.cpp b/src/tests/libxrpl/csf/impl/ledgers.cpp similarity index 98% rename from src/test/csf/impl/ledgers.cpp rename to src/tests/libxrpl/csf/impl/ledgers.cpp index 46a3600307..ed1cd927c4 100644 --- a/src/test/csf/impl/ledgers.cpp +++ b/src/tests/libxrpl/csf/impl/ledgers.cpp @@ -1,11 +1,11 @@ -#include - -#include +#include #include #include #include +#include + #include #include #include diff --git a/src/test/csf/ledgers.h b/src/tests/libxrpl/csf/ledgers.h similarity index 99% rename from src/test/csf/ledgers.h rename to src/tests/libxrpl/csf/ledgers.h index 09f5fa54de..ca4e44d5a6 100644 --- a/src/test/csf/ledgers.h +++ b/src/tests/libxrpl/csf/ledgers.h @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -9,6 +7,8 @@ #include +#include + #include #include #include diff --git a/src/test/csf/random.h b/src/tests/libxrpl/csf/random.h similarity index 98% rename from src/test/csf/random.h rename to src/tests/libxrpl/csf/random.h index f8df253642..007bdecb1b 100644 --- a/src/test/csf/random.h +++ b/src/tests/libxrpl/csf/random.h @@ -135,13 +135,13 @@ class PowerLawDistribution { double xmin_; double a_; - double inv_; + double inv_{1.0 / (1.0 - a_)}; std::uniform_real_distribution uf_{0, 1}; public: using result_type = double; - PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a}, inv_(1.0 / (1.0 - a_)) + PowerLawDistribution(double xmin, double a) : xmin_{xmin}, a_{a} { } diff --git a/src/test/csf/submitters.h b/src/tests/libxrpl/csf/submitters.h similarity index 96% rename from src/test/csf/submitters.h rename to src/tests/libxrpl/csf/submitters.h index 160d0bcd9f..4f27f0f665 100644 --- a/src/test/csf/submitters.h +++ b/src/tests/libxrpl/csf/submitters.h @@ -1,8 +1,8 @@ #pragma once -#include -#include -#include +#include +#include +#include #include #include diff --git a/src/test/csf/timers.h b/src/tests/libxrpl/csf/timers.h similarity index 96% rename from src/test/csf/timers.h rename to src/tests/libxrpl/csf/timers.h index 4f13b21b25..e89ea33698 100644 --- a/src/test/csf/timers.h +++ b/src/tests/libxrpl/csf/timers.h @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index 4abf77f578..20421ab916 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -1,6 +1,5 @@ #include -#include #include #include #include @@ -17,8 +16,6 @@ #include #include #include -#include -#include #include #include @@ -32,6 +29,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -389,7 +389,7 @@ RCLConsensus::Adaptor::onClose( if (!wrongLCL) { LedgerIndex const seq = prevLedger->header().seq + 1; - RCLCensorshipDetector::TxIDSeqVec proposed; + CensorshipDetector::TxIDSeqVec proposed; initialSet->visitLeaves( [&proposed, seq](boost::intrusive_ptr const& item) { diff --git a/src/xrpld/app/consensus/RCLConsensus.h b/src/xrpld/app/consensus/RCLConsensus.h index 4ffe18a7a8..4c07e7e646 100644 --- a/src/xrpld/app/consensus/RCLConsensus.h +++ b/src/xrpld/app/consensus/RCLConsensus.h @@ -1,20 +1,20 @@ #pragma once -#include #include #include #include #include #include #include -#include -#include -#include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -83,7 +83,7 @@ class RCLConsensus std::atomic prevRoundTime_{std::chrono::milliseconds{0}}; std::atomic mode_{ConsensusMode::Observing}; - RCLCensorshipDetector censorshipDetector_; + CensorshipDetector censorshipDetector_; NegativeUNLVote nUnlVote_; public: diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.h b/src/xrpld/app/consensus/RCLCxPeerPos.h index 050bdf6d36..078556dea8 100644 --- a/src/xrpld/app/consensus/RCLCxPeerPos.h +++ b/src/xrpld/app/consensus/RCLCxPeerPos.h @@ -1,11 +1,10 @@ #pragma once -#include - #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/consensus/RCLValidations.cpp b/src/xrpld/app/consensus/RCLValidations.cpp index 9d40e60b00..c587a04cf0 100644 --- a/src/xrpld/app/consensus/RCLValidations.cpp +++ b/src/xrpld/app/consensus/RCLValidations.cpp @@ -5,12 +5,12 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/consensus/RCLValidations.h b/src/xrpld/app/consensus/RCLValidations.h index 7eadaf0dff..963b6c150e 100644 --- a/src/xrpld/app/consensus/RCLValidations.h +++ b/src/xrpld/app/consensus/RCLValidations.h @@ -1,10 +1,9 @@ #pragma once -#include - #include #include #include +#include #include #include #include diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 4b0091dff6..47cbebb901 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -21,8 +21,6 @@ #include #include #include -#include -#include #include #include #include @@ -54,6 +52,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index c720bdf30b..d7a9a9e449 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -35,6 +34,7 @@ #include #include #include +#include #include #include #include From 7908aec2ec342a17924d81e373cc0b291318051d Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Thu, 23 Jul 2026 17:39:11 -0400 Subject: [PATCH 31/86] feat: Check default fields are not default when serializing (#6267) Co-authored-by: Vito <5780819+Tapanito@users.noreply.github.com> Co-authored-by: Ayaz Salikhov Co-authored-by: Bart --- src/libxrpl/protocol/STObject.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 4b3ace2be3..c2543d2cca 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -899,6 +899,10 @@ STObject::add(Serializer& s, WhichFields whichFields) const XRPL_ASSERT( (sType != STI_OBJECT) || (field->getFName().fieldType == STI_OBJECT), "xrpl::STObject::add : valid field type"); + XRPL_ASSERT( + getStyle(field->getFName()) != SoeDefault || !field->isDefault(), + "xrpl::STObject::add : non-default value"); + field->addFieldID(s); field->add(s); if (sType == STI_ARRAY || sType == STI_OBJECT) From 9afa1cf4d15665e5597ef69d91917baa69c79bce Mon Sep 17 00:00:00 2001 From: Kassaking7 <96991820+Kassaking7@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:40:21 -0400 Subject: [PATCH 32/86] fix: Update PermissionedDEX invariant domain tracking for valid offer replacement (#7387) Co-authored-by: Bart --- .../tx/invariants/PermissionedDEXInvariant.h | 3 +- .../invariants/PermissionedDEXInvariant.cpp | 14 ++++- src/test/app/PermissionedDEX_test.cpp | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h index 2763b80a94..ae0d573385 100644 --- a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h +++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h @@ -17,7 +17,8 @@ class ValidPermissionedDEX bool regularOffers_ = false; // post-fixCleanup3_2_0: excludes deleted offers bool badHybridsOld_ = false; // pre-fixCleanup3_1_3: missing field/domain or size > 1 bool badHybrids_ = false; // post-fixCleanup3_1_3: also catches size == 0 (size != 1) - hash_set domains_; + hash_set domainsOld_; // pre-fixCleanup3_4_0: also flags deleted domains + hash_set domains_; // post-fixCleanup3_4_0: excludes deleted domains public: void diff --git a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp index 1014642b36..44f623f284 100644 --- a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp +++ b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -19,17 +20,23 @@ namespace xrpl { void ValidPermissionedDEX::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) { + auto trackDomain = [this, isDelete](uint256 const& domain) { + domainsOld_.insert(domain); + if (!isDelete) + domains_.insert(domain); + }; + if (after && after->getType() == ltDIR_NODE) { if (after->isFieldPresent(sfDomainID)) - domains_.insert(after->getFieldH256(sfDomainID)); + trackDomain(after->getFieldH256(sfDomainID)); } if (after && after->getType() == ltOFFER) { if (after->isFieldPresent(sfDomainID)) { - domains_.insert(after->getFieldH256(sfDomainID)); + trackDomain(after->getFieldH256(sfDomainID)); } else { @@ -87,7 +94,8 @@ ValidPermissionedDEX::finalize( // for both payment and offercreate, there shouldn't be another domain // that's different from the domain specified - for (auto const& d : domains_) + auto const& domains = view.rules().enabled(fixCleanup3_4_0) ? domains_ : domainsOld_; + for (auto const& d : domains) { if (d != domain) { diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 998b7b1c7f..67cb7602a0 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -2001,6 +2001,61 @@ class PermissionedDEX_test : public beast::unit_test::Suite } } + void + testReplaceDomainOfferWithOtherDomainOffer(FeatureBitset features) + { + bool const fixEnabled = features[fixCleanup3_4_0]; + + testcase << "Replace domain offer via OfferCreate" + << (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainA, credType] = + PermissionedDEX(env); + + Account const domainOwnerB("permdex-domainOwnerB"); + auto const domainB = + setupDomain(env, {alice, bob, carol, gw}, domainOwnerB, "permdex-other-domain"); + BEAST_EXPECT(domainA != domainB); + + auto const oldSeq = env.seq(alice); + env(offer(alice, USD(100), XRP(1)), Domain(domainA)); + env.close(); + + BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true)); + auto const oldOffer = env.le(keylet::offer(alice.id(), oldSeq)); + if (!BEAST_EXPECT(oldOffer)) + return; + BEAST_EXPECT(oldOffer->getFieldH256(sfDomainID) == domainA); + + auto const newSeq = env.seq(alice); + // The invariant should reject mixing active Permissioned DEX domains, + // not a domain that is only touched because its offer is being deleted. + if (fixEnabled) + { + env(offer(alice, USD(100), XRP(2)), Domain(domainB), Json(jss::OfferSequence, oldSeq)); + env.close(); + + 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)); + if (!BEAST_EXPECT(newOffer)) + return; + BEAST_EXPECT(newOffer->getFieldH256(sfDomainID) == domainB); + } + else + { + env(offer(alice, USD(100), XRP(2)), + Domain(domainB), + Json(jss::OfferSequence, oldSeq), + Ter(tecINVARIANT_FAILED)); + env.close(); + + BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true)); + BEAST_EXPECT(!offerExists(env, alice, newSeq)); + } + } + public: void run() override @@ -2038,6 +2093,8 @@ public: // only after fixCleanup3_2_0. testCancelRegularOfferWithDomainCreate(all); testCancelRegularOfferWithDomainCreate(all - fixCleanup3_2_0); + testReplaceDomainOfferWithOtherDomainOffer(all); + testReplaceDomainOfferWithOtherDomainOffer(all - fixCleanup3_4_0); } }; From 29d74142aee00930efcbf809efd0104f6c688fe5 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Fri, 24 Jul 2026 15:59:24 +0100 Subject: [PATCH 33/86] =?UTF-8?q?build:=20Pat=D1=81h=20binary=20in=20local?= =?UTF-8?q?=20Linux=20nix=20environment=20(#7859)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/scripts/strategy-matrix/linux.json | 2 +- .github/workflows/build-nix-images.yml | 2 ++ .github/workflows/on-pr.yml | 1 + .github/workflows/on-trigger.yml | 1 + .github/workflows/publish-docs.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/reusable-upload-recipe.yml | 2 +- .../default-loader-path.sh | 0 cmake/CompilationEnv.cmake | 21 +++++++++++ cmake/PatchNixBinary.cmake | 35 ++++++++++++------- cmake/XrplCompiler.cmake | 3 +- cmake/XrplSanity.cmake | 13 +++++++ nix/devshell.nix | 34 +++++++++++++----- nix/docker/Dockerfile | 2 +- nix/docker/README.md | 7 ++-- 15 files changed, 97 insertions(+), 30 deletions(-) rename nix/docker/loader-path.sh => bin/default-loader-path.sh (100%) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 992f314686..2a0b5e8e0e 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -1,5 +1,5 @@ { - "image_tag": "sha-3122de8", + "image_tag": "sha-40cdf49", "configs": { "ubuntu": [ { diff --git a/.github/workflows/build-nix-images.yml b/.github/workflows/build-nix-images.yml index 8574182a7e..fe2f43fdcc 100644 --- a/.github/workflows/build-nix-images.yml +++ b/.github/workflows/build-nix-images.yml @@ -13,6 +13,7 @@ on: - "!nix/docker/README.md" - "!nix/devshell.nix" - "bin/check-tools.sh" + - "bin/default-loader-path.sh" - "bin/install-sanitizer-libs.sh" pull_request: paths: @@ -24,6 +25,7 @@ on: - "!nix/docker/README.md" - "!nix/devshell.nix" - "bin/check-tools.sh" + - "bin/default-loader-path.sh" - "bin/install-sanitizer-libs.sh" workflow_dispatch: diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 442a202a44..13c807ffca 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -90,6 +90,7 @@ jobs: .clang-tidy .codecov.yml bin/check-tools.sh + bin/default-loader-path.sh cfg/** cmake/** conan/** diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 49a93d2746..b8899cec72 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -28,6 +28,7 @@ on: - ".clang-tidy" - ".codecov.yml" - "bin/check-tools.sh" + - "bin/default-loader-path.sh" - "cfg/**" - "cmake/**" - "conan/**" diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 19c73f93d6..49f5c021a3 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -41,7 +41,7 @@ env: jobs: build: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3122de8 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49 steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index e81bbea367..90f24bc464 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -34,7 +34,7 @@ jobs: needs: [determine-files] if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }} runs-on: ["self-hosted", "Linux", "X64", "heavy"] - container: "ghcr.io/xrplf/xrpld/nix-debian:sha-3122de8" + container: "ghcr.io/xrplf/xrpld/nix-debian:sha-40cdf49" permissions: contents: read issues: write diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index df4a2d9516..0f00ce7ca0 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -40,7 +40,7 @@ defaults: jobs: upload: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3122de8 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49 env: REMOTE_NAME: ${{ inputs.remote_name }} CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }} diff --git a/nix/docker/loader-path.sh b/bin/default-loader-path.sh similarity index 100% rename from nix/docker/loader-path.sh rename to bin/default-loader-path.sh diff --git a/cmake/CompilationEnv.cmake b/cmake/CompilationEnv.cmake index 8e69a4dfdd..471c43d6c6 100644 --- a/cmake/CompilationEnv.cmake +++ b/cmake/CompilationEnv.cmake @@ -29,6 +29,27 @@ if(CMAKE_GENERATOR STREQUAL "Xcode") set(is_xcode TRUE) endif() +# -------------------------------------------------------------------- +# Nix toolchain detection +# -------------------------------------------------------------------- +# True when the C++ compiler resolves into the Nix store. CMAKE_CXX_COMPILER may +# be referenced through a symlink outside the store (a Nix profile, a /usr/bin +# alternative, ...), so resolve the real path before matching. +set(is_nix_compiler FALSE) +get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH) +if(_cxx_real MATCHES "^/nix/store/") + set(is_nix_compiler TRUE) +endif() +unset(_cxx_real) + +# True inside the Nix CI Docker image, identified by the /nix/ci-env tree it +# ships (see nix/docker/Dockerfile). The dev shell and bare systems don't have +# it, so it distinguishes the CI image from other Nix-compiler environments. +set(is_ci_image FALSE) +if(EXISTS "/nix/ci-env/bin") + set(is_ci_image TRUE) +endif() + # -------------------------------------------------------------------- # Operating system detection # -------------------------------------------------------------------- diff --git a/cmake/PatchNixBinary.cmake b/cmake/PatchNixBinary.cmake index 79ca0b150c..2490416f1f 100644 --- a/cmake/PatchNixBinary.cmake +++ b/cmake/PatchNixBinary.cmake @@ -1,26 +1,37 @@ #[===================================================================[ Patch executables to run in non-Nix environments. - 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. + The Nix toolchain links binaries against an ELF interpreter (loader) + that lives in the Nix store, so the resulting binaries don't run elsewhere. + `patch_nix_binary` adds a POST_BUILD step that resets the interpreter + to the system default loader and drops the rpath. - 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 - is skipped for sanitizer builds, whose runtime libraries are resolved through - the rpath. Everywhere else `patch_nix_binary` is a no-op. + This runs by default for Nix-toolchain builds (determined by whether the compiler resolves under /nix/store/). + Those builds are where binaries get a Nix-store loader. + It is opted out of by setting the XRPLD_NO_PATCH_NIX_BINARY environment variable — + the plain Nix dev shells set it, since their binaries link a newer glibc + and must not be retargeted to the system loader. + + Non-Nix builds (a system compiler, already using the system loader) and sanitizer builds + (runtime libraries resolved through the rpath) are skipped too. + Everywhere else `patch_nix_binary` is a no-op. + + The default loader is resolved by bin/default-loader-path.sh. #]===================================================================] include_guard(GLOBAL) include(CompilationEnv) -# Provided by the Nix-based CI image; prints the system default ELF loader path. -set(_loader_path_script "/tmp/loader-path.sh") +# Resolves the system default ELF loader path for the current architecture. +set(_loader_path_script "${CMAKE_SOURCE_DIR}/bin/default-loader-path.sh") -if(is_linux AND NOT SANITIZERS_ENABLED AND EXISTS "${_loader_path_script}") +if( + is_linux + AND NOT SANITIZERS_ENABLED + AND is_nix_compiler + AND NOT DEFINED ENV{XRPLD_NO_PATCH_NIX_BINARY} +) execute_process( COMMAND "${_loader_path_script}" OUTPUT_VARIABLE DEFAULT_LOADER_PATH diff --git a/cmake/XrplCompiler.cmake b/cmake/XrplCompiler.cmake index cb4e797137..e262acf1c9 100644 --- a/cmake/XrplCompiler.cmake +++ b/cmake/XrplCompiler.cmake @@ -171,9 +171,8 @@ else() # Clang wrapper supplies those paths itself (via -nostdinc++), so at compile time the # flag is unused -> Clang errors under our -Werror. At link time the flag IS consumed # (it selects the C++ runtime), so we move it there instead of dropping it entirely. - get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH) if( - _cxx_real MATCHES "^/nix/store/" + is_nix_compiler AND is_linux AND is_clang AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc" diff --git a/cmake/XrplSanity.cmake b/cmake/XrplSanity.cmake index a35645ad5c..ba9f7988bc 100644 --- a/cmake/XrplSanity.cmake +++ b/cmake/XrplSanity.cmake @@ -36,6 +36,19 @@ elseif(is_gcc) endif() endif() +# A Nix compiler is only meant to be used from a managed environment: the xrpld +# dev shell (which exports XRPL_DEVSHELL) or the CI image. Using one from a bare +# shell usually means a leaked toolchain (picked up via PATH or a Conan profile) +# and leads to confusing breakage, so fail early with guidance. +if(is_nix_compiler AND NOT is_ci_image AND NOT DEFINED ENV{XRPL_DEVSHELL}) + message( + FATAL_ERROR + "A Nix compiler (${CMAKE_CXX_COMPILER}) is being used outside the xrpld " + "dev shell. Enter it with `nix develop` (see docs/build/nix.md) before " + "configuring the build." + ) +endif() + # check for in-source build and fail if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") message( diff --git a/nix/devshell.nix b/nix/devshell.nix index 9b453ddef8..cb4a99c76a 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -50,12 +50,17 @@ let # compilerName is the command used to print the version, or null for none. makeShell = { + shellName, stdenv, compilerName, version ? null, versionedTools ? [ ], extraPackages ? [ ], warningHook ? "", + # Opt out of PatchNixBinary.cmake retargeting binaries to the system + # loader. The plain toolchain links a newer glibc, so it must not be + # patched; the custom toolchain patches by default. + noPatchNixBinary ? false, }: let compilerVersionHook = @@ -73,14 +78,20 @@ let tools = versionedTools; }); in - (pkgs.mkShell.override { inherit stdenv; }) { - packages = commonPackages ++ versionedLinks ++ extraPackages; - shellHook = '' - echo "Welcome to xrpld development shell"; - ${compilerVersionHook} - ${warningHook} - ''; - }; + (pkgs.mkShell.override { inherit stdenv; }) ( + { + packages = commonPackages ++ versionedLinks ++ extraPackages; + # Marks a managed dev shell, so the build (XrplSanity.cmake) can tell an + # intentional Nix toolchain from one leaked into a bare shell. + XRPL_DEVSHELL = shellName; + shellHook = '' + echo "Welcome to xrpld development shell"; + ${compilerVersionHook} + ${warningHook} + ''; + } + // pkgs.lib.optionalAttrs noPatchNixBinary { XRPLD_NO_PATCH_NIX_BINARY = "1"; } + ); in rec { # macOS: Nix Clang. Linux: Nix GCC. @@ -89,6 +100,7 @@ rec { # gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no # custom glibc, so they fall back to the plain nixpkgs toolchain. gcc = makeShell { + shellName = "gcc"; stdenv = customGccStdenv; compilerName = "gcc"; version = gccVersion; @@ -97,6 +109,7 @@ rec { }; clang = makeShell { + shellName = "clang"; stdenv = customClangStdenv; compilerName = "clang"; version = llvmVersion; @@ -105,6 +118,7 @@ rec { # Nix provides no compiler; use the one from your system (e.g. Apple Clang). no-compiler = makeShell { + shellName = "no-compiler"; stdenv = pkgs.stdenvNoCC; compilerName = null; }; @@ -115,19 +129,23 @@ rec { # makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc. // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { gcc-plain = makeShell { + shellName = "gcc-plain"; stdenv = plainGccStdenv; compilerName = "gcc"; version = gccVersion; versionedTools = gccVersionedTools; extraPackages = [ plainGcov ]; warningHook = plainWarningHook; + noPatchNixBinary = true; }; clang-plain = makeShell { + shellName = "clang-plain"; stdenv = plainClangStdenv; compilerName = "clang"; version = llvmVersion; versionedTools = clangVersionedTools; warningHook = plainWarningHook; + noPatchNixBinary = true; }; } diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index 53b646ac7c..74c630cb61 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -58,7 +58,7 @@ ENV GIT_SSL_CAINFO="/nix/ci-env/etc/ssl/certs/ca-bundle.crt" # Externally-built dynamically-linked ELF binaries hard-code the loader path # (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Install it # from the Nix store when the base image doesn't already provide one. -COPY nix/docker/loader-path.sh /tmp/loader-path.sh +COPY bin/default-loader-path.sh /tmp/loader-path.sh RUN < Date: Fri, 24 Jul 2026 16:52:45 +0100 Subject: [PATCH 34/86] chore: Verify tooling version for Nix-managed environments (#7862) --- .github/workflows/check-tools.yml | 114 ++++++++++++++++++++++++++++ bin/check-tools.sh | 21 +++-- docs/build/nix.md | 8 ++ nix/check-tools/README.md | 49 ++++++++++++ nix/check-tools/macos.txt | 47 ++++++++++++ nix/check-tools/nix-nixos-amd64.txt | 55 ++++++++++++++ nix/check-tools/nix-nixos-arm64.txt | 55 ++++++++++++++ 7 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/check-tools.yml create mode 100644 nix/check-tools/README.md create mode 100644 nix/check-tools/macos.txt create mode 100644 nix/check-tools/nix-nixos-amd64.txt create mode 100644 nix/check-tools/nix-nixos-arm64.txt diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml new file mode 100644 index 0000000000..02e2038de0 --- /dev/null +++ b/.github/workflows/check-tools.yml @@ -0,0 +1,114 @@ +# Verifies the committed snapshots of `bin/check-tools.sh` output for each Nix +# environment (see nix/check-tools/). If the environment changes — a new image +# tag, an updated flake.lock, a different tool list — without the matching +# snapshot being regenerated and committed, this workflow fails so the drift is +# caught in review. +# +# To regenerate the snapshots, see nix/check-tools/README.md. +name: Check tools + +on: + pull_request: + paths: + - ".github/workflows/check-tools.yml" + - ".github/scripts/strategy-matrix/linux.json" + - "bin/check-tools.sh" + - "nix/check-tools/**" + - "flake.nix" + - "flake.lock" + - "rust-toolchain.toml" + push: + branches: + - "develop" + paths: + - ".github/workflows/check-tools.yml" + - ".github/scripts/strategy-matrix/linux.json" + - "bin/check-tools.sh" + - "nix/check-tools/**" + - "flake.nix" + - "flake.lock" + - "rust-toolchain.toml" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + # The nix-nixos image tag is pinned alongside the build matrix in linux.json, + # so snapshots are checked against the exact image CI builds against. + linux-image-tag: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Read nix image tag + id: tag + run: echo "tag=$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" >>"${GITHUB_OUTPUT}" + + # One job for all environments; they differ only in whether the tools come + # from the nix-nixos container (Linux) or `nix develop` (macOS). + check-tools: + needs: linux-image-tag + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu + snapshot: nix/check-tools/nix-nixos-amd64.txt + nix_develop: false + - runner: ubuntu-arm64 + snapshot: nix/check-tools/nix-nixos-arm64.txt + nix_develop: false + - runner: macos-26-apple-clang-21 + snapshot: nix/check-tools/macos.txt + nix_develop: true + runs-on: ${{ matrix.runner }} + # Linux runs inside the pinned nix-nixos image; macOS runs natively and uses + # the flake's dev shell instead (see the run step below). + container: ${{ !matrix.nix_develop && format('ghcr.io/xrplf/xrpld/nix-nixos:{0}', needs.linux-image-tag.outputs.tag) || null }} + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Prepare runner + uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 + with: + enable_ccache: false + + - name: Regenerate snapshot + env: + CHECK_TOOLS_SKIP_CLONE: "1" + # check-tools.sh skips some macOS tools when CI is set; the snapshots + # capture the full `nix develop` environment, so unset it here. + CI: "" + run: | + if [ "${{ matrix.nix_develop }}" = "true" ]; then + # `nix develop` prints the dev-shell greeting first; keep only the + # check-tools.sh output (from the "Detected OS:" line onward). + nix --extra-experimental-features "nix-command flakes" develop \ + -c bash bin/check-tools.sh | sed -n '/^Detected OS:/,$p' >"${{ matrix.snapshot }}" + else + bash bin/check-tools.sh >"${{ matrix.snapshot }}" + fi + + - name: Verify snapshot is up to date + run: | + if ! git diff --exit-code -- "${{ matrix.snapshot }}"; then + echo "::error::${{ matrix.snapshot }} is out of date. Regenerate it (see nix/check-tools/README.md) and commit the result." + exit 1 + fi + + - name: Upload regenerated snapshot + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: check-tools-${{ runner.os }}-${{ runner.arch }} + path: ${{ matrix.snapshot }} diff --git a/bin/check-tools.sh b/bin/check-tools.sh index 7886bcf8b0..e230302742 100755 --- a/bin/check-tools.sh +++ b/bin/check-tools.sh @@ -30,8 +30,10 @@ missing=() checked=0 # check [probe-command...] -# Runs the probe (default: " --version") quietly. Records as -# missing if the command is not found or exits non-zero. +# Runs the probe (default: " --version"), capturing both stdout and +# stderr, and prints one aligned line: the status, the name, and the first +# non-blank line of the probe output (its version). Records as missing +# if the command is not found or exits non-zero. check() { local name="$1" shift @@ -40,10 +42,11 @@ check() { probe=("${name}" --version) fi - echo "Checking ${name}..." checked=$((checked + 1)) - if "${probe[@]}" | head -n 1; then - printf ' [ ok ] %s\n' "${name}" + local output version + if output="$("${probe[@]}" 2>&1)"; then + version="$(printf '%s\n' "${output}" | grep -m1 '[^[:space:]]' || true)" + printf ' [ ok ] %-20s %s\n' "${name}" "${version}" else printf ' [MISS] %s\n' "${name}" missing+=("${name}") @@ -85,12 +88,14 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then check file check less check make - check netstat which netstat + # net-tools netstat reports "net-tools X.Y"; macOS ships BSD netstat with no + # version flag, so fall back to a presence marker there. + check netstat sh -c 'command -v netstat >/dev/null && { netstat --version 2>&1 | grep -m1 -oE "net-tools [0-9.]+" || echo present; }' check ninja - check perl + check perl perl -e 'print "$^V\n"' check pkg-config check vim - check zip + check zip bash -c 'zip --version 2>&1 | grep -m1 -oE "Zip [0-9.]+"' # These tools are present in our Linux CI images and in local development # setups, but not in the macOS CI environment. So check them everywhere diff --git a/docs/build/nix.md b/docs/build/nix.md index b95b82fc42..d0001294e3 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -154,6 +154,14 @@ conan install .. --output-folder . --build '*' --settings build_type=Release To update `flake.lock` to the latest revision use `nix flake update` command. +## Tooling snapshots + +The tool versions in each Nix environment are recorded in +[`nix/check-tools/`](../../nix/check-tools) and verified by CI. If you change the +environment (bump the CI image tag, update `flake.lock`, or edit the tool list in +`bin/check-tools.sh`), CI fails until you regenerate and commit the affected +snapshot — see [`nix/check-tools/README.md`](../../nix/check-tools/README.md). + ## Troubleshooting See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues, diff --git a/nix/check-tools/README.md b/nix/check-tools/README.md new file mode 100644 index 0000000000..fe9ab3e250 --- /dev/null +++ b/nix/check-tools/README.md @@ -0,0 +1,49 @@ +# check-tools snapshots + +These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh) +— the versions of the development tooling — in each Nix environment: + +| File | Environment | +| --------------------- | ----------------------------------- | +| `nix-nixos-amd64.txt` | `nix-nixos` CI image, `linux/amd64` | +| `nix-nixos-arm64.txt` | `nix-nixos` CI image, `linux/arm64` | +| `macos.txt` | macOS, inside `nix develop` | + +The [`check-tools`](../../.github/workflows/check-tools.yml) workflow regenerates +each snapshot in its environment and fails if it differs from the committed file. +So if you change the environment (bump the image tag in +[`linux.json`](../../.github/scripts/strategy-matrix/linux.json), update +`flake.lock`, change the tool list in `check-tools.sh`, …) you must regenerate +and commit the affected snapshots. + +Each snapshot is `check-tools.sh` stdout with the git-clone connectivity check +skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it contains only deterministic version +data. On macOS the dev-shell greeting that `nix develop` prints first is dropped +with `sed -n '/^Detected OS:/,$p'`. + +## Regenerating + +The two Linux snapshots come from the `nix-nixos` image (Docker or a compatible +runtime such as Apple `container`). The image tag is pinned in `linux.json`: + +```bash +img="ghcr.io/xrplf/xrpld/nix-nixos:$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" + +for arch in amd64 arm64; do + container run --rm -i -e CHECK_TOOLS_SKIP_CLONE=1 -a "${arch}" --entrypoint bash "${img}" -s \ + "nix/check-tools/nix-nixos-${arch}.txt" +done +``` + +(With Docker, replace `container run … -a "${arch}"` with +`docker run … --platform "linux/${arch}"`.) + +The macOS snapshot is generated locally. `CI=` is unset so `check-tools.sh` +checks the full dev-shell tool set (it otherwise skips some tools when `CI` is +set): + +```bash +CI= nix develop -c bash -c 'CHECK_TOOLS_SKIP_CLONE=1 bash bin/check-tools.sh' | + sed -n '/^Detected OS:/,$p' \ + >nix/check-tools/macos.txt +``` diff --git a/nix/check-tools/macos.txt b/nix/check-tools/macos.txt new file mode 100644 index 0000000000..af9814e9cb --- /dev/null +++ b/nix/check-tools/macos.txt @@ -0,0 +1,47 @@ +Detected OS: macos (Darwin arm64) + +Core build tools: + [ ok ] cmake cmake version 4.1.2 + [ ok ] conan Conan version 2.28.1 + [ ok ] git git version 2.54.0 + [ ok ] python3 Python 3.13.13 + +Development tooling: + [ ok ] ccache ccache version 4.13.6 + [ ok ] clang clang version 21.1.8 + [ ok ] clang++ clang version 21.1.8 + [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 + [ ok ] curl curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 + [ ok ] file file-5.47 + [ ok ] less less 692 (PCRE2 regular expressions) + [ ok ] make GNU Make 4.4.1 + [ ok ] netstat present + [ ok ] ninja 1.13.2 + [ ok ] perl v5.42.0 + [ ok ] pkg-config 0.29.2 + [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) + [ ok ] zip Zip 3.0 + [ ok ] clang-format clang-format version 21.1.8 + [ ok ] dot dot - graphviz version 12.2.1 (0) + [ ok ] doxygen 1.16.1 + [ ok ] gcovr gcovr 8.4 + [ ok ] gh gh version 2.94.0 (nixpkgs) + [ ok ] git-cliff git-cliff 2.13.1 + [ ok ] git-lfs git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3) + [ ok ] gpg gpg (GnuPG) 2.4.9 + [ ok ] pre-commit pre-commit 4.5.1 + [ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + +Rust toolchain: + [ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21) + [ ok ] cargo-audit cargo-audit-audit 0.22.1 + [ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5 + [ ok ] cargo-nextest cargo-nextest 0.9.137 + [ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14) + [ ok ] rust-analyzer rust-analyzer 1.95.0 (59807616 2026-04-14) + [ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14) + [ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14) + +Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). + +All 36 checked tools are present and runnable. diff --git a/nix/check-tools/nix-nixos-amd64.txt b/nix/check-tools/nix-nixos-amd64.txt new file mode 100644 index 0000000000..b922cca4a8 --- /dev/null +++ b/nix/check-tools/nix-nixos-amd64.txt @@ -0,0 +1,55 @@ +Detected OS: linux (Linux x86_64) + +Core build tools: + [ ok ] cmake cmake version 4.1.2 + [ ok ] conan Conan version 2.28.1 + [ ok ] git git version 2.54.0 + [ ok ] python3 Python 3.13.13 + +Development tooling: + [ ok ] ccache ccache version 4.13.6 + [ ok ] clang clang version 22.1.7 + [ ok ] clang++ clang version 22.1.7 + [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 + [ ok ] curl curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 + [ ok ] file file-5.47 + [ ok ] less less 692 (PCRE2 regular expressions) + [ ok ] make GNU Make 4.4.1 + [ ok ] netstat net-tools 2.10 + [ ok ] ninja 1.13.2 + [ ok ] perl v5.42.0 + [ ok ] pkg-config 0.29.2 + [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) + [ ok ] zip Zip 3.0 + [ ok ] clang-format clang-format version 22.1.7 + [ ok ] dot dot - graphviz version 12.2.1 (0) + [ ok ] doxygen 1.16.1 + [ ok ] gcovr gcovr 8.4 + [ ok ] gh gh version 2.94.0 (nixpkgs) + [ ok ] git-cliff git-cliff 2.13.1 + [ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3) + [ ok ] gpg gpg (GnuPG) 2.4.9 + [ ok ] pre-commit pre-commit 4.5.1 + [ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + +Rust toolchain: + [ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21) + [ ok ] cargo-audit cargo-audit-audit 0.22.1 + [ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5 + [ ok ] cargo-nextest cargo-nextest 0.9.137 + [ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14) + [ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14) + [ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14) + [ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14) + +GCC toolchain: + [ ok ] gcc gcc (GCC) 15.2.0 + [ ok ] g++ g++ (GCC) 15.2.0 + [ ok ] gcov gcov (GCC) 15.2.0 + +Mold: + [ ok ] mold mold 2.41.0 (compatible with GNU ld) + +Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). + +All 40 checked tools are present and runnable. diff --git a/nix/check-tools/nix-nixos-arm64.txt b/nix/check-tools/nix-nixos-arm64.txt new file mode 100644 index 0000000000..5267839682 --- /dev/null +++ b/nix/check-tools/nix-nixos-arm64.txt @@ -0,0 +1,55 @@ +Detected OS: linux (Linux aarch64) + +Core build tools: + [ ok ] cmake cmake version 4.1.2 + [ ok ] conan Conan version 2.28.1 + [ ok ] git git version 2.54.0 + [ ok ] python3 Python 3.13.13 + +Development tooling: + [ ok ] ccache ccache version 4.13.6 + [ ok ] clang clang version 22.1.7 + [ ok ] clang++ clang version 22.1.7 + [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 + [ ok ] curl curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 + [ ok ] file file-5.47 + [ ok ] less less 692 (PCRE2 regular expressions) + [ ok ] make GNU Make 4.4.1 + [ ok ] netstat net-tools 2.10 + [ ok ] ninja 1.13.2 + [ ok ] perl v5.42.0 + [ ok ] pkg-config 0.29.2 + [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) + [ ok ] zip Zip 3.0 + [ ok ] clang-format clang-format version 22.1.7 + [ ok ] dot dot - graphviz version 12.2.1 (0) + [ ok ] doxygen 1.16.1 + [ ok ] gcovr gcovr 8.4 + [ ok ] gh gh version 2.94.0 (nixpkgs) + [ ok ] git-cliff git-cliff 2.13.1 + [ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3) + [ ok ] gpg gpg (GnuPG) 2.4.9 + [ ok ] pre-commit pre-commit 4.5.1 + [ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + +Rust toolchain: + [ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21) + [ ok ] cargo-audit cargo-audit-audit 0.22.1 + [ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5 + [ ok ] cargo-nextest cargo-nextest 0.9.137 + [ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14) + [ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14) + [ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14) + [ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14) + +GCC toolchain: + [ ok ] gcc gcc (GCC) 15.2.0 + [ ok ] g++ g++ (GCC) 15.2.0 + [ ok ] gcov gcov (GCC) 15.2.0 + +Mold: + [ ok ] mold mold 2.41.0 (compatible with GNU ld) + +Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). + +All 40 checked tools are present and runnable. From fecfc0cf3faf88799e1bc3b40dff821b1572ffc0 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Fri, 24 Jul 2026 18:30:20 +0100 Subject: [PATCH 35/86] chore: Fix clang version in devshell (#7860) Co-authored-by: Ayaz Salikhov --- .github/workflows/check-tools.yml | 14 +++++++------- nix/check-tools/macos.txt | 6 +++--- ...ix-nixos-amd64.txt => nix-ubuntu-amd64.txt} | 0 ...ix-nixos-arm64.txt => nix-ubuntu-arm64.txt} | 0 nix/packages.nix | 18 +++++++++++++++++- 5 files changed, 27 insertions(+), 11 deletions(-) rename nix/check-tools/{nix-nixos-amd64.txt => nix-ubuntu-amd64.txt} (100%) rename nix/check-tools/{nix-nixos-arm64.txt => nix-ubuntu-arm64.txt} (100%) diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index 02e2038de0..6daaf98114 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -13,7 +13,7 @@ on: - ".github/workflows/check-tools.yml" - ".github/scripts/strategy-matrix/linux.json" - "bin/check-tools.sh" - - "nix/check-tools/**" + - "nix/**" - "flake.nix" - "flake.lock" - "rust-toolchain.toml" @@ -24,7 +24,7 @@ on: - ".github/workflows/check-tools.yml" - ".github/scripts/strategy-matrix/linux.json" - "bin/check-tools.sh" - - "nix/check-tools/**" + - "nix/**" - "flake.nix" - "flake.lock" - "rust-toolchain.toml" @@ -61,11 +61,11 @@ jobs: fail-fast: false matrix: include: - - runner: ubuntu - snapshot: nix/check-tools/nix-nixos-amd64.txt + - runner: ubuntu-latest + snapshot: nix/check-tools/nix-ubuntu-amd64.txt nix_develop: false - - runner: ubuntu-arm64 - snapshot: nix/check-tools/nix-nixos-arm64.txt + - runner: ubuntu-24.04-arm + snapshot: nix/check-tools/nix-ubuntu-arm64.txt nix_develop: false - runner: macos-26-apple-clang-21 snapshot: nix/check-tools/macos.txt @@ -73,7 +73,7 @@ jobs: runs-on: ${{ matrix.runner }} # Linux runs inside the pinned nix-nixos image; macOS runs natively and uses # the flake's dev shell instead (see the run step below). - container: ${{ !matrix.nix_develop && format('ghcr.io/xrplf/xrpld/nix-nixos:{0}', needs.linux-image-tag.outputs.tag) || null }} + container: ${{ !matrix.nix_develop && format('ghcr.io/xrplf/xrpld/nix-ubuntu:{0}', needs.linux-image-tag.outputs.tag) || null }} steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/nix/check-tools/macos.txt b/nix/check-tools/macos.txt index af9814e9cb..93cc926181 100644 --- a/nix/check-tools/macos.txt +++ b/nix/check-tools/macos.txt @@ -8,8 +8,8 @@ Core build tools: Development tooling: [ ok ] ccache ccache version 4.13.6 - [ ok ] clang clang version 21.1.8 - [ ok ] clang++ clang version 21.1.8 + [ ok ] clang clang version 22.1.7 + [ ok ] clang++ clang version 22.1.7 [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 [ ok ] curl curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 [ ok ] file file-5.47 @@ -21,7 +21,7 @@ Development tooling: [ ok ] pkg-config 0.29.2 [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) [ ok ] zip Zip 3.0 - [ ok ] clang-format clang-format version 21.1.8 + [ ok ] clang-format clang-format version 22.1.7 [ ok ] dot dot - graphviz version 12.2.1 (0) [ ok ] doxygen 1.16.1 [ ok ] gcovr gcovr 8.4 diff --git a/nix/check-tools/nix-nixos-amd64.txt b/nix/check-tools/nix-ubuntu-amd64.txt similarity index 100% rename from nix/check-tools/nix-nixos-amd64.txt rename to nix/check-tools/nix-ubuntu-amd64.txt diff --git a/nix/check-tools/nix-nixos-arm64.txt b/nix/check-tools/nix-ubuntu-arm64.txt similarity index 100% rename from nix/check-tools/nix-nixos-arm64.txt rename to nix/check-tools/nix-ubuntu-arm64.txt diff --git a/nix/packages.nix b/nix/packages.nix index 3cf0f57c3e..01ab2ecf9a 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -16,7 +16,23 @@ let exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@" ''; - rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml; + # rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so + # cargo has a linker). That default may be different from the clang we pin here, + # so it shadows our clang and the build can silently use a different compiler + # version. Drop that cc from every propagation channel instead of pinning a + # replacement: the toolchain then carries no compiler and cargo just uses the + # active shell's stdenv cc. Must cover all channels — rust-overlay uses both + # propagatedBuildInputs and depsHostHostPropagated. + rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml; + rustToolchain = + let + defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv + withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath); + in + rustToolchainBase.overrideAttrs (old: { + propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]); + depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]); + }); # Nix wraps its toolchain so that binaries are exposed only under unsuffixed # names (gcc, g++, clang-tidy, ...). Several tools probe for a From a5cc339d7b8d097a0ae3792420225565e2525699 Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 24 Jul 2026 18:39:35 -0400 Subject: [PATCH 36/86] 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 37/86] 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 b878818e809455cb82dc055a473e066d309fa9bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:55:01 +0000 Subject: [PATCH 38/86] ci: [DEPENDABOT] bump actions/checkout from 7.0.0 to 7.0.1 (#7871) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/check-pr-description.yml | 2 +- .github/workflows/check-tools.yml | 4 ++-- .github/workflows/on-pr.yml | 2 +- .github/workflows/publish-docs.yml | 2 +- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/reusable-check-levelization.yml | 2 +- .github/workflows/reusable-check-rename.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 2 +- .github/workflows/reusable-package.yml | 4 ++-- .github/workflows/reusable-strategy-matrix.yml | 2 +- .github/workflows/reusable-upload-recipe.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/check-pr-description.yml b/.github/workflows/check-pr-description.yml index 744449f216..f8e7b6cdc4 100644 --- a/.github/workflows/check-pr-description.yml +++ b/.github/workflows/check-pr-description.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Write PR body to file env: diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index 6daaf98114..af20c5f17e 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -47,7 +47,7 @@ jobs: tag: ${{ steps.tag.outputs.tag }} steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Read nix image tag id: tag @@ -76,7 +76,7 @@ jobs: container: ${{ !matrix.nix_develop && format('ghcr.io/xrplf/xrpld/nix-ubuntu:{0}', needs.linux-image-tag.outputs.tag) || null }} steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 13c807ffca..1cd97305da 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -52,7 +52,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Determine changed files # This step checks whether any files have changed that should # cause the next jobs to run. We do it this way rather than diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 49f5c021a3..c1e67e2010 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -44,7 +44,7 @@ jobs: container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49 steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 6372bb6328..74425febe8 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -110,7 +110,7 @@ jobs: uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 diff --git a/.github/workflows/reusable-check-levelization.yml b/.github/workflows/reusable-check-levelization.yml index 88c95ac3ba..7f547f2ab6 100644 --- a/.github/workflows/reusable-check-levelization.yml +++ b/.github/workflows/reusable-check-levelization.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check levelization run: python .github/scripts/levelization/generate.py - name: Check for differences diff --git a/.github/workflows/reusable-check-rename.yml b/.github/workflows/reusable-check-rename.yml index 9a91e98ee3..874c8adcde 100644 --- a/.github/workflows/reusable-check-rename.yml +++ b/.github/workflows/reusable-check-rename.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check definitions run: .github/scripts/rename/definitions.sh . - name: Check copyright notices diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 90f24bc464..3c19b58a12 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -40,7 +40,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 55bc20dc5c..e1c11ac677 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -27,7 +27,7 @@ jobs: matrix: ${{ steps.generate.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download pre-built binary uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index de8d9cfc8e..12f11b0fbe 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -23,7 +23,7 @@ jobs: matrix: ${{ steps.generate.outputs.matrix }} steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index 0f00ce7ca0..bce4da2df6 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -47,7 +47,7 @@ jobs: CONAN_PASSWORD_XRPLF: ${{ secrets.remote_password }} steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Generate build version number id: version diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 8a02c4c2db..80a75a1fbf 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -65,7 +65,7 @@ jobs: uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 From 20801d98acd36f1aa4bc283ccc00d46db6d20f14 Mon Sep 17 00:00:00 2001 From: Andrzej Budzanowski Date: Mon, 27 Jul 2026 13:58:15 +0200 Subject: [PATCH 39/86] test: Improve the server status test to not race and randomly fail (#7304) Co-authored-by: Alex Kremer --- src/test/jtx/AbstractClient.h | 11 +++ src/test/jtx/Env.h | 70 ++++++++++++++++-- src/test/jtx/WSClient_test.cpp | 44 ++++++++++- src/test/jtx/impl/JSONRPCClient.cpp | 92 +++++++++++++++++++++-- src/test/jtx/impl/WSClient.cpp | 101 ++++++++++++++++++-------- src/test/server/ServerStatus_test.cpp | 48 ++++++++---- 6 files changed, 307 insertions(+), 59 deletions(-) diff --git a/src/test/jtx/AbstractClient.h b/src/test/jtx/AbstractClient.h index f9d8de0768..58f57f67a5 100644 --- a/src/test/jtx/AbstractClient.h +++ b/src/test/jtx/AbstractClient.h @@ -40,6 +40,17 @@ public: */ [[nodiscard]] virtual unsigned version() const = 0; + + /** + * Close the client's connection to the server. + * + * Releases the connection the client holds against the server's per-port + * connection limit. After this call the client must not be used to + * invoke() again. Tests use this to deterministically free the slot + * rather than waiting for the server's idle timeout to drop it. + */ + virtual void + disconnect() = 0; }; } // namespace xrpl::test diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 0df62c7e9b..a175fd5006 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -482,11 +482,52 @@ public: app().getNumberOfThreads() == 1, "syncClose() is only useful on an application with a single thread"); auto const result = close(); - auto serverBarrier = std::make_shared>(); - auto future = serverBarrier->get_future(); - boost::asio::post(app().getIOContext(), [serverBarrier]() { serverBarrier->set_value(); }); - auto const status = future.wait_for(timeout); - return result && status == std::future_status::ready; + return result && drainServerIo(timeout); + } + + /** + * Disconnect the Env's built-in client and wait for the server to + * register the dropped connection. + * + * Env holds one persistent client connection to the server's RPC port for + * its whole lifetime (see client()), and that connection counts against + * the port's connection limit. Tests that need a known starting occupancy + * can call this to deterministically release that slot instead of waiting + * out the server's localhost idle timeout. + * + * The server decrements its per-port connection count in the peer's + * destructor, which runs when the io_context processes the end-of-stream + * on the closed socket. After closing the client this drains the server's + * io_context twice: the first barrier guarantees the reactor has reaped + * the closed socket and queued the peer's teardown, and the second + * guarantees that teardown (and therefore the count decrement) has run. + * + * This is only sound when the server uses a single io_context thread, so + * that draining establishes ordering against the teardown - configure the + * Env with singleThreadIo() (as syncClose() also requires). Like + * syncClose(), it relies on loopback teardown latency being negligible. + * + * @param timeout Maximum time to wait for each barrier task to execute + * @return true if both barriers executed within timeout, false otherwise + */ + [[nodiscard]] bool + disconnectClient(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1}) + { + XRPL_ASSERT( + app().getNumberOfThreads() == 1, + "disconnectClient() is only useful on an application with a single " + "thread"); + + bundle_.client->disconnect(); + + // Drain the server's single io thread twice: the first barrier flushes + // the reactor's reap of the closed socket (queuing the peer teardown), + // the second flushes that teardown - and therefore the connection-count + // decrement. Both run unconditionally so a timed-out first drain does + // not short-circuit the second. + bool const reaped = drainServerIo(timeout); + bool const toreDown = drainServerIo(timeout); + return reaped && toreDown; } /** @@ -846,6 +887,25 @@ public: } private: + /** + * Drain the (single) server io_context thread once. + * + * Posts a barrier task to the server's io_context and blocks until it + * runs, so every task queued before it has been processed. Only meaningful + * with a single io thread (see syncClose()/disconnectClient()). + * + * @param timeout Maximum time to wait for the barrier task to execute + * @return true if the barrier ran within timeout, false otherwise + */ + [[nodiscard]] bool + drainServerIo(std::chrono::steady_clock::duration timeout) + { + auto barrier = std::make_shared>(); + auto future = barrier->get_future(); + boost::asio::post(app().getIOContext(), [barrier]() { barrier->set_value(); }); + return future.wait_for(timeout) == std::future_status::ready; + } + void fund(bool setDefaultRipple, STAmount const& amount, Account const& account); diff --git a/src/test/jtx/WSClient_test.cpp b/src/test/jtx/WSClient_test.cpp index d77e0f948b..801ca50504 100644 --- a/src/test/jtx/WSClient_test.cpp +++ b/src/test/jtx/WSClient_test.cpp @@ -13,8 +13,9 @@ class WSClient_test : public beast::unit_test::Suite { public: void - run() override + testSmoke() { + testcase("smoke"); using namespace jtx; Env env(*this); auto wsc = makeWSClient(env.app().config()); @@ -28,6 +29,47 @@ public: auto jv = wsc->getMsg(std::chrono::seconds(1)); pass(); } + + void + testGracefulDisconnect() + { + testcase("graceful disconnect"); + using namespace jtx; + using namespace std::chrono; + + Env env(*this); + auto wsc = makeWSClient(env.app().config()); + + // Put real traffic on the connection before closing it. + json::Value stream; + stream["streams"] = json::ValueType::Array; + stream["streams"].append("ledger"); + auto const sub = wsc->invoke("subscribe", stream); + BEAST_EXPECT(sub.isMember("result") || sub.isMember("status")); + + // disconnect() performs a graceful WebSocket closing handshake and + // blocks until the server acknowledges. On loopback that completes in + // well under its internal 1s timeout; only a broken async_close/ack + // coordination would fall through to the force-close path at ~1s. A + // generous bound keeps this from flaking under load while still + // catching that regression. + auto const start = steady_clock::now(); + wsc->disconnect(); + auto const elapsed = duration_cast(steady_clock::now() - start); + BEAST_EXPECT(elapsed < milliseconds{750}); + + // disconnect() must be idempotent: a second call (and the subsequent + // destructor) must not hang, double-close, or crash. + wsc->disconnect(); + pass(); + } + + void + run() override + { + testSmoke(); + testGracefulDisconnect(); + } }; BEAST_DEFINE_TESTSUITE(WSClient, jtx, xrpl); diff --git a/src/test/jtx/impl/JSONRPCClient.cpp b/src/test/jtx/impl/JSONRPCClient.cpp index 495fc5a657..06474c3616 100644 --- a/src/test/jtx/impl/JSONRPCClient.cpp +++ b/src/test/jtx/impl/JSONRPCClient.cpp @@ -14,18 +14,23 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include +#include +#include +#include #include #include #include @@ -84,6 +89,40 @@ class JSONRPCClient : public AbstractClient boost::beast::multi_buffer bout_; unsigned rpcVersion_; + bool disconnected_ = false; + + // Errors that mean the persistent keep-alive connection was dropped by the + // server (rather than a genuine protocol failure), so the request can be + // safely retried on a fresh connection. + static bool + droppedConnection(boost::system::error_code const& ec) + { + namespace error = boost::asio::error; + static auto const kDroppedConnectionErrors = std::to_array({ + boost::beast::http::error::end_of_stream, + error::eof, + error::connection_reset, + error::connection_aborted, + error::broken_pipe, + error::not_connected, + }); + + return std::ranges::any_of( + kDroppedConnectionErrors, + [&ec](boost::system::error_code const& e) { return ec == e; }); + } + + // Tear down and re-establish the socket to ep_, discarding any buffered + // bytes left over from the dropped connection. + void + reconnect() + { + boost::system::error_code ec; + stream_.close(ec); + bin_.clear(); + stream_.connect(ep_); + } + public: explicit JSONRPCClient(Config const& cfg, unsigned rpcVersion) : ep_(getEndpoint(cfg)), stream_(ios_), rpcVersion_(rpcVersion) @@ -91,12 +130,10 @@ public: stream_.connect(ep_); } - /* - Return value is an Object type with up to three keys: - status - error - result - */ + // Return value is an Object type with up to three keys: + // status + // error + // result json::Value invoke(std::string const& cmd, json::Value const& params) override { @@ -104,6 +141,13 @@ public: using namespace boost::asio; using namespace std::string_literals; + // Once disconnect() has released the slot, the client must not be + // reused (see AbstractClient::disconnect). Refuse rather than let the + // failed write/read below trip the reconnect path and silently + // re-consume a connection slot, which would defeat disconnectClient(). + if (disconnected_) + Throw("JSONRPCClient::invoke called after disconnect()"); + request req; req.method(boost::beast::http::verb::post); req.target("/"); @@ -131,10 +175,29 @@ public: req.body() = to_string(jr); } req.prepare_payload(); - write(stream_, req); + // The client keeps a single keep-alive connection for its whole + // lifetime, but the server drops idle localhost connections after a few + // seconds (BaseHTTPPeer::kTimeoutSecondsLocal). If a slow gap between + // requests let the server close the socket, the write/read here fails + // with end_of_stream; reconnect and retry the request exactly once. response res; - read(stream_, bin_, res); + auto writeAndRead = [&] { + write(stream_, req); + read(stream_, bin_, res); + }; + try + { + writeAndRead(); + } + catch (boost::system::system_error const& e) + { + if (!droppedConnection(e.code())) + throw; + reconnect(); + res = {}; + writeAndRead(); + } json::Reader jr; json::Value jv; @@ -151,6 +214,19 @@ public: { return rpcVersion_; } + + void + disconnect() override + { + if (disconnected_) + return; + + disconnected_ = true; + + boost::system::error_code ec; + stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); + stream_.close(ec); + } }; std::unique_ptr diff --git a/src/test/jtx/impl/WSClient.cpp b/src/test/jtx/impl/WSClient.cpp index ca322415fb..a8702c12d9 100644 --- a/src/test/jtx/impl/WSClient.cpp +++ b/src/test/jtx/impl/WSClient.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -112,10 +113,11 @@ class WSClientImpl : public WSClient bool peerClosed_ = false; - // synchronize destructor - bool b0_ = false; - std::mutex m0_; - std::condition_variable cv0_; + // disconnect() waits on this until the read loop ends (for any reason: + // the server acknowledged our close, or a timeout force-closed the socket). + static constexpr auto kDisconnectTimeout = std::chrono::seconds{1}; + xrpl::Mutex readEnded_; + std::condition_variable readEndCv_; // synchronize message queue std::mutex m_; @@ -127,23 +129,26 @@ class WSClientImpl : public WSClient void cleanup() { - boost::asio::post(ios_, boost::asio::bind_executor(strand_, [this] { - if (!peerClosed_) - { - ws_.async_close( - {}, boost::asio::bind_executor(strand_, [&](error_code) { - try - { - stream_.cancel(); - } - // NOLINTNEXTLINE(bugprone-empty-catch) - catch (boost::system::system_error const&) - { - // ignored - } - })); - } - })); + boost::asio::post( + ios_, // + boost::asio::bind_executor(strand_, [this] { + if (!peerClosed_) + { + ws_.async_close( + {}, // + boost::asio::bind_executor(strand_, [&](error_code) { + try + { + stream_.cancel(); + } + // NOLINTNEXTLINE(bugprone-empty-catch) + catch (boost::system::system_error const&) + { + // ignored + } + })); + } + })); work_ = std::nullopt; thread_.join(); } @@ -289,6 +294,44 @@ public: return rpcVersion_; } + void + disconnect() override + { + // Perform a graceful WebSocket closing handshake and block until the + // read loop ends, so the server observes a clean close (not a RST) and + // has finished tearing the connection down by the time we return. + // If the server already closed, the wait below returns immediately. + boost::asio::post( + ios_, + boost::asio::bind_executor( + strand_, // + [this] { + if (!peerClosed_) + { + ws_.async_close( + boost::beast::websocket::close_code::normal, + boost::asio::bind_executor(strand_, [](error_code) {})); + } + })); + + auto lock = readEnded_.lock(); + readEndCv_.wait_for(lock, kDisconnectTimeout, [&lock] { return *lock; }); + + // On timeout (server gone or not replying) force the socket closed so + // the outstanding read ends and the worker thread can later be joined. + if (!*lock) + { + boost::asio::post( + ios_, + boost::asio::bind_executor( + strand_, // + [this] { + boost::system::error_code ec; + stream_.close(ec); + })); + } + } + private: void onReadMsg(error_code const& ec) @@ -297,33 +340,31 @@ private: { if (ec == boost::beast::websocket::error::closed) peerClosed_ = true; + + *readEnded_.lock() = true; + readEndCv_.notify_all(); + return; } json::Value jv; json::Reader jr; + jr.parse(bufferString(rb_.data()), jv); rb_.consume(rb_.size()); + auto m = std::make_shared(std::move(jv)); { std::scoped_lock const lock(m_); msgs_.push_front(m); cv_.notify_all(); } + ws_.async_read( rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) { onReadMsg(ec); })); } - - // Called when the read op terminates - void - onReadDone() - { - std::scoped_lock const lock(m0_); - b0_ = true; - cv0_.notify_all(); - } }; std::unique_ptr diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp index 60ea622616..5adf6a08f5 100644 --- a/src/test/server/ServerStatus_test.cpp +++ b/src/test/server/ServerStatus_test.cpp @@ -558,10 +558,12 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En using namespace test::jtx; using namespace boost::asio; using namespace boost::beast::http; - Env env{*this, envconfig([&](std::unique_ptr cfg) { + // Run the server with a single io thread so disconnectClient() below + // can deterministically drain the server's io_context (see its docs). + Env env{*this, singleThreadIo(envconfig([&](std::unique_ptr cfg) { (*cfg)[Sections::kPortRpc].set(Keys::kLimit, std::to_string(limit)); return cfg; - })}; + }))}; auto const section = env.app().config().section(Sections::kPortRpc); // NOLINTBEGIN(bugprone-unchecked-optional-access) @@ -580,16 +582,27 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En BEAST_EXPECT(!ec); std::vector> clients; - int connectionCount{1}; // starts at 1 because the Env already has one - // for JSONRPCCLient - // for nonzero limits, go one past the limit, although failures happen - // at the limit, so this really leads to the last two clients failing. - // for zero limit, pick an arbitrary nonzero number of clients - all - // should connect fine. + // Env owns a persistent JSON-RPC HTTP client connection to port_rpc as + // part of startup, which counts against this port's connection limit. + // This test wants a known starting occupancy of zero, so for nonzero + // limits it deterministically drops that hidden client and waits for + // the server to register the disconnect before opening its own clients. + // + // Starting from zero is important because the port limit rejects once + // the incremented connection count reaches the configured limit. With a + // zero baseline and N = limit + 1 test-owned clients, exactly the last + // two requests should be rejected. + if (limit != 0) + BEAST_EXPECT(env.disconnectClient()); + + // For nonzero limits, go one past the limit. The port rejects at the + // limit, not only above it, so this yields the last two clients + // failing. For zero limit, pick an arbitrary nonzero number of clients + // and expect them all to succeed. int const testTo = (limit == 0) ? 50 : limit + 1; - while (connectionCount < testTo) + while (static_cast(clients.size()) < testTo) { clients.emplace_back(ip::tcp::socket{ios}, boost::beast::multi_buffer{}); async_connect(clients.back().first, it, yield[ec]); @@ -597,19 +610,24 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En auto req = makeHTTPRequest(ip, port, to_string(jr), {}); async_write(clients.back().first, req, yield[ec]); BEAST_EXPECT(!ec); - ++connectionCount; } - int readCount = 0; + int successfulReads = 0; for (auto& [soc, buf] : clients) { boost::beast::http::response resp; async_read(soc, buf, resp, yield[ec]); - ++readCount; - // expect the reads to fail for the clients that connected at or - // above the limit. If limit is 0, all reads should succeed - BEAST_EXPECT((limit == 0 || readCount < limit - 1) ? (!ec) : bool(ec)); + if (!ec) + ++successfulReads; } + + // This test cares about the exact number of accepted requests, not which + // specific client observed the rejection. With a zero baseline (the + // hidden Env client dropped above), the server accepts until the + // connection count reaches the limit: all clients for limit 0, else + // limit - 1 of the limit + 1 clients (the last two are rejected). + int const expectedReads = (limit == 0) ? static_cast(clients.size()) : limit - 1; + BEAST_EXPECT(successfulReads == expectedReads); } void From 29120dfcbd1a6903dc0bc129f57935433ccff717 Mon Sep 17 00:00:00 2001 From: Andrzej Budzanowski Date: Mon, 27 Jul 2026 15:00:14 +0200 Subject: [PATCH 40/86] test: Migrate `nodestore` tests from Beast to GTest (#7292) Co-authored-by: Marek Foss Co-authored-by: Alex Kremer --- .../scripts/levelization/results/ordering.txt | 3 - .github/scripts/rename/namespace.sh | 3 - include/xrpl/core/ServiceRegistry.h | 6 +- include/xrpl/nodestore/Backend.h | 4 +- include/xrpl/nodestore/Database.h | 6 +- include/xrpl/nodestore/DatabaseRotating.h | 6 +- include/xrpl/nodestore/DummyScheduler.h | 4 +- include/xrpl/nodestore/Factory.h | 4 +- include/xrpl/nodestore/Manager.h | 4 +- include/xrpl/nodestore/NodeObject.h | 2 +- include/xrpl/nodestore/Scheduler.h | 4 +- include/xrpl/nodestore/Task.h | 4 +- include/xrpl/nodestore/Types.h | 4 +- include/xrpl/nodestore/detail/BatchWriter.h | 4 +- .../xrpl/nodestore/detail/DatabaseNodeImp.h | 6 +- .../nodestore/detail/DatabaseRotatingImp.h | 6 +- include/xrpl/nodestore/detail/DecodedBlob.h | 4 +- include/xrpl/nodestore/detail/EncodedBlob.h | 8 +- include/xrpl/nodestore/detail/ManagerImp.h | 4 +- include/xrpl/nodestore/detail/codec.h | 6 +- include/xrpl/nodestore/detail/varint.h | 4 +- include/xrpl/shamap/Family.h | 4 +- src/benchmarks/libxrpl/nodestore/Backend.cpp | 4 +- src/benchmarks/libxrpl/nodestore/Database.cpp | 4 +- .../libxrpl/nodestore/NodeStoreBench.h | 4 +- src/libxrpl/nodestore/BatchWriter.cpp | 6 +- src/libxrpl/nodestore/Database.cpp | 14 +- src/libxrpl/nodestore/DatabaseNodeImp.cpp | 4 +- src/libxrpl/nodestore/DatabaseRotatingImp.cpp | 8 +- src/libxrpl/nodestore/DecodedBlob.cpp | 6 +- src/libxrpl/nodestore/DummyScheduler.cpp | 4 +- src/libxrpl/nodestore/ManagerImp.cpp | 6 +- .../nodestore/backend/MemoryFactory.cpp | 10 +- src/libxrpl/nodestore/backend/NuDBFactory.cpp | 6 +- src/libxrpl/nodestore/backend/NullFactory.cpp | 4 +- .../nodestore/backend/RocksDBFactory.cpp | 12 +- src/test/app/SHAMapStore_test.cpp | 6 +- src/test/nodestore/Backend_test.cpp | 110 ----- src/test/nodestore/Basics_test.cpp | 73 --- ...abase_test.cpp => DatabaseConfig_test.cpp} | 316 ++----------- src/test/nodestore/NuDBFactory_test.cpp | 443 ------------------ src/test/nodestore/TestBase.h | 202 -------- src/test/nodestore/import_test.cpp | 4 +- src/test/nodestore/varint_test.cpp | 57 --- src/tests/libxrpl/CMakeLists.txt | 1 + src/tests/libxrpl/helpers/CaptureSink.h | 50 ++ src/tests/libxrpl/helpers/TestFamily.h | 10 +- .../libxrpl/helpers/TestServiceRegistry.h | 2 +- src/tests/libxrpl/nodestore/Backend.cpp | 182 +++++++ src/tests/libxrpl/nodestore/Basics.cpp | 41 ++ src/tests/libxrpl/nodestore/Database.cpp | 248 ++++++++++ src/tests/libxrpl/nodestore/NuDBFactory.cpp | 297 ++++++++++++ src/tests/libxrpl/nodestore/TestBase.h | 169 +++++++ src/tests/libxrpl/nodestore/varint.cpp | 46 ++ src/tests/libxrpl/shamap/common.h | 10 +- src/xrpld/app/ledger/AccountStateSF.h | 4 +- src/xrpld/app/ledger/InboundLedger.h | 2 +- src/xrpld/app/ledger/TransactionStateSF.h | 4 +- src/xrpld/app/ledger/detail/InboundLedger.cpp | 2 +- src/xrpld/app/ledger/detail/LedgerMaster.cpp | 2 +- src/xrpld/app/main/Application.cpp | 10 +- src/xrpld/app/main/NodeStoreScheduler.cpp | 8 +- src/xrpld/app/main/NodeStoreScheduler.h | 10 +- src/xrpld/app/misc/SHAMapStore.h | 4 +- src/xrpld/app/misc/SHAMapStoreImp.cpp | 22 +- src/xrpld/app/misc/SHAMapStoreImp.h | 12 +- src/xrpld/shamap/NodeFamily.h | 6 +- 67 files changed, 1235 insertions(+), 1310 deletions(-) delete mode 100644 src/test/nodestore/Backend_test.cpp delete mode 100644 src/test/nodestore/Basics_test.cpp rename src/test/nodestore/{Database_test.cpp => DatabaseConfig_test.cpp} (59%) delete mode 100644 src/test/nodestore/NuDBFactory_test.cpp delete mode 100644 src/test/nodestore/TestBase.h delete mode 100644 src/test/nodestore/varint_test.cpp create mode 100644 src/tests/libxrpl/helpers/CaptureSink.h create mode 100644 src/tests/libxrpl/nodestore/Backend.cpp create mode 100644 src/tests/libxrpl/nodestore/Basics.cpp create mode 100644 src/tests/libxrpl/nodestore/Database.cpp create mode 100644 src/tests/libxrpl/nodestore/NuDBFactory.cpp create mode 100644 src/tests/libxrpl/nodestore/TestBase.h create mode 100644 src/tests/libxrpl/nodestore/varint.cpp diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index 709ba4d6d4..5577c363fd 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -130,12 +130,9 @@ test.ledger > xrpl.json test.ledger > xrpl.ledger test.ledger > xrpl.protocol test.nodestore > test.jtx -test.nodestore > test.unit_test test.nodestore > xrpl.basics -test.nodestore > xrpl.config test.nodestore > xrpld.core test.nodestore > xrpl.nodestore -test.nodestore > xrpl.protocol test.nodestore > xrpl.rdb test.overlay > test.jtx test.overlay > test.unit_test diff --git a/.github/scripts/rename/namespace.sh b/.github/scripts/rename/namespace.sh index bb186bc8bc..94a9205f76 100755 --- a/.github/scripts/rename/namespace.sh +++ b/.github/scripts/rename/namespace.sh @@ -46,9 +46,6 @@ for DIRECTORY in "${DIRECTORIES[@]}"; do done done -# Special case for NuDBFactory that has ripple twice in the test suite name. -${SED_COMMAND} -i -E 's/(BEAST_DEFINE_TESTSUITE.+)ripple(.+)/\1xrpl\2/g' src/test/nodestore/NuDBFactory_test.cpp - DIRECTORY=$1 find "${DIRECTORY}" -type f -name "*.md" | while read -r FILE; do echo "Processing file: ${FILE}" diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h index 592964134b..2747ecd9e8 100644 --- a/include/xrpl/core/ServiceRegistry.h +++ b/include/xrpl/core/ServiceRegistry.h @@ -15,9 +15,9 @@ namespace xrpl { // Forward declarations -namespace NodeStore { +namespace node_store { class Database; -} // namespace NodeStore +} // namespace node_store namespace Resource { class Manager; } // namespace Resource @@ -164,7 +164,7 @@ public: getResourceManager() = 0; // Storage services - virtual NodeStore::Database& + virtual node_store::Database& getNodeStore() = 0; virtual SHAMapStore& diff --git a/include/xrpl/nodestore/Backend.h b/include/xrpl/nodestore/Backend.h index 564a874c5e..85d076bcfb 100644 --- a/include/xrpl/nodestore/Backend.h +++ b/include/xrpl/nodestore/Backend.h @@ -13,7 +13,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * A backend used for the NodeStore. @@ -163,4 +163,4 @@ public: fdRequired() const = 0; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/Database.h b/include/xrpl/nodestore/Database.h index 96ba91bd76..902cfc9e03 100644 --- a/include/xrpl/nodestore/Database.h +++ b/include/xrpl/nodestore/Database.h @@ -25,7 +25,7 @@ namespace xrpl { class Section; } // namespace xrpl -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Persistency layer for NodeObject @@ -248,7 +248,7 @@ protected: void storeStats(std::uint64_t count, std::uint64_t sz) { - XRPL_ASSERT(count <= sz, "xrpl::NodeStore::Database::storeStats : valid inputs"); + XRPL_ASSERT(count <= sz, "xrpl::node_store::Database::storeStats : valid inputs"); storeCount_ += count; storeSz_ += sz; } @@ -308,4 +308,4 @@ private: threadEntry(); }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/DatabaseRotating.h b/include/xrpl/nodestore/DatabaseRotating.h index 5381b5c435..21b8b422c7 100644 --- a/include/xrpl/nodestore/DatabaseRotating.h +++ b/include/xrpl/nodestore/DatabaseRotating.h @@ -9,7 +9,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /* This class has two key-value store Backend objects for persisting SHAMap * records. This facilitates online deletion of data. New backends are @@ -38,7 +38,7 @@ public: */ virtual void rotate( - std::unique_ptr&& newBackend, + std::unique_ptr&& newBackend, std::function const& f) = 0; @@ -56,4 +56,4 @@ public: setRotationInFlight(bool inFlight) = 0; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/DummyScheduler.h b/include/xrpl/nodestore/DummyScheduler.h index 49b0d37462..fc7a040b5a 100644 --- a/include/xrpl/nodestore/DummyScheduler.h +++ b/include/xrpl/nodestore/DummyScheduler.h @@ -3,7 +3,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Simple NodeStore Scheduler that just performs the tasks synchronously. @@ -21,4 +21,4 @@ public: onBatchWrite(BatchWriteReport const& report) override; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/Factory.h b/include/xrpl/nodestore/Factory.h index a18023a8a8..568e9b0712 100644 --- a/include/xrpl/nodestore/Factory.h +++ b/include/xrpl/nodestore/Factory.h @@ -14,7 +14,7 @@ namespace xrpl { class Section; } // namespace xrpl -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Base class for backend factories. @@ -70,4 +70,4 @@ public: } }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/Manager.h b/include/xrpl/nodestore/Manager.h index 54d99fe94b..1b869e4bca 100644 --- a/include/xrpl/nodestore/Manager.h +++ b/include/xrpl/nodestore/Manager.h @@ -10,7 +10,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Singleton for managing NodeStore factories and back ends. @@ -98,4 +98,4 @@ public: beast::Journal journal) = 0; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/NodeObject.h b/include/xrpl/nodestore/NodeObject.h index b96d65fa12..db139b9aa8 100644 --- a/include/xrpl/nodestore/NodeObject.h +++ b/include/xrpl/nodestore/NodeObject.h @@ -8,7 +8,7 @@ #include #include -// VFALCO NOTE Intentionally not in the NodeStore namespace +// VFALCO NOTE Intentionally not in the node_store namespace namespace xrpl { diff --git a/include/xrpl/nodestore/Scheduler.h b/include/xrpl/nodestore/Scheduler.h index 5d93a80eaa..40c36ce8ab 100644 --- a/include/xrpl/nodestore/Scheduler.h +++ b/include/xrpl/nodestore/Scheduler.h @@ -4,7 +4,7 @@ #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { enum class FetchType { Synchronous, Async }; @@ -71,4 +71,4 @@ public: onBatchWrite(BatchWriteReport const& report) = 0; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/Task.h b/include/xrpl/nodestore/Task.h index 59fe648476..22cec62eae 100644 --- a/include/xrpl/nodestore/Task.h +++ b/include/xrpl/nodestore/Task.h @@ -1,6 +1,6 @@ #pragma once -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Derived classes perform scheduled tasks. @@ -17,4 +17,4 @@ struct Task performScheduledTask() = 0; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/Types.h b/include/xrpl/nodestore/Types.h index 872d948a36..21af6fa68b 100644 --- a/include/xrpl/nodestore/Types.h +++ b/include/xrpl/nodestore/Types.h @@ -5,7 +5,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { // This is only used to pre-allocate the array for // batch objects and does not affect the amount written. @@ -36,4 +36,4 @@ enum class Status { */ using Batch = std::vector>; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/BatchWriter.h b/include/xrpl/nodestore/detail/BatchWriter.h index b89df0da14..6ac3428752 100644 --- a/include/xrpl/nodestore/detail/BatchWriter.h +++ b/include/xrpl/nodestore/detail/BatchWriter.h @@ -9,7 +9,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Batch-writing assist logic. @@ -86,4 +86,4 @@ private: Batch writeSet_; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/DatabaseNodeImp.h b/include/xrpl/nodestore/detail/DatabaseNodeImp.h index 6f2fca682f..33a2e27939 100644 --- a/include/xrpl/nodestore/detail/DatabaseNodeImp.h +++ b/include/xrpl/nodestore/detail/DatabaseNodeImp.h @@ -22,7 +22,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { class DatabaseNodeImp : public Database { @@ -68,7 +68,7 @@ public: XRPL_ASSERT( backend_, - "xrpl::NodeStore::DatabaseNodeImp::DatabaseNodeImp : non-null " + "xrpl::node_store::DatabaseNodeImp::DatabaseNodeImp : non-null " "backend"); } @@ -138,4 +138,4 @@ private: } }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h index ecbe9a513d..9b566195ef 100644 --- a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h +++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h @@ -16,7 +16,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { class DatabaseRotatingImp : public DatabaseRotating { @@ -41,7 +41,7 @@ public: void rotate( - std::unique_ptr&& newBackend, + std::unique_ptr&& newBackend, std::function const& f) override; @@ -94,4 +94,4 @@ private: forEach(std::function)> f) override; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h b/include/xrpl/nodestore/detail/DecodedBlob.h index d0cc5e3404..bd90ff2f1b 100644 --- a/include/xrpl/nodestore/detail/DecodedBlob.h +++ b/include/xrpl/nodestore/detail/DecodedBlob.h @@ -4,7 +4,7 @@ #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Parsed key/value blob into NodeObject components. @@ -49,4 +49,4 @@ private: int dataBytes_; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h b/include/xrpl/nodestore/detail/EncodedBlob.h index d668cdccd8..d171df3cc9 100644 --- a/include/xrpl/nodestore/detail/EncodedBlob.h +++ b/include/xrpl/nodestore/detail/EncodedBlob.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { /** * Convert a NodeObject from in-memory to database format. @@ -68,7 +68,7 @@ class EncodedBlob public: explicit EncodedBlob(std::shared_ptr const& obj) : size_([&obj]() { - XRPL_ASSERT(obj, "xrpl::NodeStore::EncodedBlob::EncodedBlob : non-null input"); + XRPL_ASSERT(obj, "xrpl::node_store::EncodedBlob::EncodedBlob : non-null input"); if (!obj) throw std::runtime_error("EncodedBlob: unseated std::shared_ptr used."); @@ -88,7 +88,7 @@ public: XRPL_ASSERT( ((ptr_ == payload_.data()) && (size_ <= payload_.size())) || ((ptr_ != payload_.data()) && (size_ > payload_.size())), - "xrpl::NodeStore::EncodedBlob::~EncodedBlob : valid payload " + "xrpl::node_store::EncodedBlob::~EncodedBlob : valid payload " "pointer"); if (ptr_ != payload_.data()) @@ -114,4 +114,4 @@ public: } }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/ManagerImp.h b/include/xrpl/nodestore/detail/ManagerImp.h index fc84b0aa57..f1653b45dc 100644 --- a/include/xrpl/nodestore/detail/ManagerImp.h +++ b/include/xrpl/nodestore/detail/ManagerImp.h @@ -13,7 +13,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { class ManagerImp : public Manager { @@ -57,4 +57,4 @@ public: beast::Journal journal) override; }; -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h index 2f69d532be..a3dfa7c944 100644 --- a/include/xrpl/nodestore/detail/codec.h +++ b/include/xrpl/nodestore/detail/codec.h @@ -21,7 +21,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { template std::pair @@ -269,7 +269,7 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) case 1: // lz4 { std::uint8_t* p = nullptr; - auto const lzr = NodeStore::lz4Compress(in, inSize, [&p, &vn, &bf](std::size_t n) { + auto const lzr = node_store::lz4Compress(in, inSize, [&p, &vn, &bf](std::size_t n) { p = reinterpret_cast(bf(vn + n)); return p + vn; }); @@ -316,4 +316,4 @@ filterInner(void* in, std::size_t inSize) } } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/varint.h index 5a65545d3a..afbf71cdea 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/varint.h @@ -6,7 +6,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { // This is a variant of the base128 varint format from // google protocol buffers: @@ -123,4 +123,4 @@ write(nudb::detail::ostream& os, std::size_t t) writeVarint(os.data(sizeVarint(t)), t); } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/include/xrpl/shamap/Family.h b/include/xrpl/shamap/Family.h index 7624b3e600..467d41e4cc 100644 --- a/include/xrpl/shamap/Family.h +++ b/include/xrpl/shamap/Family.h @@ -26,10 +26,10 @@ public: explicit Family() = default; virtual ~Family() = default; - virtual NodeStore::Database& + virtual node_store::Database& db() = 0; - [[nodiscard]] virtual NodeStore::Database const& + [[nodiscard]] virtual node_store::Database const& db() const = 0; virtual beast::Journal const& diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp index 7db0185053..f854dbd3a7 100644 --- a/src/benchmarks/libxrpl/nodestore/Backend.cpp +++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp @@ -17,7 +17,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { namespace { constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000}; @@ -326,4 +326,4 @@ registerStoreBatch(BackendConfig const& bc) }(); } // namespace -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/benchmarks/libxrpl/nodestore/Database.cpp b/src/benchmarks/libxrpl/nodestore/Database.cpp index 2303075ab9..cd4337b603 100644 --- a/src/benchmarks/libxrpl/nodestore/Database.cpp +++ b/src/benchmarks/libxrpl/nodestore/Database.cpp @@ -18,7 +18,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { namespace { // Number of distinct objects pre-generated per run. @@ -240,4 +240,4 @@ registerWorkload(BackendConfig const& bc, Workload const& w) }(); } // namespace -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h index fe6c2a350e..57abf42e89 100644 --- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -32,7 +32,7 @@ // Shared helpers for the NodeStore benchmarks. // -namespace xrpl::NodeStore { +namespace xrpl::node_store { // Fill `bytes` of memory at `buffer` with random bits drawn from `g`. template @@ -315,4 +315,4 @@ backendConfigs() return kConfigs; } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/BatchWriter.cpp b/src/libxrpl/nodestore/BatchWriter.cpp index e0a1fbf20c..92dfa09e0c 100644 --- a/src/libxrpl/nodestore/BatchWriter.cpp +++ b/src/libxrpl/nodestore/BatchWriter.cpp @@ -11,7 +11,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { BatchWriter::BatchWriter(Callback& callback, Scheduler& scheduler) : callback_(callback), scheduler_(scheduler) @@ -72,7 +72,7 @@ BatchWriter::writeBatch() writeSet_.swap(set); XRPL_ASSERT( - writeSet_.empty(), "xrpl::NodeStore::BatchWriter::writeBatch : writes not set"); + writeSet_.empty(), "xrpl::node_store::BatchWriter::writeBatch : writes not set"); writeLoad_ = set.size(); if (set.empty()) @@ -107,4 +107,4 @@ BatchWriter::waitForWriting() writeCondition_.wait(sl); } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/Database.cpp b/src/libxrpl/nodestore/Database.cpp index ac51dbfb2c..f9de660042 100644 --- a/src/libxrpl/nodestore/Database.cpp +++ b/src/libxrpl/nodestore/Database.cpp @@ -30,7 +30,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { Database::Database( Scheduler& scheduler, @@ -43,7 +43,7 @@ Database::Database( , requestBundle_(get(config, Keys::kRqBundle, 4)) , readThreads_(std::max(1, readThreads)) { - XRPL_ASSERT(readThreads, "xrpl::NodeStore::Database::Database : nonzero threads input"); + XRPL_ASSERT(readThreads, "xrpl::node_store::Database::Database : nonzero threads input"); if (earliestLedgerSeq_ < 1) Throw("Invalid earliest_seq"); @@ -89,7 +89,7 @@ Database::Database( { XRPL_ASSERT( !it->second.empty(), - "xrpl::NodeStore::Database::Database : non-empty " + "xrpl::node_store::Database::Database : non-empty " "data"); auto const& hash = it->first; @@ -164,7 +164,7 @@ Database::stop() { XRPL_ASSERT( steady_clock::now() - start < 30s, - "xrpl::NodeStore::Database::stop : maximum stop duration"); + "xrpl::node_store::Database::stop : maximum stop duration"); std::this_thread::yield(); } @@ -213,7 +213,7 @@ Database::importInternal(Backend& dstBackend, Database& srcDB) }; srcDB.forEach([&](std::shared_ptr nodeObject) { - XRPL_ASSERT(nodeObject, "xrpl::NodeStore::Database::importInternal : non-null node"); + XRPL_ASSERT(nodeObject, "xrpl::node_store::Database::importInternal : non-null node"); if (!nodeObject) // This should never happen return; @@ -257,7 +257,7 @@ Database::fetchNodeObject( void Database::getCountsJson(json::Value& obj) { - XRPL_ASSERT(obj.isObject(), "xrpl::NodeStore::Database::getCountsJson : valid input type"); + XRPL_ASSERT(obj.isObject(), "xrpl::node_store::Database::getCountsJson : valid input type"); { std::unique_lock const lock(readLock_); @@ -276,4 +276,4 @@ Database::getCountsJson(json::Value& obj) obj[jss::node_reads_duration_us] = std::to_string(fetchDurationUs_); } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/DatabaseNodeImp.cpp b/src/libxrpl/nodestore/DatabaseNodeImp.cpp index 9323d69131..1b880ac658 100644 --- a/src/libxrpl/nodestore/DatabaseNodeImp.cpp +++ b/src/libxrpl/nodestore/DatabaseNodeImp.cpp @@ -15,7 +15,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { void DatabaseNodeImp::store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t) @@ -125,4 +125,4 @@ DatabaseNodeImp::fetchNodeObject( return nodeObject; } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp index 23a48a3bf3..81b1d6b297 100644 --- a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp +++ b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp @@ -22,7 +22,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { DatabaseRotatingImp::DatabaseRotatingImp( Scheduler& scheduler, @@ -43,7 +43,7 @@ DatabaseRotatingImp::DatabaseRotatingImp( void DatabaseRotatingImp::rotate( - std::unique_ptr&& newBackend, + std::unique_ptr&& newBackend, std::function const& f) { // Pass these two names to the callback function @@ -52,7 +52,7 @@ DatabaseRotatingImp::rotate( // Hold on to current archive backend pointer until after the // callback finishes. Only then will the archive directory be // deleted. - std::shared_ptr oldArchiveBackend; + std::shared_ptr oldArchiveBackend; std::uint64_t copyForwards = 0; { std::scoped_lock const lock(mutex_); @@ -232,4 +232,4 @@ DatabaseRotatingImp::forEach(std::function)> f) archive->forEach(f); } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/DecodedBlob.cpp b/src/libxrpl/nodestore/DecodedBlob.cpp index 9740462ae8..321089d40f 100644 --- a/src/libxrpl/nodestore/DecodedBlob.cpp +++ b/src/libxrpl/nodestore/DecodedBlob.cpp @@ -10,7 +10,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) : key_(key) { @@ -55,7 +55,7 @@ DecodedBlob::DecodedBlob(void const* key, void const* value, int valueBytes) : k std::shared_ptr DecodedBlob::createObject() { - XRPL_ASSERT(success_, "xrpl::NodeStore::DecodedBlob::createObject : valid object type"); + XRPL_ASSERT(success_, "xrpl::node_store::DecodedBlob::createObject : valid object type"); std::shared_ptr object; @@ -69,4 +69,4 @@ DecodedBlob::createObject() return object; } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/DummyScheduler.cpp b/src/libxrpl/nodestore/DummyScheduler.cpp index 1f93ed3d0f..32cd14cbdc 100644 --- a/src/libxrpl/nodestore/DummyScheduler.cpp +++ b/src/libxrpl/nodestore/DummyScheduler.cpp @@ -3,7 +3,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { void DummyScheduler::scheduleTask(Task& task) @@ -22,4 +22,4 @@ DummyScheduler::onBatchWrite(BatchWriteReport const& report) { } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/ManagerImp.cpp b/src/libxrpl/nodestore/ManagerImp.cpp index a3db22ce74..c78ccd5761 100644 --- a/src/libxrpl/nodestore/ManagerImp.cpp +++ b/src/libxrpl/nodestore/ManagerImp.cpp @@ -22,7 +22,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { ManagerImp& ManagerImp::instance() @@ -112,7 +112,7 @@ ManagerImp::erase(Factory& factory) std::scoped_lock const _(mutex_); auto const iter = std::ranges::find_if(list_, [&factory](Factory* other) { return other == &factory; }); - XRPL_ASSERT(iter != list_.end(), "xrpl::NodeStore::ManagerImp::erase : valid input"); + XRPL_ASSERT(iter != list_.end(), "xrpl::node_store::ManagerImp::erase : valid input"); list_.erase(iter); } @@ -135,4 +135,4 @@ Manager::instance() return ManagerImp::instance(); } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp b/src/libxrpl/nodestore/backend/MemoryFactory.cpp index 22557d652e..39d2123bc9 100644 --- a/src/libxrpl/nodestore/backend/MemoryFactory.cpp +++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp @@ -24,7 +24,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { struct MemoryDB { @@ -132,7 +132,7 @@ public: Status fetch(uint256 const& hash, std::shared_ptr* pObject) override { - XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::fetch : non-null database"); + XRPL_ASSERT(db_, "xrpl::node_store::MemoryBackend::fetch : non-null database"); std::scoped_lock const _(db_->mutex); @@ -149,7 +149,7 @@ public: void store(std::shared_ptr const& object) override { - XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::store : non-null database"); + XRPL_ASSERT(db_, "xrpl::node_store::MemoryBackend::store : non-null database"); std::scoped_lock const _(db_->mutex); db_->table.emplace(object->getHash(), object); } @@ -169,7 +169,7 @@ public: void forEach(std::function)> f) override { - XRPL_ASSERT(db_, "xrpl::NodeStore::MemoryBackend::forEach : non-null database"); + XRPL_ASSERT(db_, "xrpl::node_store::MemoryBackend::forEach : non-null database"); for (auto const& e : db_->table) f(e.second); } @@ -216,4 +216,4 @@ MemoryFactory::createInstance( return std::make_unique(keyBytes, keyValues, journal); } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp index 38ea34258f..bbf37f3edf 100644 --- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp @@ -44,7 +44,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { class NuDBBackend : public Backend { @@ -136,7 +136,7 @@ public: { // LCOV_EXCL_START UNREACHABLE( - "xrpl::NodeStore::NuDBBackend::open : database is already " + "xrpl::node_store::NuDBBackend::open : database is already " "open"); JLOG(j.error()) << "database is already open"; return; @@ -441,4 +441,4 @@ registerNuDBFactory(Manager& manager) static NuDBFactory const kInstance{manager}; } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/backend/NullFactory.cpp b/src/libxrpl/nodestore/backend/NullFactory.cpp index 0c76cb9938..feef3d37d0 100644 --- a/src/libxrpl/nodestore/backend/NullFactory.cpp +++ b/src/libxrpl/nodestore/backend/NullFactory.cpp @@ -13,7 +13,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { class NullBackend : public Backend { @@ -125,4 +125,4 @@ registerNullFactory(Manager& manager) static NullFactory const kInstance{manager}; } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp index 673b0daae0..4b7a1171fe 100644 --- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp @@ -42,7 +42,7 @@ #include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { class RocksDBEnv : public rocksdb::EnvWrapper { @@ -231,7 +231,7 @@ public: { // LCOV_EXCL_START UNREACHABLE( - "xrpl::NodeStore::RocksDBBackend::open : database is already " + "xrpl::node_store::RocksDBBackend::open : database is already " "open"); JLOG(journal.error()) << "database is already open"; return; @@ -279,7 +279,7 @@ public: Status fetch(uint256 const& hash, std::shared_ptr* pObject) override { - XRPL_ASSERT(db, "xrpl::NodeStore::RocksDBBackend::fetch : non-null database"); + XRPL_ASSERT(db, "xrpl::node_store::RocksDBBackend::fetch : non-null database"); pObject->reset(); Status status = Status::Ok; @@ -339,7 +339,7 @@ public: { XRPL_ASSERT( db, - "xrpl::NodeStore::RocksDBBackend::storeBatch : non-null " + "xrpl::node_store::RocksDBBackend::storeBatch : non-null " "database"); rocksdb::WriteBatch wb; @@ -369,7 +369,7 @@ public: void forEach(std::function)> f) override { - XRPL_ASSERT(db, "xrpl::NodeStore::RocksDBBackend::forEach : non-null database"); + XRPL_ASSERT(db, "xrpl::node_store::RocksDBBackend::forEach : non-null database"); rocksdb::ReadOptions const options; std::unique_ptr it(db->NewIterator(options)); @@ -468,6 +468,6 @@ registerRocksDBFactory(Manager& manager) static RocksDBFactory const kInstance{manager}; } -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store #endif diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index c219bd8737..537ee4c177 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -489,7 +489,7 @@ public: lastRotated = ledgerSeq - 1; } - std::unique_ptr + std::unique_ptr makeBackendRotating(jtx::Env& env, NodeStoreScheduler& scheduler, std::string path) { Section section{env.app().config().section(Sections::kNodeDatabase)}; @@ -500,7 +500,7 @@ public: newPath = path; section.set(Keys::kPath, newPath.string()); - auto backend{NodeStore::Manager::instance().makeBackend( + auto backend{node_store::Manager::instance().makeBackend( section, megabytes(env.app().config().getValueFor(SizedItem::BurstSize, std::nullopt)), scheduler, @@ -549,7 +549,7 @@ public: auto archiveBackend = makeBackendRotating(env, scheduler, archiveDb); static constexpr int kReadThreads = 4; - auto dbr = std::make_unique( + auto dbr = std::make_unique( scheduler, kReadThreads, std::move(writableBackend), diff --git a/src/test/nodestore/Backend_test.cpp b/src/test/nodestore/Backend_test.cpp deleted file mode 100644 index 65601b0cf5..0000000000 --- a/src/test/nodestore/Backend_test.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace xrpl::NodeStore { - -// Tests the Backend interface -// -class Backend_test : public TestBase -{ -public: - void - testBackend(std::string const& type, std::uint64_t const seedValue, int numObjsToTest = 2000) - { - DummyScheduler scheduler; - - testcase("Backend type=" + type); - - Section params; - beast::TempDir const tempDir; - params.set(Keys::kType, type); - params.set(Keys::kPath, tempDir.path()); - - beast::xor_shift_engine rng(seedValue); - - // Create a batch - auto batch = createPredictableBatch(numObjsToTest, rng()); - - using beast::Severity; - test::SuiteJournal journal("Backend_test", *this); - - { - // Open the backend - std::unique_ptr backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - backend->open(); - - // Write the batch - storeBatch(*backend, batch); - - { - // Read it back in - Batch copy; - fetchCopyOfBatch(*backend, ©, batch); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - - { - // Reorder and read the copy again - std::shuffle(batch.begin(), batch.end(), rng); - Batch copy; - fetchCopyOfBatch(*backend, ©, batch); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - } - - { - // Re-open the backend - std::unique_ptr backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - backend->open(); - - // Read it back in - Batch copy; - fetchCopyOfBatch(*backend, ©, batch); - // Canonicalize the source and destination batches - std::ranges::sort(batch, LessThan{}); - std::ranges::sort(copy, LessThan{}); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - } - - //-------------------------------------------------------------------------- - - void - run() override - { - std::uint64_t const seedValue = 50; - - testBackend("nudb", seedValue); - -#if XRPL_ROCKSDB_AVAILABLE - testBackend("rocksdb", seedValue); -#endif - -#ifdef XRPL_ENABLE_SQLITE_BACKEND_TESTS - testBackend("sqlite", seedValue); -#endif - } -}; - -BEAST_DEFINE_TESTSUITE(Backend, nodestore, xrpl); - -} // namespace xrpl::NodeStore diff --git a/src/test/nodestore/Basics_test.cpp b/src/test/nodestore/Basics_test.cpp deleted file mode 100644 index 7d77b18630..0000000000 --- a/src/test/nodestore/Basics_test.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include - -#include -#include -#include -#include - -#include -#include - -namespace xrpl::NodeStore { - -// Tests predictable batches, and NodeObject blob encoding -// -class NodeStoreBasic_test : public TestBase -{ -public: - // Make sure predictable object generation works! - void - testBatches(std::uint64_t const seedValue) - { - testcase("batch"); - - auto batch1 = createPredictableBatch(kNumObjectsToTest, seedValue); - - auto batch2 = createPredictableBatch(kNumObjectsToTest, seedValue); - - BEAST_EXPECT(areBatchesEqual(batch1, batch2)); - - auto batch3 = createPredictableBatch(kNumObjectsToTest, seedValue + 1); - - BEAST_EXPECT(!areBatchesEqual(batch1, batch3)); - } - - // Checks encoding/decoding blobs - void - testBlobs(std::uint64_t const seedValue) - { - testcase("encoding"); - - auto batch = createPredictableBatch(kNumObjectsToTest, seedValue); - - for (auto const& expected : batch) - { - EncodedBlob const encoded(expected); - - DecodedBlob decoded(encoded.getKey(), encoded.getData(), encoded.getSize()); - - BEAST_EXPECT(decoded.wasOk()); - - if (decoded.wasOk()) - { - std::shared_ptr const object(decoded.createObject()); - - BEAST_EXPECT(isSame(expected, object)); - } - } - } - - void - run() override - { - std::uint64_t const seedValue = 50; - - testBatches(seedValue); - - testBlobs(seedValue); - } -}; - -BEAST_DEFINE_TESTSUITE(NodeStoreBasic, nodestore, xrpl); - -} // namespace xrpl::NodeStore diff --git a/src/test/nodestore/Database_test.cpp b/src/test/nodestore/DatabaseConfig_test.cpp similarity index 59% rename from src/test/nodestore/Database_test.cpp rename to src/test/nodestore/DatabaseConfig_test.cpp index bb8ec7d4fd..1f7f0f67bd 100644 --- a/src/test/nodestore/Database_test.cpp +++ b/src/test/nodestore/DatabaseConfig_test.cpp @@ -1,44 +1,21 @@ #include #include #include -#include -#include #include -#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include #include -#include -#include #include -namespace xrpl::NodeStore { +namespace xrpl::node_store { -class Database_test : public TestBase +class DatabaseConfig_test : public beast::unit_test::Suite { - test::SuiteJournal journal_; - public: - Database_test() : journal_("Database_test", *this) - { - } - void testConfig() { @@ -73,8 +50,8 @@ public: Env env = [&]() { auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "high"); + auto& section = p->section("sqlite"); + section.set("safety_level", "high"); } p->ledgerHistory = 100'000'000; @@ -102,8 +79,8 @@ public: Env env = [&]() { auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "low"); + auto& section = p->section("sqlite"); + section.set("safety_level", "low"); } p->ledgerHistory = 100'000'000; @@ -131,10 +108,10 @@ public: Env env = [&]() { auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kJournalMode, "off"); - section.set(Keys::kSynchronous, "extra"); - section.set(Keys::kTempStore, "default"); + auto& section = p->section("sqlite"); + section.set("journal_mode", "off"); + section.set("synchronous", "extra"); + section.set("temp_store", "default"); } return Env( @@ -145,7 +122,7 @@ public: }(); // No warning, even though higher risk settings were used because - // LEDGER_HISTORY is small + // ledgerHistory is small BEAST_EXPECT(!found); auto const s = setupDatabaseCon(env.app().config()); if (BEAST_EXPECT(s.globalPragma->size() == 3)) @@ -163,10 +140,10 @@ public: Env env = [&]() { auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kJournalMode, "off"); - section.set(Keys::kSynchronous, "extra"); - section.set(Keys::kTempStore, "default"); + auto& section = p->section("sqlite"); + section.set("journal_mode", "off"); + section.set("synchronous", "extra"); + section.set("temp_store", "default"); } p->ledgerHistory = 50'000'000; @@ -178,7 +155,7 @@ public: }(); // No warning, even though higher risk settings were used because - // LEDGER_HISTORY is small + // ledgerHistory is small BEAST_EXPECT(found); auto const s = setupDatabaseCon(env.app().config()); if (BEAST_EXPECT(s.globalPragma->size() == 3)) @@ -199,11 +176,11 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "low"); - section.set(Keys::kJournalMode, "off"); - section.set(Keys::kSynchronous, "extra"); - section.set(Keys::kTempStore, "default"); + auto& section = p->section("sqlite"); + section.set("safety_level", "low"); + section.set("journal_mode", "off"); + section.set("synchronous", "extra"); + section.set("temp_store", "default"); } try @@ -230,9 +207,9 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "high"); - section.set(Keys::kJournalMode, "off"); + auto& section = p->section("sqlite"); + section.set("safety_level", "high"); + section.set("journal_mode", "off"); } try @@ -259,9 +236,9 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "low"); - section.set(Keys::kSynchronous, "extra"); + auto& section = p->section("sqlite"); + section.set("safety_level", "low"); + section.set("synchronous", "extra"); } try @@ -288,9 +265,9 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "high"); - section.set(Keys::kTempStore, "default"); + auto& section = p->section("sqlite"); + section.set("safety_level", "high"); + section.set("temp_store", "default"); } try @@ -317,8 +294,8 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSafetyLevel, "slow"); + auto& section = p->section("sqlite"); + section.set("safety_level", "slow"); } try @@ -345,8 +322,8 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kJournalMode, "fast"); + auto& section = p->section("sqlite"); + section.set("journal_mode", "fast"); } try @@ -373,8 +350,8 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kSynchronous, "instant"); + auto& section = p->section("sqlite"); + section.set("synchronous", "instant"); } try @@ -401,8 +378,8 @@ public: auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kTempStore, "network"); + auto& section = p->section("sqlite"); + section.set("temp_store", "network"); } try @@ -436,9 +413,9 @@ public: Env env = [&]() { auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kPageSize, "512"); - section.set(Keys::kJournalSizeLimit, "2582080"); + auto& section = p->section("sqlite"); + section.set("page_size", "512"); + section.set("journal_size_limit", "2582080"); } return Env(*this, std::move(p)); }(); @@ -457,8 +434,8 @@ public: bool found = false; auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kPageSize, "256"); + auto& section = p->section("sqlite"); + section.set("page_size", "256"); } try { @@ -480,8 +457,8 @@ public: bool found = false; auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kPageSize, "131072"); + auto& section = p->section("sqlite"); + section.set("page_size", "131072"); } try { @@ -503,8 +480,8 @@ public: bool found = false; auto p = test::jtx::envconfig(); { - auto& section = p->section(Sections::kSqlite); - section.set(Keys::kPageSize, "513"); + auto& section = p->section("sqlite"); + section.set("page_size", "513"); } try { @@ -522,208 +499,13 @@ public: } } - //-------------------------------------------------------------------------- - - void - testImport( - std::string const& destBackendType, - std::string const& srcBackendType, - std::int64_t seedValue) - { - DummyScheduler scheduler; - - beast::TempDir const nodeDb; - Section srcParams; - srcParams.set(Keys::kType, srcBackendType); - srcParams.set(Keys::kPath, nodeDb.path()); - - // Create a batch - auto batch = createPredictableBatch(kNumObjectsToTest, seedValue); - - // Write to source db - { - std::unique_ptr src = - Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal_); - storeBatch(*src, batch); - } - - Batch copy; - - { - // Re-open the db - std::unique_ptr src = - Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal_); - - // Set up the destination database - beast::TempDir const destDb; - Section destParams; - destParams.set(Keys::kType, destBackendType); - destParams.set(Keys::kPath, destDb.path()); - - std::unique_ptr dest = - Manager::instance().makeDatabase(megabytes(4), scheduler, 2, destParams, journal_); - - testcase("import into '" + destBackendType + "' from '" + srcBackendType + "'"); - - // Do the import - dest->importDatabase(*src); - - // Get the results of the import - fetchCopyOfBatch(*dest, ©, batch); - } - - // Canonicalize the source and destination batches - std::ranges::sort(batch, LessThan{}); - std::ranges::sort(copy, LessThan{}); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - - //-------------------------------------------------------------------------- - - void - testNodeStore( - std::string const& type, - bool const testPersistence, - std::int64_t const seedValue, - int numObjsToTest = 2000) - { - DummyScheduler scheduler; - - std::string const s = "NodeStore backend '" + type + "'"; - - testcase(s); - - beast::TempDir const nodeDb; - Section nodeParams; - nodeParams.set(Keys::kType, type); - nodeParams.set(Keys::kPath, nodeDb.path()); - - beast::xor_shift_engine rng(seedValue); - - // Create a batch - auto batch = createPredictableBatch(numObjsToTest, rng()); - - { - // Open the database - std::unique_ptr db = - Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal_); - - // Write the batch - storeBatch(*db, batch); - - { - // Read it back in - Batch copy; - fetchCopyOfBatch(*db, ©, batch); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - - { - // Reorder and read the copy again - std::shuffle(batch.begin(), batch.end(), rng); - Batch copy; - fetchCopyOfBatch(*db, ©, batch); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - } - - if (testPersistence) - { - // Re-open the database without the ephemeral DB - std::unique_ptr db = - Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal_); - - // Read it back in - Batch copy; - fetchCopyOfBatch(*db, ©, batch); - - // Canonicalize the source and destination batches - std::ranges::sort(batch, LessThan{}); - std::ranges::sort(copy, LessThan{}); - BEAST_EXPECT(areBatchesEqual(batch, copy)); - } - - if (type == "memory") - { - // Verify default earliest ledger sequence - { - std::unique_ptr db = Manager::instance().makeDatabase( - megabytes(4), scheduler, 2, nodeParams, journal_); - BEAST_EXPECT(db->earliestLedgerSeq() == kXrpLedgerEarliestSeq); - } - - // Set an invalid earliest ledger sequence - try - { - nodeParams.set(Keys::kEarliestSeq, "0"); - std::unique_ptr const db = Manager::instance().makeDatabase( - megabytes(4), scheduler, 2, nodeParams, journal_); - } - catch (std::runtime_error const& e) - { - BEAST_EXPECT(std::strcmp(e.what(), "Invalid earliest_seq") == 0); - } - - { - // Set a valid earliest ledger sequence - nodeParams.set(Keys::kEarliestSeq, "1"); - std::unique_ptr db = Manager::instance().makeDatabase( - megabytes(4), scheduler, 2, nodeParams, journal_); - - // Verify database uses the earliest ledger sequence setting - BEAST_EXPECT(db->earliestLedgerSeq() == 1); - } - - // Create another database that attempts to set the value again - try - { - // Set to default earliest ledger sequence - nodeParams.set(Keys::kEarliestSeq, std::to_string(kXrpLedgerEarliestSeq)); - std::unique_ptr const db2 = Manager::instance().makeDatabase( - megabytes(4), scheduler, 2, nodeParams, journal_); - } - catch (std::runtime_error const& e) - { - BEAST_EXPECT(std::strcmp(e.what(), "earliest_seq set more than once") == 0); - } - } - } - - //-------------------------------------------------------------------------- - void run() override { - std::int64_t const seedValue = 50; - testConfig(); - - testNodeStore("memory", false, seedValue); - - // Persistent backend tests - { - testNodeStore("nudb", true, seedValue); - -#if XRPL_ROCKSDB_AVAILABLE - testNodeStore("rocksdb", true, seedValue); -#endif - } - - // Import tests - { - testImport("nudb", "nudb", seedValue); - -#if XRPL_ROCKSDB_AVAILABLE - testImport("rocksdb", "rocksdb", seedValue); -#endif - -#if XRPL_ENABLE_SQLITE_BACKEND_TESTS - testImport("sqlite", "sqlite", seedValue); -#endif - } } }; -BEAST_DEFINE_TESTSUITE(Database, nodestore, xrpl); +BEAST_DEFINE_TESTSUITE(DatabaseConfig, nodestore, xrpl); -} // namespace xrpl::NodeStore +} // namespace xrpl::node_store diff --git a/src/test/nodestore/NuDBFactory_test.cpp b/src/test/nodestore/NuDBFactory_test.cpp deleted file mode 100644 index d0675b3893..0000000000 --- a/src/test/nodestore/NuDBFactory_test.cpp +++ /dev/null @@ -1,443 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::NodeStore { - -class NuDBFactory_test : public TestBase -{ -private: - // Helper function to create a Section with specified parameters - static Section - createSection(std::string const& path, std::string const& blockSize = "") - { - Section params; - params.set(Keys::kType, "nudb"); - params.set(Keys::kPath, path); - if (!blockSize.empty()) - params.set(Keys::kNudbBlockSize, blockSize); - return params; - } - - // Helper function to create a backend and test basic functionality - bool - testBackendFunctionality(Section const& params, std::size_t expectedBlocksize) - { - try - { - DummyScheduler scheduler; - test::SuiteJournal journal("NuDBFactory_test", *this); - - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - - if (!BEAST_EXPECT(backend)) - return false; - - if (!BEAST_EXPECT(backend->getBlockSize() == expectedBlocksize)) - return false; - - backend->open(); - - if (!BEAST_EXPECT(backend->isOpen())) - return false; - - // Test basic store/fetch functionality - auto batch = createPredictableBatch(10, 12345); - storeBatch(*backend, batch); - - Batch copy; - fetchCopyOfBatch(*backend, ©, batch); - - backend->close(); - - return areBatchesEqual(batch, copy); - } - catch (...) - { - return false; - } - } - - // Helper function to test log messages - void - testLogMessage(Section const& params, beast::Severity level, std::string const& expectedMessage) - { - test::StreamSink sink(level); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - auto backend = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - - std::string const logOutput = sink.messages().str(); - BEAST_EXPECT(logOutput.contains(expectedMessage)); - } - - // Helper function to test power of two validation - void - testPowerOfTwoValidation(std::string const& size, bool shouldWork) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), size); - - test::StreamSink sink(beast::Severity::Warning); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - auto backend = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - - std::string const logOutput = sink.messages().str(); - bool const hasWarning = logOutput.contains("Invalid nudb_block_size"); - - BEAST_EXPECT(hasWarning == !shouldWork); - } - -public: - void - testDefaultBlockSize() - { - testcase("Default block size (no nudb_block_size specified)"); - - beast::TempDir const tempDir; - auto params = createSection(tempDir.path()); - - // Should work with default 4096 block size - BEAST_EXPECT(testBackendFunctionality(params, 4096)); - } - - void - testValidBlockSizes() - { - testcase("Valid block sizes"); - - std::vector const validSizes = {4096, 8192, 16384, 32768}; - - for (auto const& size : validSizes) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), to_string(size)); - - BEAST_EXPECT(testBackendFunctionality(params, size)); - } - // Empty value is ignored by the config parser, so uses the - // default - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), ""); - - BEAST_EXPECT(testBackendFunctionality(params, 4096)); - } - - void - testInvalidBlockSizes() - { - testcase("Invalid block sizes"); - - std::vector const invalidSizes = { - "2048", // Too small - "1024", // Too small - "65536", // Too large - "131072", // Too large - "5000", // Not power of 2 - "6000", // Not power of 2 - "10000", // Not power of 2 - "0", // Zero - "-1", // Negative - "abc", // Non-numeric - "4k", // Invalid format - "4096.5" // Decimal - }; - - for (auto const& size : invalidSizes) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), size); - - // Fails - BEAST_EXPECT(!testBackendFunctionality(params, 4096)); - } - - // Test whitespace cases separately since lexical_cast may handle them - std::vector const whitespaceInvalidSizes = { - "4096 ", // Trailing space - might be handled by lexical_cast - " 4096" // Leading space - might be handled by lexical_cast - }; - - for (auto const& size : whitespaceInvalidSizes) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), size); - - // Fails - BEAST_EXPECT(!testBackendFunctionality(params, 4096)); - } - } - - void - testLogMessages() - { - testcase("Log message verification"); - - // Test valid custom block size logging - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), "8192"); - - testLogMessage(params, beast::Severity::Info, "Using custom NuDB block size: 8192"); - } - - // Test invalid block size failure - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), "5000"); - - test::StreamSink sink(beast::Severity::Warning); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - try - { - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - fail(); - } - catch (std::exception const& e) - { - std::string const logOutput{e.what()}; - BEAST_EXPECT(logOutput.contains("Invalid nudb_block_size: 5000")); - BEAST_EXPECT(logOutput.contains("Must be power of 2 between 4096 and 32768")); - } - } - - // Test non-numeric value failure - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), "invalid"); - - test::StreamSink sink(beast::Severity::Warning); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - try - { - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - - fail(); - } - catch (std::exception const& e) - { - std::string const logOutput{e.what()}; - BEAST_EXPECT(logOutput.contains("Invalid nudb_block_size value: invalid")); - } - } - } - - void - testPowerOfTwoValidation() - { - testcase("Power of 2 validation logic"); - - // Test edge cases around valid range - std::vector> const testCases = { - {"4095", false}, // Just below minimum - {"4096", true}, // Minimum valid - {"4097", false}, // Just above minimum, not power of 2 - {"8192", true}, // Valid power of 2 - {"8193", false}, // Just above valid power of 2 - {"16384", true}, // Valid power of 2 - {"32768", true}, // Maximum valid - {"32769", false}, // Just above maximum - {"65536", false} // Power of 2 but too large - }; - - for (auto const& [size, shouldWork] : testCases) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), size); - - // We test the validation logic by catching exceptions for invalid - // values - test::StreamSink sink(beast::Severity::Warning); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - try - { - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - BEAST_EXPECT(shouldWork); - } - catch (std::exception const& e) - { - std::string const logOutput{e.what()}; - BEAST_EXPECT(logOutput.contains("Invalid nudb_block_size")); - } - } - } - - void - testBothConstructorVariants() - { - testcase("Both constructor variants work with custom block size"); - - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), "16384"); - - DummyScheduler scheduler; - test::SuiteJournal journal("NuDBFactory_test", *this); - - // Test first constructor (without nudb::context) - { - auto backend1 = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - BEAST_EXPECT(backend1 != nullptr); - BEAST_EXPECT(testBackendFunctionality(params, 16384)); - } - - // Test second constructor (with nudb::context) - // Note: This would require access to nudb::context, which might not be - // easily testable without more complex setup. For now, we test that - // the factory can create backends with the first constructor. - } - - void - testConfigurationParsing() - { - testcase("Configuration parsing edge cases"); - - // Test that whitespace is handled correctly - std::vector const validFormats = { - "8192" // Basic valid format - }; - - // Test whitespace handling separately since lexical_cast behavior may - // vary - std::vector const whitespaceFormats = { - " 8192", // Leading space - may or may not be handled by - // lexical_cast - "8192 " // Trailing space - may or may not be handled by - // lexical_cast - }; - - // Test basic valid format - for (auto const& format : validFormats) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), format); - - test::StreamSink sink(beast::Severity::Info); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - - // Should log success message for valid values - std::string const logOutput = sink.messages().str(); - bool const hasSuccessMessage = logOutput.contains("Using custom NuDB block size"); - BEAST_EXPECT(hasSuccessMessage); - } - - // Test whitespace formats - these should work if lexical_cast handles - // them - for (auto const& format : whitespaceFormats) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), format); - - // Use a lower threshold to capture both info and warning messages - test::StreamSink sink(beast::Severity::Debug); - beast::Journal const journal(sink); - - DummyScheduler scheduler; - try - { - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - fail(); - } - catch (...) - { - // Fails - BEAST_EXPECT(!testBackendFunctionality(params, 8192)); - } - } - } - - void - testDataPersistence() - { - testcase("Data persistence with different block sizes"); - - std::vector const blockSizes = {"4096", "8192", "16384", "32768"}; - - for (auto const& size : blockSizes) - { - beast::TempDir const tempDir; - auto params = createSection(tempDir.path(), size); - - DummyScheduler scheduler; - test::SuiteJournal journal("NuDBFactory_test", *this); - - // Create test data - auto batch = createPredictableBatch(50, 54321); - - // Store data - { - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - backend->open(); - storeBatch(*backend, batch); - backend->close(); - } - - // Retrieve data in new backend instance - { - auto backend = - Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); - backend->open(); - - Batch copy; - fetchCopyOfBatch(*backend, ©, batch); - - BEAST_EXPECT(areBatchesEqual(batch, copy)); - backend->close(); - } - } - } - - void - run() override - { - testDefaultBlockSize(); - testValidBlockSizes(); - testInvalidBlockSizes(); - testLogMessages(); - testPowerOfTwoValidation(); - testBothConstructorVariants(); - testConfigurationParsing(); - testDataPersistence(); - } -}; - -BEAST_DEFINE_TESTSUITE(NuDBFactory, xrpl_core, xrpl); - -} // namespace xrpl::NodeStore diff --git a/src/test/nodestore/TestBase.h b/src/test/nodestore/TestBase.h deleted file mode 100644 index 235e76501f..0000000000 --- a/src/test/nodestore/TestBase.h +++ /dev/null @@ -1,202 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include - -namespace xrpl::NodeStore { - -/** - * Binary function that satisfies the strict-weak-ordering requirement. - * - * This compares the hashes of both objects and returns true if - * the first hash is considered to go before the second. - * - * @see std::sort - */ -struct LessThan -{ - bool - operator()(std::shared_ptr const& lhs, std::shared_ptr const& rhs) - const noexcept - { - return lhs->getHash() < rhs->getHash(); - } -}; - -/** - * Returns `true` if objects are identical. - */ -inline bool -isSame(std::shared_ptr const& lhs, std::shared_ptr const& rhs) -{ - return (lhs->getType() == rhs->getType()) && (lhs->getHash() == rhs->getHash()) && - (lhs->getData() == rhs->getData()); -} - -// Some common code for the unit tests -// -class TestBase : public beast::unit_test::Suite -{ -public: - // Tunable parameters - // - static std::size_t const kMinPayloadBytes = 1; - static std::size_t const kMaxPayloadBytes = 2000; - static int const kNumObjectsToTest = 2000; - -public: - // Create a predictable batch of objects - static Batch - createPredictableBatch(int numObjects, std::uint64_t seed) - { - Batch batch; - batch.reserve(numObjects); - - beast::xor_shift_engine rng(seed); - - for (int i = 0; i < numObjects; ++i) - { - NodeObjectType const type = [&] { - switch (randInt(rng, 3)) - { - case 0: - return NodeObjectType::Ledger; - case 1: - return NodeObjectType::AccountNode; - case 2: - return NodeObjectType::TransactionNode; - case 3: - default: - return NodeObjectType::Unknown; - } - }(); - - uint256 hash; - beast::rngfill(hash.begin(), hash.size(), rng); - - Blob blob(randInt(rng, kMinPayloadBytes, kMaxPayloadBytes)); - beast::rngfill(blob.data(), blob.size(), rng); - - batch.push_back(NodeObject::createObject(type, std::move(blob), hash)); - } - - return batch; - } - - // Compare two batches for equality - static bool - areBatchesEqual(Batch const& lhs, Batch const& rhs) - { - bool result = true; - - if (lhs.size() == rhs.size()) - { - for (int i = 0; i < lhs.size(); ++i) - { - if (!isSame(lhs[i], rhs[i])) - { - result = false; - break; - } - } - } - else - { - result = false; - } - - return result; - } - - // Store a batch in a backend - static void - storeBatch(Backend& backend, Batch const& batch) - { - for (auto const& object : batch) - { - backend.store(object); - } - } - - // Get a copy of a batch in a backend - void - fetchCopyOfBatch(Backend& backend, Batch* pCopy, Batch const& batch) - { - pCopy->clear(); - pCopy->reserve(batch.size()); - - for (auto const& expected : batch) - { - std::shared_ptr object; - - Status const status = backend.fetch(expected->getHash(), &object); - - BEAST_EXPECT(status == Status::Ok); - - if (status == Status::Ok) - { - BEAST_EXPECT(object != nullptr); - - pCopy->push_back(object); - } - } - } - - void - fetchMissing(Backend& backend, Batch const& batch) - { - for (auto const& expected : batch) - { - std::shared_ptr object; - - Status const status = backend.fetch(expected->getHash(), &object); - - BEAST_EXPECT(status == Status::NotFound); - } - } - - // Store all objects in a batch - static void - storeBatch(Database& db, Batch const& batch) - { - for (auto const& object : batch) - { - Blob data(object->getData()); - - db.store(object->getType(), std::move(data), object->getHash(), db.earliestLedgerSeq()); - } - } - - // Fetch all the hashes in one batch, into another batch. - static void - fetchCopyOfBatch(Database& db, Batch* pCopy, Batch const& batch) - { - pCopy->clear(); - pCopy->reserve(batch.size()); - - for (auto const& expected : batch) - { - std::shared_ptr const object = db.fetchNodeObject(expected->getHash(), 0); - - if (object != nullptr) - pCopy->push_back(object); - } - } -}; - -} // namespace xrpl::NodeStore diff --git a/src/test/nodestore/import_test.cpp b/src/test/nodestore/import_test.cpp index d8c4a96713..c30d77029d 100644 --- a/src/test/nodestore/import_test.cpp +++ b/src/test/nodestore/import_test.cpp @@ -195,7 +195,7 @@ fmtdur(std::chrono::duration const& d) } // namespace detail -namespace NodeStore { +namespace node_store { //------------------------------------------------------------------------------ @@ -552,5 +552,5 @@ BEAST_DEFINE_TESTSUITE_MANUAL(import, nodestore, xrpl); //------------------------------------------------------------------------------ -} // namespace NodeStore +} // namespace node_store } // namespace xrpl diff --git a/src/test/nodestore/varint_test.cpp b/src/test/nodestore/varint_test.cpp deleted file mode 100644 index 68e88d831a..0000000000 --- a/src/test/nodestore/varint_test.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include -#include - -#include -#include -#include -#include - -namespace xrpl::NodeStore::tests { - -class varint_test : public beast::unit_test::Suite -{ -public: - void - testVarints(std::vector vv) - { - testcase("encode, decode"); - for (auto const v : vv) - { - std::array::kMax> vi{}; - auto const n0 = writeVarint(vi.data(), v); - expect(n0 > 0, "write error"); - expect(n0 == sizeVarint(v), "size error"); - std::size_t v1 = 0; - auto const n1 = readVarint(vi.data(), n0, v1); - expect(n1 == n0, "read error"); - expect(v == v1, "wrong value"); - } - } - - void - run() override - { - testVarints( - {0, - 1, - 2, - 126, - 127, - 128, - 253, - 254, - 255, - 16127, - 16128, - 16129, - 0xff, - 0xffff, - 0xffffffff, - 0xffffffffffffUL, - 0xffffffffffffffffUL}); - } -}; - -BEAST_DEFINE_TESTSUITE(varint, nodestore, xrpl); - -} // namespace xrpl::NodeStore::tests diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 4828e03815..8e4ece1234 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -35,6 +35,7 @@ set(test_modules shamap tx protocol_autogen + nodestore ) if(NOT WIN32) list(APPEND test_modules net) diff --git a/src/tests/libxrpl/helpers/CaptureSink.h b/src/tests/libxrpl/helpers/CaptureSink.h new file mode 100644 index 0000000000..9918d13f9e --- /dev/null +++ b/src/tests/libxrpl/helpers/CaptureSink.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +namespace xrpl::test { + +class CaptureSink : public beast::Journal::Sink +{ + mutable std::mutex mutex_; + std::stringstream strm_; + +public: + explicit CaptureSink(beast::Severity threshold = beast::Severity::Debug) + : Sink{threshold, false} + { + } + + void + write(beast::Severity level, std::string const& text) override + { + if (level < threshold()) + return; + writeAlways(level, text); + } + + void + writeAlways(beast::Severity /*level*/, std::string const& text) override + { + // Journal sinks may be written to concurrently (e.g. from a backend's background workers), + // so serialize access to strm_. write() funnels into writeAlways(), so the lock lives here + // only: locking in both would self-deadlock on this non-recursive mutex. + std::scoped_lock const lock(mutex_); + strm_ << text << '\n'; + } + + [[nodiscard]] std::string + messages() const + { + // Returns a snapshot of the captured output. Takes the lock so the read is safe even if a + // writer is still active. + std::scoped_lock const lock(mutex_); + return strm_.str(); + } +}; + +} // namespace xrpl::test diff --git a/src/tests/libxrpl/helpers/TestFamily.h b/src/tests/libxrpl/helpers/TestFamily.h index 1a11d3bb68..8a599ab4da 100644 --- a/src/tests/libxrpl/helpers/TestFamily.h +++ b/src/tests/libxrpl/helpers/TestFamily.h @@ -29,11 +29,11 @@ namespace xrpl::test { class TestFamily : public Family { private: - std::unique_ptr db_; + std::unique_ptr db_; TestStopwatch clock_; std::shared_ptr fbCache_; std::shared_ptr tnCache_; - NodeStore::DummyScheduler scheduler_; + node_store::DummyScheduler scheduler_; beast::Journal j_; public: @@ -51,16 +51,16 @@ public: Section config; config.set(Keys::kType, "memory"); config.set(Keys::kPath, "TestFamily"); - db_ = NodeStore::Manager::instance().makeDatabase(megabytes(4), scheduler_, 1, config, j); + db_ = node_store::Manager::instance().makeDatabase(megabytes(4), scheduler_, 1, config, j); } - NodeStore::Database& + node_store::Database& db() override { return *db_; } - [[nodiscard]] NodeStore::Database const& + [[nodiscard]] node_store::Database const& db() const override { return *db_; diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h index 5475b54dc6..f7b09bccd1 100644 --- a/src/tests/libxrpl/helpers/TestServiceRegistry.h +++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h @@ -220,7 +220,7 @@ public: } // Storage services - NodeStore::Database& + node_store::Database& getNodeStore() override { throw std::logic_error("TestServiceRegistry::getNodeStore() not implemented"); diff --git a/src/tests/libxrpl/nodestore/Backend.cpp b/src/tests/libxrpl/nodestore/Backend.cpp new file mode 100644 index 0000000000..eb78851429 --- /dev/null +++ b/src/tests/libxrpl/nodestore/Backend.cpp @@ -0,0 +1,182 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::node_store { + +namespace { + +std::vector +backendTypes() +{ + std::vector types{"nudb"}; +#if XRPL_ROCKSDB_AVAILABLE + types.emplace_back("rocksdb"); +#endif +#ifdef XRPL_ENABLE_SQLITE_BACKEND_TESTS + types.emplace_back("sqlite"); +#endif + return types; +} + +// Run work(i) for every i in [0, n) spread across numThreads threads, handing +// out indices via a shared atomic counter (mirrors the old Timing_test +// parallel-for so the N items are partitioned, not duplicated). +template +void +parallelFor(std::size_t n, std::size_t numThreads, Work work) +{ + std::atomic next{0}; + auto const runner = [&] { + for (std::size_t i = next++; i < n; i = next++) + work(i); + }; + + auto threads = std::views::iota(std::size_t{0}, numThreads) | + std::views::transform([&](std::size_t) { return std::thread{runner}; }) | + std::ranges::to(); + + std::ranges::for_each(threads, &std::thread::join); +} + +} // namespace + +class BackendTypeTest : public ::testing::TestWithParam +{ +protected: + void + SetUp() override + { + params_.set("type", GetParam()); + params_.set("path", tempDir_.path()); + + beast::xor_shift_engine rng(kSeedValue); + batch_ = createPredictableBatch(kNumObjects, rng()); + } + + std::unique_ptr + makeOpenBackend() + { + auto backend = Manager::instance().makeBackend(params_, megabytes(4), scheduler_, journal_); + backend->open(); + return backend; + } + + DummyScheduler scheduler_; + beast::TempDir const tempDir_; + beast::Journal const journal_{TestSink::instance()}; + Section params_; + Batch batch_; +}; + +TEST_P(BackendTypeTest, store_and_fetch) +{ + auto backend = makeOpenBackend(); + storeBatch(*backend, batch_); + + { + SCOPED_TRACE("read in original order"); + auto const copy = fetchCopyOfBatch(*backend, batch_); + EXPECT_EQ(batch_, copy); + } + + { + SCOPED_TRACE("read in shuffled order"); + beast::xor_shift_engine rng(kSeedValue); + std::shuffle(batch_.begin(), batch_.end(), rng); + auto const copy = fetchCopyOfBatch(*backend, batch_); + EXPECT_EQ(batch_, copy); + } +} + +TEST_P(BackendTypeTest, persists_after_reopen) +{ + { + auto backend = makeOpenBackend(); + storeBatch(*backend, batch_); + } + + // re-open a fresh backend instance over the same path + auto backend = makeOpenBackend(); + auto copy = fetchCopyOfBatch(*backend, batch_); + std::ranges::sort(batch_, LessThan{}); + std::ranges::sort(copy, LessThan{}); + EXPECT_EQ(batch_, copy); +} + +// missing-key path. Replaces the correctness half of Timing_test::doMissing +// (and the missing branch of doMixed): every fetch on an empty backend must +// report Status::NotFound. +TEST_P(BackendTypeTest, fetch_missing) +{ + auto backend = makeOpenBackend(); + // deliberately do NOT store batch_ — every key must be absent + fetchMissing(*backend, batch_); +} + +// concurrent store/fetch correctness. Replaces the correctness half of the +// multi-threaded Timing_test workloads (which only ran manually, never in CI): +// many threads store disjoint objects, then many threads fetch and verify each +// round-trips. Doubles as a thread-safety smoke test for the backend. +TEST_P(BackendTypeTest, concurrent_store_and_fetch) +{ + // The SQLite backend is not designed for concurrent writers (and the old + // Timing_test only exercised nudb/rocksdb under threads). + if (GetParam() == "sqlite") + GTEST_SKIP() << "sqlite backend is not exercised under concurrency"; + + for (auto const numThreads : {4uz, 8uz}) + { + SCOPED_TRACE("threads=" + std::to_string(numThreads)); + + auto backend = makeOpenBackend(); + + // concurrent stores of disjoint objects + parallelFor(batch_.size(), numThreads, [&](std::size_t i) { backend->store(batch_[i]); }); + + // concurrent fetches, each verifying its object round-trips. Worker + // threads only touch an atomic counter; the EXPECT runs on the main + // thread after join to avoid relying on cross-thread assertion support. + std::atomic mismatches{0}; + parallelFor(batch_.size(), numThreads, [&](std::size_t i) { + std::shared_ptr result; + if (backend->fetch(batch_[i]->getHash(), &result) != Status::Ok || !result || + !isSame(result, batch_[i])) + { + ++mismatches; + } + }); + EXPECT_EQ(mismatches.load(), 0u); + + backend->close(); + } +} + +INSTANTIATE_TEST_SUITE_P( + BackendTypes, + BackendTypeTest, + ::testing::ValuesIn(backendTypes()), + [](::testing::TestParamInfo const& info) { return info.param; }); + +} // namespace xrpl::node_store diff --git a/src/tests/libxrpl/nodestore/Basics.cpp b/src/tests/libxrpl/nodestore/Basics.cpp new file mode 100644 index 0000000000..5bb902af0d --- /dev/null +++ b/src/tests/libxrpl/nodestore/Basics.cpp @@ -0,0 +1,41 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace xrpl::node_store { + +TEST(NodeStoreBasics, predictable_batches) +{ + auto const batch1 = createPredictableBatch(kNumObjectsToTest, kSeedValue); + auto const batch2 = createPredictableBatch(kNumObjectsToTest, kSeedValue); + EXPECT_EQ(batch1, batch2); + + auto const batch3 = createPredictableBatch(kNumObjectsToTest, kSeedValue + 1); + EXPECT_NE(batch1, batch3); +} + +TEST(NodeStoreBasics, blob_encoding) +{ + auto const batch = createPredictableBatch(kNumObjectsToTest, kSeedValue); + for (std::size_t i = 0; i < batch.size(); ++i) + { + SCOPED_TRACE("blob index=" + std::to_string(i)); + EncodedBlob const encoded(batch[i]); + DecodedBlob decoded(encoded.getKey(), encoded.getData(), encoded.getSize()); + EXPECT_TRUE(decoded.wasOk()); + if (decoded.wasOk()) + { + std::shared_ptr const object(decoded.createObject()); + EXPECT_TRUE(isSame(batch[i], object)); + } + } +} + +} // namespace xrpl::node_store diff --git a/src/tests/libxrpl/nodestore/Database.cpp b/src/tests/libxrpl/nodestore/Database.cpp new file mode 100644 index 0000000000..23087a2f84 --- /dev/null +++ b/src/tests/libxrpl/nodestore/Database.cpp @@ -0,0 +1,248 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::node_store { + +namespace { + +std::vector +allBackends() +{ + std::vector types{"memory", "nudb"}; +#if XRPL_ROCKSDB_AVAILABLE + types.emplace_back("rocksdb"); +#endif + return types; +} + +std::vector +persistentBackends() +{ + std::vector types{"nudb"}; +#if XRPL_ROCKSDB_AVAILABLE + types.emplace_back("rocksdb"); +#endif + return types; +} + +std::vector +importBackends() +{ + std::vector types{"nudb"}; +#if XRPL_ROCKSDB_AVAILABLE + types.emplace_back("rocksdb"); +#endif +#ifdef XRPL_ENABLE_SQLITE_BACKEND_TESTS + types.emplace_back("sqlite"); +#endif + return types; +} + +} // namespace + +// Shared setup for the parameterized Database tests: builds the node params, +// journal and a predictable batch per test, mirroring Backend.cpp's fixture. +class NodeStoreDatabaseTestBase : public ::testing::TestWithParam +{ +protected: + void + SetUp() override + { + nodeParams_.set("type", GetParam()); + nodeParams_.set("path", nodeDb_.path()); + + beast::xor_shift_engine rng(kSeedValue); + batch_ = createPredictableBatch(kNumObjects, rng()); + } + + std::unique_ptr + makeDatabase() + { + return Manager::instance().makeDatabase(megabytes(4), scheduler_, 2, nodeParams_, journal_); + } + + DummyScheduler scheduler_; + beast::TempDir const nodeDb_; + beast::Journal const journal_{TestSink::instance()}; + Section nodeParams_; + Batch batch_; +}; + +class NodeStoreDatabaseTest : public NodeStoreDatabaseTestBase +{ +}; + +class NodeStoreDatabasePersistenceTest : public NodeStoreDatabaseTestBase +{ +}; + +TEST_P(NodeStoreDatabaseTest, store_and_fetch) +{ + auto db = makeDatabase(); + + storeBatch(*db, batch_); + + { + SCOPED_TRACE("read in original order"); + auto const copy = fetchCopyOfBatch(*db, batch_); + EXPECT_EQ(batch_, copy); + } + + { + SCOPED_TRACE("read in shuffled order"); + beast::xor_shift_engine rng(kSeedValue); + std::shuffle(batch_.begin(), batch_.end(), rng); + auto const copy = fetchCopyOfBatch(*db, batch_); + EXPECT_EQ(batch_, copy); + } +} + +TEST_P(NodeStoreDatabasePersistenceTest, round_trip) +{ + { + auto db = makeDatabase(); + storeBatch(*db, batch_); + } + + // re-open without the ephemeral db + auto db = makeDatabase(); + + auto copy = fetchCopyOfBatch(*db, batch_); + std::ranges::sort(batch_, LessThan{}); + std::ranges::sort(copy, LessThan{}); + EXPECT_EQ(batch_, copy); +} + +// missing-key path at the Database layer. Mirrors Backend's fetch_missing — +// fetching keys that were never stored must return nullptr (NotFound). +TEST_P(NodeStoreDatabaseTest, fetch_missing) +{ + auto db = makeDatabase(); + + // never store: every key must be absent + fetchMissing(*db, batch_); +} + +INSTANTIATE_TEST_SUITE_P( + NodeStoreBackends, + NodeStoreDatabaseTest, + ::testing::ValuesIn(allBackends()), + [](::testing::TestParamInfo const& info) { return info.param; }); + +INSTANTIATE_TEST_SUITE_P( + PersistentBackends, + NodeStoreDatabasePersistenceTest, + ::testing::ValuesIn(persistentBackends()), + [](::testing::TestParamInfo const& info) { return info.param; }); + +TEST(NodeStoreDatabase, memory_earliest_seq) +{ + DummyScheduler scheduler; + beast::TempDir const nodeDb; + Section nodeParams; + nodeParams.set("type", "memory"); + nodeParams.set("path", nodeDb.path()); + + beast::Journal const journal(TestSink::instance()); + + // default earliest ledger sequence + { + auto db = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal); + EXPECT_EQ(db->earliestLedgerSeq(), kXrpLedgerEarliestSeq); + } + + // invalid earliest_seq value + { + nodeParams.set("earliest_seq", "0"); + try + { + auto db = + Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal); + FAIL() << "expected runtime_error for earliest_seq=0"; + } + catch (std::runtime_error const& e) + { + EXPECT_STREQ(e.what(), "Invalid earliest_seq"); + } + } + + // valid earliest_seq value + { + nodeParams.set("earliest_seq", "1"); + auto db = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, nodeParams, journal); + EXPECT_EQ(db->earliestLedgerSeq(), 1u); + } +} + +class DatabaseImportTest : public ::testing::TestWithParam +{ +}; + +TEST_P(DatabaseImportTest, same_backend) +{ + auto const type = GetParam(); + + DummyScheduler scheduler; + beast::Journal const journal(TestSink::instance()); + + beast::TempDir const srcDir; + Section srcParams; + srcParams.set("type", type); + srcParams.set("path", srcDir.path()); + + auto batch = createPredictableBatch(kNumObjects, kSeedValue); + + // write to source db + { + auto src = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal); + storeBatch(*src, batch); + } + + Batch copy; + { + // re-open source and import into a fresh destination + auto src = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal); + + beast::TempDir const destDir; + Section destParams; + destParams.set("type", type); + destParams.set("path", destDir.path()); + + auto dest = + Manager::instance().makeDatabase(megabytes(4), scheduler, 2, destParams, journal); + + dest->importDatabase(*src); + copy = fetchCopyOfBatch(*dest, batch); + } + + std::ranges::sort(batch, LessThan{}); + std::ranges::sort(copy, LessThan{}); + EXPECT_EQ(batch, copy); +} + +INSTANTIATE_TEST_SUITE_P( + ImportBackends, + DatabaseImportTest, + ::testing::ValuesIn(importBackends()), + [](::testing::TestParamInfo const& info) { return info.param; }); + +} // namespace xrpl::node_store diff --git a/src/tests/libxrpl/nodestore/NuDBFactory.cpp b/src/tests/libxrpl/nodestore/NuDBFactory.cpp new file mode 100644 index 0000000000..c126984630 --- /dev/null +++ b/src/tests/libxrpl/nodestore/NuDBFactory.cpp @@ -0,0 +1,297 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::node_store { + +namespace { + +Section +makeSection(std::string const& path, std::string const& blockSize = "") +{ + Section params; + params.set("type", "nudb"); + params.set("path", path); + if (!blockSize.empty()) + params.set("nudb_block_size", blockSize); + return params; +} + +void +runRoundTrip(Section const& params, std::size_t expectedBlocksize) +{ + DummyScheduler scheduler; + beast::Journal const journal(TestSink::instance()); + auto backend = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + + ASSERT_TRUE(backend); + ASSERT_EQ(backend->getBlockSize(), expectedBlocksize); + backend->open(); + ASSERT_TRUE(backend->isOpen()); + + auto const batch = createPredictableBatch(10, 12345); + storeBatch(*backend, batch); + + auto const copy = fetchCopyOfBatch(*backend, batch); + + backend->close(); + EXPECT_EQ(batch, copy); +} + +} // namespace + +TEST(NuDBFactory, default_block_size) +{ + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path()); + ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096)); +} + +TEST(NuDBFactory, valid_block_sizes) +{ + auto const kValidSizes = std::to_array({4096, 8192, 16384, 32768}); + for (auto const size : kValidSizes) + { + SCOPED_TRACE("size=" + std::to_string(size)); + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), std::to_string(size)); + ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, size)); + } + + // empty value is ignored by config parser; default (4096) is used + { + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), ""); + ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096)); + } +} + +TEST(NuDBFactory, invalid_block_sizes) +{ + std::vector const kInvalidSizes = { + "2048", // too small + "1024", // too small + "65536", // too large + "131072", // too large + "5000", // not power of 2 + "6000", // not power of 2 + "10000", // not power of 2 + "0", // zero + "-1", // negative + "abc", // non-numeric + "4k", // invalid format + "4096.5"}; // decimal + + for (auto const& size : kInvalidSizes) + { + SCOPED_TRACE("size='" + size + "'"); + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), size); + EXPECT_THROW(runRoundTrip(params, 4096), std::exception); + } + + // whitespace handling — lexical_cast may or may not strip; treat as invalid + std::vector const kWhitespaceSizes = {"4096 ", " 4096"}; + for (auto const& size : kWhitespaceSizes) + { + SCOPED_TRACE("size='" + size + "'"); + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), size); + EXPECT_THROW(runRoundTrip(params, 4096), std::exception); + } +} + +TEST(NuDBFactory, log_messages) +{ + // valid custom block size emits info log + { + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), "8192"); + test::CaptureSink sink(beast::Severity::Info); + beast::Journal const journal(sink); + + DummyScheduler scheduler; + [[maybe_unused]] auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + + EXPECT_TRUE(sink.messages().contains("Using custom NuDB block size: 8192")); + } + + // invalid block size throws with informative message + { + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), "5000"); + test::CaptureSink sink(beast::Severity::Warning); + beast::Journal const journal(sink); + DummyScheduler scheduler; + try + { + auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + FAIL() << "expected exception for invalid block size 5000"; + } + catch (std::exception const& e) + { + std::string const what{e.what()}; + EXPECT_TRUE(what.contains("Invalid nudb_block_size: 5000")); + EXPECT_TRUE(what.contains("Must be power of 2 between 4096 and 32768")); + } + } + + // non-numeric value throws + { + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), "invalid"); + test::CaptureSink sink(beast::Severity::Warning); + beast::Journal const journal(sink); + DummyScheduler scheduler; + try + { + auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + FAIL() << "expected exception for non-numeric block size"; + } + catch (std::exception const& e) + { + std::string const what{e.what()}; + EXPECT_TRUE(what.contains("Invalid nudb_block_size value: invalid")); + } + } +} + +TEST(NuDBFactory, power_of_two_validation) +{ + std::vector> const kCASES = { + {"4095", false}, // just below minimum + {"4096", true}, // minimum valid + {"4097", false}, // not power of 2 + {"8192", true}, // valid power of 2 + {"8193", false}, // not power of 2 + {"16384", true}, // valid power of 2 + {"32768", true}, // maximum valid + {"32769", false}, // just above maximum + {"65536", false}}; // power of 2 but too large + + for (auto const& [size, shouldWork] : kCASES) + { + SCOPED_TRACE("size=" + size + " shouldWork=" + (shouldWork ? "true" : "false")); + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), size); + test::CaptureSink sink(beast::Severity::Warning); + beast::Journal const journal(sink); + DummyScheduler scheduler; + try + { + auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + EXPECT_TRUE(shouldWork); + } + catch (std::exception const& e) + { + // A throw is only expected for sizes that should NOT work; if a + // valid size throws, fail here instead of silently matching the + // message below (which would mask the regression). + EXPECT_FALSE(shouldWork); + std::string const what{e.what()}; + EXPECT_TRUE(what.contains("Invalid nudb_block_size")); + } + } +} + +TEST(NuDBFactory, both_constructor_variants) +{ + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), "16384"); + DummyScheduler scheduler; + beast::Journal const journal(TestSink::instance()); + + auto backend1 = Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + EXPECT_NE(backend1, nullptr); + ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 16384)); + + // Test second constructor (with nudb::context) + // Note: This would require access to nudb::context, which might not be + // easily testable without more complex setup. For now, we test that + // the factory can create backends with the first constructor. +} + +TEST(NuDBFactory, configuration_parsing) +{ + // basic valid format emits success log + { + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), "8192"); + test::CaptureSink sink(beast::Severity::Info); + beast::Journal const journal(sink); + DummyScheduler scheduler; + [[maybe_unused]] auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + EXPECT_TRUE(sink.messages().contains("Using custom NuDB block size")); + } + + // Test whitespace handling separately since lexical_cast behavior may vary + std::vector const kWhitespaceFormats = {" 8192", "8192 "}; + for (auto const& format : kWhitespaceFormats) + { + SCOPED_TRACE("format='" + format + "'"); + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), format); + test::CaptureSink sink(beast::Severity::Debug); + beast::Journal const journal(sink); + DummyScheduler scheduler; + EXPECT_ANY_THROW(Manager::instance().makeBackend(params, megabytes(4), scheduler, journal)); + } +} + +TEST(NuDBFactory, data_persistence) +{ + std::vector const kBlockSizes = {"4096", "8192", "16384", "32768"}; + for (auto const& size : kBlockSizes) + { + SCOPED_TRACE("size=" + size); + beast::TempDir const tempDir; + auto const params = makeSection(tempDir.path(), size); + DummyScheduler scheduler; + beast::Journal const journal(TestSink::instance()); + + // Create test data + auto const batch = createPredictableBatch(50, 54321); + + // Store data + { + auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + backend->open(); + storeBatch(*backend, batch); + backend->close(); + } + + // Retrieve data in new backend instance + { + auto backend = + Manager::instance().makeBackend(params, megabytes(4), scheduler, journal); + backend->open(); + auto const copy = fetchCopyOfBatch(*backend, batch); + EXPECT_EQ(batch, copy); + backend->close(); + } + } +} + +} // namespace xrpl::node_store diff --git a/src/tests/libxrpl/nodestore/TestBase.h b/src/tests/libxrpl/nodestore/TestBase.h new file mode 100644 index 0000000000..5a262ac7bb --- /dev/null +++ b/src/tests/libxrpl/nodestore/TestBase.h @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace xrpl::node_store { + +constexpr std::size_t kMinPayloadBytes = 1; +constexpr std::size_t kMaxPayloadBytes = 2000; +constexpr int kNumObjectsToTest = 2000; +constexpr int kNumObjects = 2000; +constexpr std::uint64_t kSeedValue = 50; + +struct LessThan +{ + bool + operator()(std::shared_ptr const& lhs, std::shared_ptr const& rhs) + const noexcept + { + return lhs->getHash() < rhs->getHash(); + } +}; + +[[nodiscard]] inline bool +isSame(std::shared_ptr const& lhs, std::shared_ptr const& rhs) +{ + return (lhs->getType() == rhs->getType()) && (lhs->getHash() == rhs->getHash()) && + (lhs->getData() == rhs->getData()); +} + +[[nodiscard]] inline Batch +createPredictableBatch(std::size_t numObjects, std::uint64_t seed) +{ + Batch batch; + batch.reserve(numObjects); + + beast::xor_shift_engine rng(seed); + + for (auto i = 0uz; i < numObjects; ++i) + { + NodeObjectType const type = [&] { + switch (randInt(rng, 3)) + { + case 0: + return NodeObjectType::Ledger; + case 1: + return NodeObjectType::AccountNode; + case 2: + return NodeObjectType::TransactionNode; + case 3: + default: + return NodeObjectType::Unknown; + } + }(); + + uint256 hash; + beast::rngfill(hash.begin(), hash.size(), rng); + + Blob blob(randInt(rng, kMinPayloadBytes, kMaxPayloadBytes)); + beast::rngfill(blob.data(), blob.size(), rng); + + batch.emplace_back(NodeObject::createObject(type, std::move(blob), hash)); + } + + return batch; +} + +inline void +storeBatch(Backend& backend, Batch const& batch) +{ + for (auto const& obj : batch) + backend.store(obj); +} + +[[nodiscard]] inline Batch +fetchCopyOfBatch(Backend& backend, Batch const& batch) +{ + Batch copy; + copy.reserve(batch.size()); + + for (auto i = 0uz; i < batch.size(); ++i) + { + SCOPED_TRACE("fetchCopyOfBatch index=" + std::to_string(i)); + std::shared_ptr object; + Status const status = backend.fetch(batch[i]->getHash(), &object); + EXPECT_EQ(status, Status::Ok); + if (status == Status::Ok) + { + EXPECT_NE(object, nullptr); + copy.emplace_back(object); + } + } + return copy; +} + +inline void +fetchMissing(Backend& backend, Batch const& batch) +{ + for (auto i = 0uz; i < batch.size(); ++i) + { + SCOPED_TRACE("fetchMissing index=" + std::to_string(i)); + std::shared_ptr object; + Status const status = backend.fetch(batch[i]->getHash(), &object); + EXPECT_EQ(status, Status::NotFound); + } +} + +inline void +storeBatch(Database& db, Batch const& batch) +{ + for (auto const& obj : batch) + { + Blob data(obj->getData()); + db.store(obj->getType(), std::move(data), obj->getHash(), db.earliestLedgerSeq()); + } +} + +[[nodiscard]] inline Batch +fetchCopyOfBatch(Database& db, Batch const& batch) +{ + Batch copy; + copy.reserve(batch.size()); + + for (auto const& obj : batch) + { + std::shared_ptr const result = db.fetchNodeObject(obj->getHash(), 0); + if (result != nullptr) + copy.emplace_back(result); + } + return copy; +} + +inline void +fetchMissing(Database& db, Batch const& batch) +{ + for (auto i = 0uz; i < batch.size(); ++i) + { + SCOPED_TRACE("fetchMissing(Database) index=" + std::to_string(i)); + EXPECT_EQ(db.fetchNodeObject(batch[i]->getHash(), 0), nullptr); + } +} + +} // namespace xrpl::node_store + +namespace xrpl { + +[[nodiscard]] inline bool +operator==(node_store::Batch const& lhs, node_store::Batch const& rhs) +{ + return std::ranges::equal(lhs, rhs, node_store::isSame); +} + +} // namespace xrpl diff --git a/src/tests/libxrpl/nodestore/varint.cpp b/src/tests/libxrpl/nodestore/varint.cpp new file mode 100644 index 0000000000..fee96f314b --- /dev/null +++ b/src/tests/libxrpl/nodestore/varint.cpp @@ -0,0 +1,46 @@ +#include + +#include + +#include +#include +#include +#include +#include + +using namespace xrpl::node_store; + +TEST(varint, encode_decode) +{ + std::vector const kVALUES = { + 0, + 1, + 2, + 126, + 127, + 128, + 253, + 254, + 255, + 16127, + 16128, + 16129, + 0xff, + 0xffff, + 0xffffffff, + 0xffffffffffffUL, + 0xffffffffffffffffUL}; + + for (auto const v : kVALUES) + { + SCOPED_TRACE("value=" + std::to_string(v)); + std::array::kMax> vi{}; + auto const n0 = writeVarint(vi.data(), v); + EXPECT_GT(n0, 0u) << "write error"; + EXPECT_EQ(n0, sizeVarint(v)) << "size error"; + std::size_t v1 = 0; + auto const n1 = readVarint(vi.data(), n0, v1); + EXPECT_EQ(n1, n0) << "read error"; + EXPECT_EQ(v1, v) << "wrong value"; + } +} diff --git a/src/tests/libxrpl/shamap/common.h b/src/tests/libxrpl/shamap/common.h index 5b44f2b251..91401d2973 100644 --- a/src/tests/libxrpl/shamap/common.h +++ b/src/tests/libxrpl/shamap/common.h @@ -24,13 +24,13 @@ namespace xrpl::tests { class TestNodeFamily : public Family { private: - std::unique_ptr db_; + std::unique_ptr db_; std::shared_ptr fbCache_; std::shared_ptr tnCache_; TestStopwatch clock_; - NodeStore::DummyScheduler scheduler_; + node_store::DummyScheduler scheduler_; beast::Journal const j_; @@ -49,17 +49,17 @@ public: Section testSection; testSection.set(Keys::kType, "memory"); testSection.set(Keys::kPath, "SHAMap_test"); - db_ = NodeStore::Manager::instance().makeDatabase( + db_ = node_store::Manager::instance().makeDatabase( megabytes(4), scheduler_, 1, testSection, j); } - NodeStore::Database& + node_store::Database& db() override { return *db_; } - [[nodiscard]] NodeStore::Database const& + [[nodiscard]] node_store::Database const& db() const override { return *db_; diff --git a/src/xrpld/app/ledger/AccountStateSF.h b/src/xrpld/app/ledger/AccountStateSF.h index f5117db4d4..5c1d260c9a 100644 --- a/src/xrpld/app/ledger/AccountStateSF.h +++ b/src/xrpld/app/ledger/AccountStateSF.h @@ -18,7 +18,7 @@ namespace xrpl { class AccountStateSF : public SHAMapSyncFilter { public: - AccountStateSF(NodeStore::Database& db, AbstractFetchPackContainer& fp) : db_(db), fp_(fp) + AccountStateSF(node_store::Database& db, AbstractFetchPackContainer& fp) : db_(db), fp_(fp) { } @@ -34,7 +34,7 @@ public: getNode(SHAMapHash const& nodeHash) const override; private: - NodeStore::Database& db_; + node_store::Database& db_; AbstractFetchPackContainer& fp_; }; diff --git a/src/xrpld/app/ledger/InboundLedger.h b/src/xrpld/app/ledger/InboundLedger.h index d8a9ddf46b..31ca4169ce 100644 --- a/src/xrpld/app/ledger/InboundLedger.h +++ b/src/xrpld/app/ledger/InboundLedger.h @@ -136,7 +136,7 @@ private: addPeers(); void - tryDB(NodeStore::Database& srcDB); + tryDB(node_store::Database& srcDB); void done(); diff --git a/src/xrpld/app/ledger/TransactionStateSF.h b/src/xrpld/app/ledger/TransactionStateSF.h index a3f7e7f55a..b8c1b2c835 100644 --- a/src/xrpld/app/ledger/TransactionStateSF.h +++ b/src/xrpld/app/ledger/TransactionStateSF.h @@ -18,7 +18,7 @@ namespace xrpl { class TransactionStateSF : public SHAMapSyncFilter { public: - TransactionStateSF(NodeStore::Database& db, AbstractFetchPackContainer& fp) : db_(db), fp_(fp) + TransactionStateSF(node_store::Database& db, AbstractFetchPackContainer& fp) : db_(db), fp_(fp) { } @@ -34,7 +34,7 @@ public: getNode(SHAMapHash const& nodeHash) const override; private: - NodeStore::Database& db_; + node_store::Database& db_; AbstractFetchPackContainer& fp_; }; diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index 627a5d574f..55a2a9d283 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -224,7 +224,7 @@ InboundLedger::neededStateHashes(int max, SHAMapSyncFilter const* filter) const // See how much of the ledger data is stored locally // Data found in a fetch pack will be stored void -InboundLedger::tryDB(NodeStore::Database& srcDB) +InboundLedger::tryDB(node_store::Database& srcDB) { if (!haveHeader_) { diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 2bd83b0f18..9a0335fc3c 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -653,7 +653,7 @@ LedgerMaster::tryFill(std::shared_ptr ledger) std::uint32_t minHas = seq; std::uint32_t maxHas = seq; - NodeStore::Database& nodeStore{app_.getNodeStore()}; + node_store::Database& nodeStore{app_.getNodeStore()}; while (!app_.getJobQueue().isStopping() && seq > 0) { { diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index d329475874..5c8fdad37c 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -232,7 +232,7 @@ public: std::unique_ptr resourceManager_; - std::unique_ptr nodeStore_; + std::unique_ptr nodeStore_; NodeFamily nodeFamily_; std::unique_ptr orderBookDB_; std::unique_ptr pathRequestManager_; @@ -655,7 +655,7 @@ public: return tempNodeCache_; } - NodeStore::Database& + node_store::Database& getNodeStore() override { return *nodeStore_; @@ -860,9 +860,9 @@ public: if (config_->doImport) { auto j = logs_->journal("NodeObject"); - NodeStore::DummyScheduler dummyScheduler; - std::unique_ptr source = - NodeStore::Manager::instance().makeDatabase( + node_store::DummyScheduler dummyScheduler; + std::unique_ptr source = + node_store::Manager::instance().makeDatabase( megabytes(config_->getValueFor(SizedItem::BurstSize, std::nullopt)), dummyScheduler, 0, diff --git a/src/xrpld/app/main/NodeStoreScheduler.cpp b/src/xrpld/app/main/NodeStoreScheduler.cpp index 7892503f90..c3ac5d78cf 100644 --- a/src/xrpld/app/main/NodeStoreScheduler.cpp +++ b/src/xrpld/app/main/NodeStoreScheduler.cpp @@ -12,7 +12,7 @@ NodeStoreScheduler::NodeStoreScheduler(JobQueue& jobQueue) : jobQueue_(jobQueue) } void -NodeStoreScheduler::scheduleTask(NodeStore::Task& task) +NodeStoreScheduler::scheduleTask(node_store::Task& task) { if (jobQueue_.isStopped()) return; @@ -26,19 +26,19 @@ NodeStoreScheduler::scheduleTask(NodeStore::Task& task) } void -NodeStoreScheduler::onFetch(NodeStore::FetchReport const& report) +NodeStoreScheduler::onFetch(node_store::FetchReport const& report) { if (jobQueue_.isStopped()) return; jobQueue_.addLoadEvents( - report.fetchType == NodeStore::FetchType::Async ? JtNsAsyncRead : JtNsSyncRead, + report.fetchType == node_store::FetchType::Async ? JtNsAsyncRead : JtNsSyncRead, 1, report.elapsed); } void -NodeStoreScheduler::onBatchWrite(NodeStore::BatchWriteReport const& report) +NodeStoreScheduler::onBatchWrite(node_store::BatchWriteReport const& report) { if (jobQueue_.isStopped()) return; diff --git a/src/xrpld/app/main/NodeStoreScheduler.h b/src/xrpld/app/main/NodeStoreScheduler.h index 8bfd1607ae..09a48d5be1 100644 --- a/src/xrpld/app/main/NodeStoreScheduler.h +++ b/src/xrpld/app/main/NodeStoreScheduler.h @@ -7,19 +7,19 @@ namespace xrpl { /** - * A NodeStore::Scheduler which uses the JobQueue. + * A node_store::Scheduler which uses the JobQueue. */ -class NodeStoreScheduler : public NodeStore::Scheduler +class NodeStoreScheduler : public node_store::Scheduler { public: explicit NodeStoreScheduler(JobQueue& jobQueue); void - scheduleTask(NodeStore::Task& task) override; + scheduleTask(node_store::Task& task) override; void - onFetch(NodeStore::FetchReport const& report) override; + onFetch(node_store::FetchReport const& report) override; void - onBatchWrite(NodeStore::BatchWriteReport const& report) override; + onBatchWrite(node_store::BatchWriteReport const& report) override; private: JobQueue& jobQueue_; diff --git a/src/xrpld/app/misc/SHAMapStore.h b/src/xrpld/app/misc/SHAMapStore.h index df696c685f..eeb04df53d 100644 --- a/src/xrpld/app/misc/SHAMapStore.h +++ b/src/xrpld/app/misc/SHAMapStore.h @@ -43,7 +43,7 @@ public: [[nodiscard]] virtual std::uint32_t clampFetchDepth(std::uint32_t fetchDepth) const = 0; - virtual std::unique_ptr + virtual std::unique_ptr makeNodeStore(int readThreads) = 0; /** @@ -101,5 +101,5 @@ public: //------------------------------------------------------------------------------ std::unique_ptr -makeSHAMapStore(Application& app, NodeStore::Scheduler& scheduler, beast::Journal journal); +makeSHAMapStore(Application& app, node_store::Scheduler& scheduler, beast::Journal journal); } // namespace xrpl diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp index 9b5f412fc5..e41837d206 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.cpp +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp @@ -94,7 +94,7 @@ SHAMapStoreImp::SavedStateDB::setLastRotated(LedgerIndex seq) SHAMapStoreImp::SHAMapStoreImp( Application& app, - NodeStore::Scheduler& scheduler, + node_store::Scheduler& scheduler, beast::Journal journal) : app_(app) , scheduler_(scheduler) @@ -165,7 +165,7 @@ SHAMapStoreImp::SHAMapStoreImp( } } -std::unique_ptr +std::unique_ptr SHAMapStoreImp::makeNodeStore(int readThreads) { auto nscfg = app_.config().section(Sections::kNodeDatabase); @@ -185,7 +185,7 @@ SHAMapStoreImp::makeNodeStore(int readThreads) std::to_string(app_.config().getValueFor(SizedItem::TreeCacheAge, std::nullopt))); } - std::unique_ptr db; + std::unique_ptr db; if (deleteInterval_ != 0u) { @@ -201,7 +201,7 @@ SHAMapStoreImp::makeNodeStore(int readThreads) // Create NodeStore with two backends to allow online deletion of // data - auto dbr = std::make_unique( + auto dbr = std::make_unique( scheduler_, readThreads, std::move(writableBackend), @@ -210,11 +210,11 @@ SHAMapStoreImp::makeNodeStore(int readThreads) app_.getJournal(kNodeStoreName)); fdRequired_ += dbr->fdRequired(); dbRotating_ = dbr.get(); - db.reset(dynamic_cast(dbr.release())); + db.reset(dynamic_cast(dbr.release())); } else { - db = NodeStore::Manager::instance().makeDatabase( + db = node_store::Manager::instance().makeDatabase( megabytes(app_.config().getValueFor(SizedItem::BurstSize, std::nullopt)), scheduler_, readThreads, @@ -257,7 +257,7 @@ SHAMapStoreImp::copyNode(std::uint64_t& nodeCount, SHAMapTreeNode const& node) { // Copy a single record from node to dbRotating_ auto obj = dbRotating_->fetchNodeObject( - node.getHash().asUInt256(), 0, NodeStore::FetchType::Synchronous, true); + node.getHash().asUInt256(), 0, node_store::FetchType::Synchronous, true); if (!obj) { XRPL_ASSERT(node.cowid() == 0, "SHAMapStoreImp::copyNode : rescued node must be clean"); @@ -374,7 +374,7 @@ SHAMapStoreImp::run() // exception) also clear the flag. struct RotationExposureGuard { - NodeStore::DatabaseRotating& db; + node_store::DatabaseRotating& db; ~RotationExposureGuard() { db.setRotationInFlight(false); @@ -516,7 +516,7 @@ SHAMapStoreImp::dbPaths() boost::filesystem::remove_all(p); } -std::unique_ptr +std::unique_ptr SHAMapStoreImp::makeBackendRotating(std::string path) { Section section{app_.config().section(Sections::kNodeDatabase)}; @@ -535,7 +535,7 @@ SHAMapStoreImp::makeBackendRotating(std::string path) } section.set(Keys::kPath, newPath.string()); - auto backend{NodeStore::Manager::instance().makeBackend( + auto backend{node_store::Manager::instance().makeBackend( section, megabytes(app_.config().getValueFor(SizedItem::BurstSize, std::nullopt)), scheduler_, @@ -702,7 +702,7 @@ SHAMapStoreImp::minimumOnline() const //------------------------------------------------------------------------------ std::unique_ptr -makeSHAMapStore(Application& app, NodeStore::Scheduler& scheduler, beast::Journal journal) +makeSHAMapStore(Application& app, node_store::Scheduler& scheduler, beast::Journal journal) { return std::make_unique(app, scheduler, journal); } diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h index a0ca59ecc8..8a1b7504b9 100644 --- a/src/xrpld/app/misc/SHAMapStoreImp.h +++ b/src/xrpld/app/misc/SHAMapStoreImp.h @@ -81,9 +81,9 @@ private: // minimum ledger to maintain online. std::atomic minimumOnline_; - NodeStore::Scheduler& scheduler_; + node_store::Scheduler& scheduler_; beast::Journal const journal_; - NodeStore::DatabaseRotating* dbRotating_ = nullptr; + node_store::DatabaseRotating* dbRotating_ = nullptr; SavedStateDB stateDb_; std::thread thread_; bool stop_ = false; @@ -119,7 +119,7 @@ private: static constexpr auto kNodeStoreName = "NodeStore"; public: - SHAMapStoreImp(Application& app, NodeStore::Scheduler& scheduler, beast::Journal journal); + SHAMapStoreImp(Application& app, node_store::Scheduler& scheduler, beast::Journal journal); std::uint32_t clampFetchDepth(std::uint32_t fetchDepth) const override @@ -127,7 +127,7 @@ public: return (deleteInterval_ != 0u) ? std::min(fetchDepth, deleteInterval_) : fetchDepth; } - std::unique_ptr + std::unique_ptr makeNodeStore(int readThreads) override; LedgerIndex @@ -180,7 +180,7 @@ private: void dbPaths(); - std::unique_ptr + std::unique_ptr makeBackendRotating(std::string path = std::string()); template @@ -191,7 +191,7 @@ private: for (auto const& key : cache.getKeys()) { - dbRotating_->fetchNodeObject(key, 0, NodeStore::FetchType::Synchronous, true); + dbRotating_->fetchNodeObject(key, 0, node_store::FetchType::Synchronous, true); if (!(++check % checkHealthInterval_) && healthWait() == HealthResult::Stopping) return true; } diff --git a/src/xrpld/shamap/NodeFamily.h b/src/xrpld/shamap/NodeFamily.h index d532f13ecc..1307d76886 100644 --- a/src/xrpld/shamap/NodeFamily.h +++ b/src/xrpld/shamap/NodeFamily.h @@ -33,13 +33,13 @@ public: NodeFamily(Application& app, CollectorManager& cm); - NodeStore::Database& + node_store::Database& db() override { return db_; } - [[nodiscard]] NodeStore::Database const& + [[nodiscard]] node_store::Database const& db() const override { return db_; @@ -80,7 +80,7 @@ public: private: Application& app_; - NodeStore::Database& db_; + node_store::Database& db_; beast::Journal const j_; std::shared_ptr fbCache_; From e0aa50c4bee5e93f96cb6d54ca999061eb13fd35 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Mon, 27 Jul 2026 15:18:01 +0100 Subject: [PATCH 41/86] ci: Update CI image and prepare-runner action (#7874) --- .github/scripts/strategy-matrix/linux.json | 2 +- .github/workflows/check-tools.yml | 2 +- .github/workflows/publish-docs.yml | 4 ++-- .github/workflows/reusable-build-test-config.yml | 2 +- .github/workflows/reusable-clang-tidy.yml | 4 ++-- .github/workflows/reusable-upload-recipe.yml | 2 +- .github/workflows/upload-conan-deps.yml | 2 +- nix/check-tools/README.md | 16 ++++++++-------- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 2a0b5e8e0e..60f3da09f1 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -1,5 +1,5 @@ { - "image_tag": "sha-40cdf49", + "image_tag": "sha-fecfc0c", "configs": { "ubuntu": [ { diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index af20c5f17e..99dddd7d96 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -79,7 +79,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 + uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f with: enable_ccache: false diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index c1e67e2010..a3e096315c 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -41,13 +41,13 @@ env: jobs: build: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fecfc0c steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 + uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f with: enable_ccache: false diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 74425febe8..c1b8e4195b 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -113,7 +113,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 + uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f with: enable_ccache: ${{ inputs.ccache_enabled }} diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 3c19b58a12..a6c1e6ae56 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -34,7 +34,7 @@ jobs: needs: [determine-files] if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }} runs-on: ["self-hosted", "Linux", "X64", "heavy"] - container: "ghcr.io/xrplf/xrpld/nix-debian:sha-40cdf49" + container: "ghcr.io/xrplf/xrpld/nix-debian:sha-fecfc0c" permissions: contents: read issues: write @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 + uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f with: enable_ccache: false diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index bce4da2df6..b4ab638dee 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -40,7 +40,7 @@ defaults: jobs: upload: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fecfc0c env: REMOTE_NAME: ${{ inputs.remote_name }} CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }} diff --git a/.github/workflows/upload-conan-deps.yml b/.github/workflows/upload-conan-deps.yml index 80a75a1fbf..eb58650bdf 100644 --- a/.github/workflows/upload-conan-deps.yml +++ b/.github/workflows/upload-conan-deps.yml @@ -68,7 +68,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 + uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f with: enable_ccache: false diff --git a/nix/check-tools/README.md b/nix/check-tools/README.md index fe9ab3e250..5b7538f2ca 100644 --- a/nix/check-tools/README.md +++ b/nix/check-tools/README.md @@ -3,11 +3,11 @@ These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh) — the versions of the development tooling — in each Nix environment: -| File | Environment | -| --------------------- | ----------------------------------- | -| `nix-nixos-amd64.txt` | `nix-nixos` CI image, `linux/amd64` | -| `nix-nixos-arm64.txt` | `nix-nixos` CI image, `linux/arm64` | -| `macos.txt` | macOS, inside `nix develop` | +| File | Environment | +| ---------------------- | ------------------------------------ | +| `nix-ubuntu-amd64.txt` | `nix-ubuntu` CI image, `linux/amd64` | +| `nix-ubuntu-arm64.txt` | `nix-ubuntu` CI image, `linux/arm64` | +| `macos.txt` | macOS, inside `nix develop` | The [`check-tools`](../../.github/workflows/check-tools.yml) workflow regenerates each snapshot in its environment and fails if it differs from the committed file. @@ -23,15 +23,15 @@ with `sed -n '/^Detected OS:/,$p'`. ## Regenerating -The two Linux snapshots come from the `nix-nixos` image (Docker or a compatible +The two Linux snapshots come from the `nix-ubuntu` image (Docker or a compatible runtime such as Apple `container`). The image tag is pinned in `linux.json`: ```bash -img="ghcr.io/xrplf/xrpld/nix-nixos:$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" +img="ghcr.io/xrplf/xrpld/nix-ubuntu:$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" for arch in amd64 arm64; do container run --rm -i -e CHECK_TOOLS_SKIP_CLONE=1 -a "${arch}" --entrypoint bash "${img}" -s \ - "nix/check-tools/nix-nixos-${arch}.txt" + "nix/check-tools/nix-ubuntu-${arch}.txt" done ``` From 9466ecb5c222dec63afd002dc0334a13c1f866bf Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 27 Jul 2026 15:32:53 +0100 Subject: [PATCH 42/86] ci: Group github-actions dependabot updates (#7876) --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index da7a30dc77..fcac44c44c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,3 +15,7 @@ updates: commit-message: prefix: "ci: [DEPENDABOT] " target-branch: develop + groups: + github-actions: + patterns: + - "*" From 6c9c7f0555cee008961f2167060f98a338c95aac Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Mon, 27 Jul 2026 17:34:53 +0100 Subject: [PATCH 43/86] chore: Trivial gtest migrations (#7865) --- .cspell.config.yaml | 1 - .../nodestore/detail/{varint.h => Varint.h} | 14 +- include/xrpl/nodestore/detail/codec.h | 10 +- src/test/beast/beast_Zero_test.cpp | 115 -------------- src/test/protocol/ApiVersion_test.cpp | 41 ----- src/test/protocol/Serializer_test.cpp | 52 ------- src/tests/libxrpl/CMakeLists.txt | 4 +- src/tests/libxrpl/beast/Zero.cpp | 92 +++++++++++ src/tests/libxrpl/nodestore/Codec.cpp | 146 ++++++++++++++++++ src/tests/libxrpl/nodestore/Varint.cpp | 46 ++++++ src/tests/libxrpl/nodestore/varint.cpp | 46 ------ src/tests/libxrpl/protocol/ApiVersion.cpp | 26 ++++ src/tests/libxrpl/protocol/Serializer.cpp | 49 ++++++ 13 files changed, 374 insertions(+), 268 deletions(-) rename include/xrpl/nodestore/detail/{varint.h => Varint.h} (91%) delete mode 100644 src/test/beast/beast_Zero_test.cpp delete mode 100644 src/test/protocol/ApiVersion_test.cpp delete mode 100644 src/test/protocol/Serializer_test.cpp create mode 100644 src/tests/libxrpl/beast/Zero.cpp create mode 100644 src/tests/libxrpl/nodestore/Codec.cpp create mode 100644 src/tests/libxrpl/nodestore/Varint.cpp delete mode 100644 src/tests/libxrpl/nodestore/varint.cpp create mode 100644 src/tests/libxrpl/protocol/ApiVersion.cpp create mode 100644 src/tests/libxrpl/protocol/Serializer.cpp diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 9cd8417362..e3764fd2a2 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -174,7 +174,6 @@ words: - MPTAMM - MPTDEX - Merkle - - Metafuncton - misprediction - missingok - mptbalance diff --git a/include/xrpl/nodestore/detail/varint.h b/include/xrpl/nodestore/detail/Varint.h similarity index 91% rename from include/xrpl/nodestore/detail/varint.h rename to include/xrpl/nodestore/detail/Varint.h index afbf71cdea..5474cdc8b4 100644 --- a/include/xrpl/nodestore/detail/varint.h +++ b/include/xrpl/nodestore/detail/Varint.h @@ -13,18 +13,18 @@ namespace xrpl::node_store { // https://developers.google.com/protocol-buffers/docs/encoding#varints // field tag -struct varint; +struct Varint; -// Metafuncton to return largest +// Metafunction to return largest // possible size of T represented as varint. // T must be unsigned template > -struct varint_traits; +struct VarintTraits; template -struct varint_traits +struct VarintTraits { - explicit varint_traits() = default; + explicit VarintTraits() = default; static constexpr std::size_t kMax = ((8 * sizeof(T)) + 6) / 7; }; @@ -104,7 +104,7 @@ writeVarint(void* p0, std::size_t v) template void read(nudb::detail::istream& is, std::size_t& u) - requires(std::is_same_v) + requires(std::is_same_v) { auto p0 = is(1); auto p1 = p0; @@ -118,7 +118,7 @@ read(nudb::detail::istream& is, std::size_t& u) template void write(nudb::detail::ostream& os, std::size_t t) - requires(std::is_same_v) + requires(std::is_same_v) { writeVarint(os.data(sizeVarint(t)), t); } diff --git a/include/xrpl/nodestore/detail/codec.h b/include/xrpl/nodestore/detail/codec.h index a3dfa7c944..47ad2da50a 100644 --- a/include/xrpl/nodestore/detail/codec.h +++ b/include/xrpl/nodestore/detail/codec.h @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -59,7 +59,7 @@ lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf) using std::runtime_error; using namespace nudb::detail; std::pair result; - std::array::kMax> vi{}; + std::array::kMax> vi{}; auto const n = writeVarint(vi.data(), inSize); auto const outMax = LZ4_compressBound(inSize); auto* out = reinterpret_cast(bf(n + outMax)); @@ -240,7 +240,7 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) auto* out = reinterpret_cast(bf(result.second)); result.first = out; ostream os(out, result.second); - write(os, type); + write(os, type); write(os, mask); write(os, vh.data(), n * 32); return result; @@ -252,13 +252,13 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf) auto* out = reinterpret_cast(bf(result.second)); result.first = out; ostream os(out, result.second); - write(os, type); + write(os, type); write(os, vh.data(), n * 32); return result; } } - std::array::kMax> vi{}; + std::array::kMax> vi{}; static constexpr std::size_t kCodecType = 1; auto const vn = writeVarint(vi.data(), kCodecType); diff --git a/src/test/beast/beast_Zero_test.cpp b/src/test/beast/beast_Zero_test.cpp deleted file mode 100644 index bb61844caa..0000000000 --- a/src/test/beast/beast_Zero_test.cpp +++ /dev/null @@ -1,115 +0,0 @@ -#include -#include - -namespace beast { - -struct AdlTester -{ -}; - -int -signum(AdlTester) -{ - return 0; -} - -namespace inner_adl_test { - -struct AdlTester2 -{ -}; - -int -signum(AdlTester2) -{ - return 0; -} - -} // namespace inner_adl_test - -class Zero_test : public beast::unit_test::Suite -{ -private: - struct IntegerWrapper - { - int value; - - IntegerWrapper(int v) : value(v) - { - } - - [[nodiscard]] int - signum() const - { - return value; - } - }; - -public: - void - expectSame(bool result, bool correct, char const* message) - { - expect(result == correct, message); - } - - void - testLhsZero(IntegerWrapper x) - { - expectSame(x >= kZero, x.signum() >= 0, "lhs greater-than-or-equal-to"); - expectSame(x > kZero, x.signum() > 0, "lhs greater than"); - expectSame(x == kZero, x.signum() == 0, "lhs equal to"); - expectSame(x != kZero, x.signum() != 0, "lhs not equal to"); - expectSame(x < kZero, x.signum() < 0, "lhs less than"); - expectSame(x <= kZero, x.signum() <= 0, "lhs less-than-or-equal-to"); - } - - void - testLhsZero() - { - testcase("lhs zero"); - - testLhsZero(-7); - testLhsZero(0); - testLhsZero(32); - } - - void - testRhsZero(IntegerWrapper x) - { - expectSame(kZero >= x, 0 >= x.signum(), "rhs greater-than-or-equal-to"); - expectSame(kZero > x, 0 > x.signum(), "rhs greater than"); - expectSame(kZero == x, 0 == x.signum(), "rhs equal to"); - expectSame(kZero != x, 0 != x.signum(), "rhs not equal to"); - expectSame(kZero < x, 0 < x.signum(), "rhs less than"); - expectSame(kZero <= x, 0 <= x.signum(), "rhs less-than-or-equal-to"); - } - - void - testRhsZero() - { - testcase("rhs zero"); - - testRhsZero(-4); - testRhsZero(0); - testRhsZero(64); - } - - void - testAdl() - { - expect(AdlTester{} == kZero, "ADL failure!"); - expect(inner_adl_test::AdlTester2{} == kZero, "ADL failure!"); - } - - void - run() override - { - testLhsZero(); - testRhsZero(); - testAdl(); - } -}; - -BEAST_DEFINE_TESTSUITE(Zero, beast, beast); - -} // namespace beast diff --git a/src/test/protocol/ApiVersion_test.cpp b/src/test/protocol/ApiVersion_test.cpp deleted file mode 100644 index c41fa6f6c0..0000000000 --- a/src/test/protocol/ApiVersion_test.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include - -namespace xrpl::test { -struct ApiVersion_test : beast::unit_test::Suite -{ - void - run() override - { - { - testcase("API versions invariants"); - - static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumSupportedVersion); - static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiMaximumSupportedVersion <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiBetaVersion <= RPC::kApiMaximumValidVersion); - - BEAST_EXPECT(true); - } - - { - // Update when we change versions - testcase("API versions"); - - static_assert(RPC::kApiMinimumSupportedVersion >= 1); - static_assert(RPC::kApiMinimumSupportedVersion < 2); - static_assert(RPC::kApiMaximumSupportedVersion >= 2); - static_assert(RPC::kApiMaximumSupportedVersion < 3); - static_assert(RPC::kApiMaximumValidVersion >= 3); - static_assert(RPC::kApiMaximumValidVersion < 4); - static_assert(RPC::kApiBetaVersion >= 3); - static_assert(RPC::kApiBetaVersion < 4); - - BEAST_EXPECT(true); - } - } -}; - -BEAST_DEFINE_TESTSUITE(ApiVersion, protocol, xrpl); - -} // namespace xrpl::test diff --git a/src/test/protocol/Serializer_test.cpp b/src/test/protocol/Serializer_test.cpp deleted file mode 100644 index b490e0476b..0000000000 --- a/src/test/protocol/Serializer_test.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include -#include - -#include -#include -#include - -namespace xrpl { - -struct Serializer_test : public beast::unit_test::Suite -{ - void - run() override - { - { - std::initializer_list const values = { - std::numeric_limits::min(), - -1, - 0, - 1, - std::numeric_limits::max()}; - for (std::int32_t const value : values) - { - Serializer s; - s.add32(value); - BEAST_EXPECT(s.size() == 4); - SerialIter sit(s.slice()); - BEAST_EXPECT(sit.geti32() == value); - } - } - { - std::initializer_list const values = { - std::numeric_limits::min(), - -1, - 0, - 1, - std::numeric_limits::max()}; - for (std::int64_t const value : values) - { - Serializer s; - s.add64(value); - BEAST_EXPECT(s.size() == 8); - SerialIter sit(s.slice()); - BEAST_EXPECT(sit.geti64() == value); - } - } - } -}; - -BEAST_DEFINE_TESTSUITE(Serializer, protocol, xrpl); - -} // namespace xrpl diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 8e4ece1234..c45f55b5f2 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -27,15 +27,17 @@ target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl) # supported on Windows. set(test_modules basics + beast consensus crypto json + nodestore peerfinder + protocol resource shamap tx protocol_autogen - nodestore ) if(NOT WIN32) list(APPEND test_modules net) diff --git a/src/tests/libxrpl/beast/Zero.cpp b/src/tests/libxrpl/beast/Zero.cpp new file mode 100644 index 0000000000..2ac725509a --- /dev/null +++ b/src/tests/libxrpl/beast/Zero.cpp @@ -0,0 +1,92 @@ +#include + +#include + +namespace beast { + +struct AdlTester +{ +}; + +int +signum(AdlTester) +{ + return 0; +} + +namespace inner_adl_test { + +struct AdlTester2 +{ +}; + +int +signum(AdlTester2) +{ + return 0; +} + +} // namespace inner_adl_test + +namespace { + +struct IntegerWrapper +{ + int value; + + IntegerWrapper(int v) : value(v) + { + } + + [[nodiscard]] int + signum() const + { + return value; + } +}; + +void +testLhsZero(IntegerWrapper x) +{ + EXPECT_EQ(x >= kZero, x.signum() >= 0); + EXPECT_EQ(x > kZero, x.signum() > 0); + EXPECT_EQ(x == kZero, x.signum() == 0); + EXPECT_EQ(x != kZero, x.signum() != 0); + EXPECT_EQ(x < kZero, x.signum() < 0); + EXPECT_EQ(x <= kZero, x.signum() <= 0); +} + +void +testRhsZero(IntegerWrapper x) +{ + EXPECT_EQ(kZero >= x, 0 >= x.signum()); + EXPECT_EQ(kZero > x, 0 > x.signum()); + EXPECT_EQ(kZero == x, 0 == x.signum()); + EXPECT_EQ(kZero != x, 0 != x.signum()); + EXPECT_EQ(kZero < x, 0 < x.signum()); + EXPECT_EQ(kZero <= x, 0 <= x.signum()); +} + +} // namespace + +TEST(Zero, lhs) +{ + testLhsZero(-7); + testLhsZero(0); + testLhsZero(32); +} + +TEST(Zero, rhs) +{ + testRhsZero(-4); + testRhsZero(0); + testRhsZero(64); +} + +TEST(Zero, adl) +{ + EXPECT_TRUE(AdlTester{} == kZero); + EXPECT_TRUE(inner_adl_test::AdlTester2{} == kZero); +} + +} // namespace beast diff --git a/src/tests/libxrpl/nodestore/Codec.cpp b/src/tests/libxrpl/nodestore/Codec.cpp new file mode 100644 index 0000000000..f31878f3c5 --- /dev/null +++ b/src/tests/libxrpl/nodestore/Codec.cpp @@ -0,0 +1,146 @@ +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace xrpl; +using namespace xrpl::node_store; + +namespace { + +// v1 inner-node layout: 16 hashes of 32 bytes each +constexpr std::size_t kHashCount = 16; +constexpr std::size_t kHashSize = 32; + +std::vector +makeInnerNode(std::size_t nonEmptySlots) +{ + using namespace nudb::detail; + + static constexpr std::size_t kInnerNodeSize = 525; + + std::array hashes{}; + for (auto slot = 0uz; slot < nonEmptySlots; ++slot) + { + for (auto byte = 0uz; byte < kHashSize; ++byte) + { + std::size_t const offset = (slot * kHashSize) + byte; + hashes[offset] = static_cast((offset % 255) + 1); + } + } + + std::vector blob(kInnerNodeSize); + ostream os(blob.data(), blob.size()); + write(os, 0); // index + write(os, 0); // unused + write(os, static_cast(NodeObjectType::Unknown)); + write(os, static_cast(HashPrefix::InnerNode)); + write(os, hashes.data(), hashes.size()); + + return blob; +} + +std::uint8_t +codecType(std::pair const& compressed) +{ + return static_cast(compressed.first)[0]; +} + +} // namespace + +// All 16 hash slots populated - "full v1 inner node" +TEST(Codec, inner_node_full_roundtrip) +{ + static constexpr std::uint8_t kTypeInnerNodeFull = 3; + + auto const blob = makeInnerNode(kHashCount); + + nudb::detail::buffer compressBuf; + auto const compressed = nodeobjectCompress(blob.data(), blob.size(), compressBuf); + + EXPECT_EQ(codecType(compressed), kTypeInnerNodeFull); + EXPECT_EQ(compressed.second, sizeVarint(kTypeInnerNodeFull) + (kHashCount * kHashSize)); + + nudb::detail::buffer decompressBuf; + auto const restored = nodeobjectDecompress(compressed.first, compressed.second, decompressBuf); + + EXPECT_EQ(restored.second, blob.size()); + EXPECT_EQ(std::memcmp(restored.first, blob.data(), blob.size()), 0); +} + +// Some hash slots empty - "compressed v1 inner node" +TEST(Codec, inner_node_compressed_roundtrip) +{ + static constexpr std::uint8_t kTypeInnerNodeCompressed = 2; + static constexpr std::size_t kNonEmpty = 5; + auto const blob = makeInnerNode(kNonEmpty); + + nudb::detail::buffer compressBuf; + auto const compressed = nodeobjectCompress(blob.data(), blob.size(), compressBuf); + + EXPECT_EQ(codecType(compressed), kTypeInnerNodeCompressed); + EXPECT_EQ( + compressed.second, + sizeVarint(kTypeInnerNodeCompressed) + sizeof(std::uint16_t) + (kNonEmpty * kHashSize)); + EXPECT_LT(compressed.second, blob.size()); + + nudb::detail::buffer decompressBuf; + auto const restored = nodeobjectDecompress(compressed.first, compressed.second, decompressBuf); + + EXPECT_EQ(restored.second, blob.size()); + EXPECT_EQ(std::memcmp(restored.first, blob.data(), blob.size()), 0); +} + +// Anything that is not a v1 inner node - lz4 compressed +TEST(Codec, lz4_roundtrip) +{ + // A payload that is deliberately not a v1 inner node (any size other than 525), filled with a + // short repeating pattern so lz4 actually shrinks it. + static constexpr std::size_t kNonInnerNodeSize = 1000; + static constexpr std::size_t kBytePatternPeriod = 7; + static constexpr std::uint8_t kTypeLz4 = 1; + + std::vector blob(kNonInnerNodeSize); + for (auto i = 0uz; i < blob.size(); ++i) + blob[i] = static_cast(i % kBytePatternPeriod); + + nudb::detail::buffer compressBuf; + auto const compressed = nodeobjectCompress(blob.data(), blob.size(), compressBuf); + + EXPECT_EQ(codecType(compressed), kTypeLz4); + + nudb::detail::buffer decompressBuf; + auto const restored = nodeobjectDecompress(compressed.first, compressed.second, decompressBuf); + + EXPECT_EQ(restored.second, blob.size()); + EXPECT_EQ(std::memcmp(restored.first, blob.data(), blob.size()), 0); +} + +// An uncompressed blob is never produced by the compressor but must still decode: leading varint 0 +// followed by the raw payload. +TEST(Codec, uncompressed_passthrough) +{ + static constexpr std::uint8_t kTypeUncompressed = 0; + static constexpr auto payload = std::to_array({0xde, 0xad, 0xbe, 0xef, 0x2a}); + + std::vector blob; + blob.push_back(kTypeUncompressed); // leading varint type tag + blob.insert(blob.end(), payload.begin(), payload.end()); + + nudb::detail::buffer decompressBuf; + auto const restored = nodeobjectDecompress(blob.data(), blob.size(), decompressBuf); + + EXPECT_EQ(restored.second, payload.size()); + EXPECT_EQ(std::memcmp(restored.first, payload.data(), payload.size()), 0); +} diff --git a/src/tests/libxrpl/nodestore/Varint.cpp b/src/tests/libxrpl/nodestore/Varint.cpp new file mode 100644 index 0000000000..3652fd5631 --- /dev/null +++ b/src/tests/libxrpl/nodestore/Varint.cpp @@ -0,0 +1,46 @@ +#include + +#include + +#include +#include +#include +#include + +using namespace xrpl::node_store; + +TEST(Varint, encode_decode) +{ + static constexpr auto kValues = std::to_array({ + 0, + 1, + 2, + 126, + 127, + 128, + 253, + 254, + 255, + 16127, + 16128, + 16129, + 0xff, + 0xffff, + 0xffffffff, + 0xffffffffffffUL, + std::numeric_limits::max(), + }); + + for (auto const value : kValues) + { + std::array::kMax> buffer{}; + auto const bytesWritten = writeVarint(buffer.data(), value); + EXPECT_GT(bytesWritten, 0u); + EXPECT_EQ(bytesWritten, sizeVarint(value)); + + std::size_t decoded = 0; + auto const bytesRead = readVarint(buffer.data(), bytesWritten, decoded); + EXPECT_EQ(bytesRead, bytesWritten); + EXPECT_EQ(value, decoded); + } +} diff --git a/src/tests/libxrpl/nodestore/varint.cpp b/src/tests/libxrpl/nodestore/varint.cpp deleted file mode 100644 index fee96f314b..0000000000 --- a/src/tests/libxrpl/nodestore/varint.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include - -#include - -#include -#include -#include -#include -#include - -using namespace xrpl::node_store; - -TEST(varint, encode_decode) -{ - std::vector const kVALUES = { - 0, - 1, - 2, - 126, - 127, - 128, - 253, - 254, - 255, - 16127, - 16128, - 16129, - 0xff, - 0xffff, - 0xffffffff, - 0xffffffffffffUL, - 0xffffffffffffffffUL}; - - for (auto const v : kVALUES) - { - SCOPED_TRACE("value=" + std::to_string(v)); - std::array::kMax> vi{}; - auto const n0 = writeVarint(vi.data(), v); - EXPECT_GT(n0, 0u) << "write error"; - EXPECT_EQ(n0, sizeVarint(v)) << "size error"; - std::size_t v1 = 0; - auto const n1 = readVarint(vi.data(), n0, v1); - EXPECT_EQ(n1, n0) << "read error"; - EXPECT_EQ(v1, v) << "wrong value"; - } -} diff --git a/src/tests/libxrpl/protocol/ApiVersion.cpp b/src/tests/libxrpl/protocol/ApiVersion.cpp new file mode 100644 index 0000000000..8af7787102 --- /dev/null +++ b/src/tests/libxrpl/protocol/ApiVersion.cpp @@ -0,0 +1,26 @@ +#include + +#include + +using namespace xrpl; + +TEST(ApiVersion, invariants) +{ + static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumSupportedVersion); + static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumValidVersion); + static_assert(RPC::kApiMaximumSupportedVersion <= RPC::kApiMaximumValidVersion); + static_assert(RPC::kApiBetaVersion <= RPC::kApiMaximumValidVersion); +} + +// Update when we change versions +TEST(ApiVersion, versions) +{ + static_assert(RPC::kApiMinimumSupportedVersion >= 1); + static_assert(RPC::kApiMinimumSupportedVersion < 2); + static_assert(RPC::kApiMaximumSupportedVersion >= 2); + static_assert(RPC::kApiMaximumSupportedVersion < 3); + static_assert(RPC::kApiMaximumValidVersion >= 3); + static_assert(RPC::kApiMaximumValidVersion < 4); + static_assert(RPC::kApiBetaVersion >= 3); + static_assert(RPC::kApiBetaVersion < 4); +} diff --git a/src/tests/libxrpl/protocol/Serializer.cpp b/src/tests/libxrpl/protocol/Serializer.cpp new file mode 100644 index 0000000000..fc5742444a --- /dev/null +++ b/src/tests/libxrpl/protocol/Serializer.cpp @@ -0,0 +1,49 @@ +#include + +#include + +#include +#include +#include + +using namespace xrpl; + +TEST(Serializer, add32_roundtrip) +{ + static constexpr auto kValues = std::to_array({ + std::numeric_limits::min(), + -1, + 0, + 1, + std::numeric_limits::max(), + }); + + for (std::int32_t const value : kValues) + { + Serializer s; + s.add32(value); + EXPECT_EQ(s.size(), 4); + SerialIter sit(s.slice()); + EXPECT_EQ(sit.geti32(), value); + } +} + +TEST(Serializer, add64_roundtrip) +{ + static constexpr auto kValues = std::to_array({ + std::numeric_limits::min(), + -1, + 0, + 1, + std::numeric_limits::max(), + }); + + for (std::int64_t const value : kValues) + { + Serializer s; + s.add64(value); + EXPECT_EQ(s.size(), 8); + SerialIter sit(s.slice()); + EXPECT_EQ(sit.geti64(), value); + } +} From 2db631c915d6f2d3b8df812c122e97de368be2bd Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 27 Jul 2026 13:42:07 -0400 Subject: [PATCH 44/86] ci: Change `server_definitions` upload config name (#7878) --- .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 c1b8e4195b..3023f70cdf 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -266,7 +266,7 @@ jobs: ./xrpld --definitions | python3 -m json.tool >server_definitions.json - name: Upload server definitions - if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'debian-gcc-release-amd64' }} + if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'ubuntu-gcc-debug-amd64-coverage' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: server-definitions From ff7fff2cf2e92e99be02487eabba87c5bcab42fc Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Mon, 27 Jul 2026 19:39:53 +0100 Subject: [PATCH 45/86] style: Make clang-tidy format files using clang-format rules (#7880) --- .clang-tidy | 2 ++ .github/workflows/reusable-clang-tidy.yml | 2 +- CONTRIBUTING.md | 6 ++++-- bin/pre-commit/clang_tidy_check.py | 6 +++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 68fc9e75fc..41df5470ff 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -75,6 +75,8 @@ Checks: "-*, # readability-static-accessed-through-instance, # this check is probably unnecessary. It makes the code less readable # --- +FormatStyle: file + CheckOptions: bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index a6c1e6ae56..f1fdc0569a 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -95,7 +95,7 @@ jobs: TARGETS: ${{ needs.determine-files.outputs.need_full_run != 'true' && needs.determine-files.outputs.cpp_changed_files || 'include src tests' }} run: | set -o pipefail - run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}" + run-clang-tidy -j ${{ steps.nproc.outputs.nproc }} -p "${BUILD_DIR}" -quiet -fix -format -allow-no-checks ${TARGETS} 2>&1 | tee "${OUTPUT_FILE}" - name: Print filtered clang-tidy errors if: ${{ steps.run_clang_tidy.outcome != 'success' }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7632741e35..fc385cf6ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -348,12 +348,14 @@ run-clang-tidy -p build -allow-no-checks src tests ``` This will check all source files in the `src`, `include` and `tests` directories using the compile commands from your `build` directory. -If you wish to automatically fix whatever clang-tidy finds _and_ is capable of fixing, add `-fix` to the above command: +If you wish to automatically fix whatever clang-tidy finds _and_ is capable of fixing, add `-fix -format` to the above command: ``` -run-clang-tidy -p build -quiet -fix -allow-no-checks src tests +run-clang-tidy -p build -quiet -fix -format -allow-no-checks src tests ``` +`-format` reformats the fixed code with [`.clang-format`](./.clang-format); without it the fixes are inserted in LLVM style and the `clang-format` hook rewrites them afterwards. + ## Contracts and instrumentation We are using [Antithesis](https://antithesis.com/) for continuous fuzzing, diff --git a/bin/pre-commit/clang_tidy_check.py b/bin/pre-commit/clang_tidy_check.py index cf4808d2ea..118d9619e2 100755 --- a/bin/pre-commit/clang_tidy_check.py +++ b/bin/pre-commit/clang_tidy_check.py @@ -144,7 +144,11 @@ def main(): + files ) canonicalize_fix_paths(Path(fixes_dir)) - applied = subprocess.run([clang_apply_replacements, fixes_dir]) + # `FormatStyle` in .clang-tidy does not reach this path, + # so ask for the repository style here. + applied = subprocess.run( + [clang_apply_replacements, "--format", "--style=file", fixes_dir] + ) return result.returncode or applied.returncode From 86832edc70aff0eacedcfeda7260230da54439fb Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Mon, 27 Jul 2026 19:48:53 +0100 Subject: [PATCH 46/86] chore: Move semantic version tests to gtest (#7872) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- include/xrpl/beast/core/SemanticVersion.h | 6 +- src/libxrpl/beast/core/SemanticVersion.cpp | 4 +- src/test/beast/SemanticVersion_test.cpp | 266 ---------------- src/tests/libxrpl/beast/SemanticVersion.cpp | 333 ++++++++++++++++++++ 4 files changed, 338 insertions(+), 271 deletions(-) delete mode 100644 src/test/beast/SemanticVersion_test.cpp create mode 100644 src/tests/libxrpl/beast/SemanticVersion.cpp diff --git a/include/xrpl/beast/core/SemanticVersion.h b/include/xrpl/beast/core/SemanticVersion.h index 338942c252..c2e395e3f4 100644 --- a/include/xrpl/beast/core/SemanticVersion.h +++ b/include/xrpl/beast/core/SemanticVersion.h @@ -17,14 +17,14 @@ namespace beast { class SemanticVersion { public: - using identifier_list = std::vector; + using IdentifierList = std::vector; int majorVersion; int minorVersion; int patchVersion; - identifier_list preReleaseIdentifiers; - identifier_list metaData; + IdentifierList preReleaseIdentifiers; + IdentifierList metaData; SemanticVersion(); diff --git a/src/libxrpl/beast/core/SemanticVersion.cpp b/src/libxrpl/beast/core/SemanticVersion.cpp index a99437f8f2..f902a14b07 100644 --- a/src/libxrpl/beast/core/SemanticVersion.cpp +++ b/src/libxrpl/beast/core/SemanticVersion.cpp @@ -15,7 +15,7 @@ namespace beast { std::string -printIdentifiers(SemanticVersion::identifier_list const& list) +printIdentifiers(SemanticVersion::IdentifierList const& list) { std::string ret; @@ -115,7 +115,7 @@ extractIdentifier(std::string& value, bool allowLeadingZeroes, std::string& inpu bool extractIdentifiers( - SemanticVersion::identifier_list& identifiers, + SemanticVersion::IdentifierList& identifiers, bool allowLeadingZeroes, std::string& input) { diff --git a/src/test/beast/SemanticVersion_test.cpp b/src/test/beast/SemanticVersion_test.cpp deleted file mode 100644 index d7079080c8..0000000000 --- a/src/test/beast/SemanticVersion_test.cpp +++ /dev/null @@ -1,266 +0,0 @@ -#include -#include - -#include - -namespace beast { - -class SemanticVersion_test : public unit_test::Suite -{ - using identifier_list = SemanticVersion::identifier_list; - -public: - void - checkPass(std::string const& input, bool shouldPass = true) - { - SemanticVersion v; - - if (shouldPass) - { - BEAST_EXPECT(v.parse(input)); - BEAST_EXPECT(v.print() == input); - } - else - { - BEAST_EXPECT(!v.parse(input)); - } - } - - void - checkFail(std::string const& input) - { - checkPass(input, false); - } - - // check input and input with appended metadata - void - checkMeta(std::string const& input, bool shouldPass) - { - checkPass(input, shouldPass); - - checkPass(input + "+a", shouldPass); - checkPass(input + "+1", shouldPass); - checkPass(input + "+a.b", shouldPass); - checkPass(input + "+ab.cd", shouldPass); - - checkFail(input + "!"); - checkFail(input + "+"); - checkFail(input + "++"); - checkFail(input + "+!"); - checkFail(input + "+."); - checkFail(input + "+a.!"); - } - - void - checkMetaFail(std::string const& input) - { - checkMeta(input, false); - } - - // check input, input with appended release data, - // input with appended metadata, and input with both - // appended release data and appended metadata - // - void - checkRelease(std::string const& input, bool shouldPass = true) - { - checkMeta(input, shouldPass); - - checkMeta(input + "-1", shouldPass); - checkMeta(input + "-a", shouldPass); - checkMeta(input + "-a1", shouldPass); - checkMeta(input + "-a1.b1", shouldPass); - checkMeta(input + "-ab.cd", shouldPass); - checkMeta(input + "--", shouldPass); - - checkMetaFail(input + "+"); - checkMetaFail(input + "!"); - checkMetaFail(input + "-"); - checkMetaFail(input + "-!"); - checkMetaFail(input + "-."); - checkMetaFail(input + "-a.!"); - checkMetaFail(input + "-0.a"); - } - - // Checks the major.minor.version string alone and with all - // possible combinations of release identifiers and metadata. - // - void - check(std::string const& input, bool shouldPass = true) - { - checkRelease(input, shouldPass); - } - - void - negcheck(std::string const& input) - { - check(input, false); - } - - void - testParse() - { - testcase("parsing"); - - check("0.0.0"); - check("1.2.3"); - check("2147483647.2147483647.2147483647"); // max int - - // negative values - negcheck("-1.2.3"); - negcheck("1.-2.3"); - negcheck("1.2.-3"); - - // missing parts - negcheck(""); - negcheck("1"); - negcheck("1."); - negcheck("1.2"); - negcheck("1.2."); - negcheck(".2.3"); - - // whitespace - negcheck(" 1.2.3"); - negcheck("1 .2.3"); - negcheck("1.2 .3"); - negcheck("1.2.3 "); - - // leading zeroes - negcheck("01.2.3"); - negcheck("1.02.3"); - negcheck("1.2.03"); - } - - static identifier_list - ids() - { - return identifier_list(); - } - - static identifier_list - ids(std::string const& s1) - { - identifier_list v; - v.push_back(s1); - return v; - } - - static identifier_list - ids(std::string const& s1, std::string const& s2) - { - identifier_list v; - v.push_back(s1); - v.push_back(s2); - return v; - } - - static identifier_list - ids(std::string const& s1, std::string const& s2, std::string const& s3) - { - identifier_list v; - v.push_back(s1); - v.push_back(s2); - v.push_back(s3); - return v; - } - - // Checks the decomposition of the input into appropriate values - void - checkValues( - std::string const& input, - int majorVersion, - int minorVersion, - int patchVersion, - identifier_list const& preReleaseIdentifiers = identifier_list(), - identifier_list const& metaData = identifier_list()) - { - SemanticVersion v; - - BEAST_EXPECT(v.parse(input)); - - BEAST_EXPECT(v.majorVersion == majorVersion); - BEAST_EXPECT(v.minorVersion == minorVersion); - BEAST_EXPECT(v.patchVersion == patchVersion); - - BEAST_EXPECT(v.preReleaseIdentifiers == preReleaseIdentifiers); - BEAST_EXPECT(v.metaData == metaData); - } - - void - testValues() - { - testcase("values"); - - checkValues("0.1.2", 0, 1, 2); - checkValues("1.2.3", 1, 2, 3); - checkValues("1.2.3-rc1", 1, 2, 3, ids("rc1")); - checkValues("1.2.3-rc1.debug", 1, 2, 3, ids("rc1", "debug")); - checkValues("1.2.3-rc1.debug.asm", 1, 2, 3, ids("rc1", "debug", "asm")); - checkValues("1.2.3+full", 1, 2, 3, ids(), ids("full")); - checkValues("1.2.3+full.prod", 1, 2, 3, ids(), ids("full", "prod")); - checkValues("1.2.3+full.prod.x86", 1, 2, 3, ids(), ids("full", "prod", "x86")); - checkValues( - "1.2.3-rc1.debug.asm+full.prod.x86", - 1, - 2, - 3, - ids("rc1", "debug", "asm"), - ids("full", "prod", "x86")); - } - - // makes sure the left version is less than the right - void - checkLessInternal(std::string const& lhs, std::string const& rhs) - { - SemanticVersion left; - SemanticVersion right; - - BEAST_EXPECT(left.parse(lhs)); - BEAST_EXPECT(right.parse(rhs)); - - BEAST_EXPECT(compare(left, left) == 0); - BEAST_EXPECT(compare(right, right) == 0); - BEAST_EXPECT(compare(left, right) < 0); - BEAST_EXPECT(compare(right, left) > 0); - - BEAST_EXPECT(left < right); - BEAST_EXPECT(right > left); - BEAST_EXPECT(left == left); - BEAST_EXPECT(right == right); - } - - void - checkLess(std::string const& lhs, std::string const& rhs) - { - checkLessInternal(lhs, rhs); - checkLessInternal(lhs + "+meta", rhs); - checkLessInternal(lhs, rhs + "+meta"); - checkLessInternal(lhs + "+meta", rhs + "+meta"); - } - - void - testCompare() - { - testcase("comparisons"); - - checkLess("1.0.0-alpha", "1.0.0-alpha.1"); - checkLess("1.0.0-alpha.1", "1.0.0-alpha.beta"); - checkLess("1.0.0-alpha.beta", "1.0.0-beta"); - checkLess("1.0.0-beta", "1.0.0-beta.2"); - checkLess("1.0.0-beta.2", "1.0.0-beta.11"); - checkLess("1.0.0-beta.11", "1.0.0-rc.1"); - checkLess("1.0.0-rc.1", "1.0.0"); - checkLess("0.9.9", "1.0.0"); - } - - void - run() override - { - testParse(); - testValues(); - testCompare(); - } -}; - -BEAST_DEFINE_TESTSUITE(SemanticVersion, beast, beast); -} // namespace beast diff --git a/src/tests/libxrpl/beast/SemanticVersion.cpp b/src/tests/libxrpl/beast/SemanticVersion.cpp new file mode 100644 index 0000000000..21b33c9476 --- /dev/null +++ b/src/tests/libxrpl/beast/SemanticVersion.cpp @@ -0,0 +1,333 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace { + +using IdentifierList = SemanticVersion::IdentifierList; + +// Version strings are not valid C++ identifiers, so squash their punctuation to +// turn one into a gtest parameter name. +std::string +identifierFor(std::string_view version) +{ + std::string name{version}; + std::ranges::replace_if( + name, [](char c) { return !std::isalnum(c, std::locale::classic()); }, '_'); + if (!name.empty() && std::isdigit(name.front(), std::locale::classic())) + name.insert(0, "v_"); + return name; +} + +// Pre-release and metadata suffixes, each applied to a "major.minor.patch" base. +// The valid ones leave a well-formed base well-formed; the invalid ones make any +// base malformed. +constexpr auto kValidPreRelease = + std::to_array({"", "-1", "-a", "-a1", "-a1.b1", "-ab.cd", "--"}); +constexpr auto kInvalidPreRelease = + std::to_array({"+", "!", "-", "-!", "-.", "-a.!", "-0.a"}); +constexpr auto kValidMetaData = std::to_array({"", "+a", "+1", "+a.b", "+ab.cd"}); +constexpr auto kInvalidMetaData = + std::to_array({"!", "+", "++", "+!", "+.", "+a.!"}); + +// Assembles base + preRelease + metaData and checks whether it parses. A version +// we accept must also round-trip through print(). +void +expectParse( + std::string_view base, + std::string_view preRelease, + std::string_view metaData, + bool shouldPass) +{ + auto const input = std::string{base}.append(preRelease).append(metaData); + SCOPED_TRACE(::testing::Message() << '"' << input << '"'); + + SemanticVersion v; + + if (shouldPass) + { + EXPECT_TRUE(v.parse(input)); + EXPECT_EQ(v.print(), input); + } + else + { + EXPECT_FALSE(v.parse(input)); + } +} + +struct ParseCase +{ + std::string_view testName; + std::string_view base; + bool shouldPass; +}; + +std::string +parseCaseName(::testing::TestParamInfo const& info) +{ + return std::string{info.param.testName}; +} + +constexpr auto kParseCases = std::to_array({ + {.testName = "zeroes", .base = "0.0.0", .shouldPass = true}, + {.testName = "simple", .base = "1.2.3", .shouldPass = true}, + {.testName = "max_int", .base = "2147483647.2147483647.2147483647", .shouldPass = true}, + + // negative values + {.testName = "negative_major", .base = "-1.2.3", .shouldPass = false}, + {.testName = "negative_minor", .base = "1.-2.3", .shouldPass = false}, + {.testName = "negative_patch", .base = "1.2.-3", .shouldPass = false}, + + // missing parts + {.testName = "empty", .base = "", .shouldPass = false}, + {.testName = "major_only", .base = "1", .shouldPass = false}, + {.testName = "major_then_dot", .base = "1.", .shouldPass = false}, + {.testName = "major_and_minor", .base = "1.2", .shouldPass = false}, + {.testName = "major_minor_then_dot", .base = "1.2.", .shouldPass = false}, + {.testName = "missing_major", .base = ".2.3", .shouldPass = false}, + + // whitespace + {.testName = "leading_space", .base = " 1.2.3", .shouldPass = false}, + {.testName = "space_after_major", .base = "1 .2.3", .shouldPass = false}, + {.testName = "space_after_minor", .base = "1.2 .3", .shouldPass = false}, + {.testName = "trailing_space", .base = "1.2.3 ", .shouldPass = false}, + + // leading zeroes + {.testName = "leading_zero_in_major", .base = "01.2.3", .shouldPass = false}, + {.testName = "leading_zero_in_minor", .base = "1.02.3", .shouldPass = false}, + {.testName = "leading_zero_in_patch", .base = "1.2.03", .shouldPass = false}, +}); + +struct ValuesCase +{ + std::string_view testName; + std::string_view input; + int majorVersion; + int minorVersion; + int patchVersion; + IdentifierList preReleaseIdentifiers{}; // NOLINT(readability-redundant-member-init) + IdentifierList metaData{}; // NOLINT(readability-redundant-member-init) +}; + +std::string +valuesCaseName(::testing::TestParamInfo const& info) +{ + return std::string{info.param.testName}; +} + +std::vector const kValuesCases{ + { + .testName = "zero_major", + .input = "0.1.2", + .majorVersion = 0, + .minorVersion = 1, + .patchVersion = 2, + }, + { + .testName = "simple", + .input = "1.2.3", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + }, + { + .testName = "one_pre_release_identifier", + .input = "1.2.3-rc1", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1"}, + }, + { + .testName = "two_pre_release_identifiers", + .input = "1.2.3-rc1.debug", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1", "debug"}, + }, + { + .testName = "three_pre_release_identifiers", + .input = "1.2.3-rc1.debug.asm", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1", "debug", "asm"}, + }, + { + .testName = "one_metadata_identifier", + .input = "1.2.3+full", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .metaData = {"full"}, + }, + { + .testName = "two_metadata_identifiers", + .input = "1.2.3+full.prod", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .metaData = {"full", "prod"}, + }, + { + .testName = "three_metadata_identifiers", + .input = "1.2.3+full.prod.x86", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .metaData = {"full", "prod", "x86"}, + }, + { + .testName = "pre_release_and_metadata", + .input = "1.2.3-rc1.debug.asm+full.prod.x86", + .majorVersion = 1, + .minorVersion = 2, + .patchVersion = 3, + .preReleaseIdentifiers = {"rc1", "debug", "asm"}, + .metaData = {"full", "prod", "x86"}, + }, +}; + +struct OrderCase +{ + std::string_view lesser; + std::string_view greater; +}; + +std::string +orderCaseName(::testing::TestParamInfo const& info) +{ + return identifierFor(info.param.lesser) + "_below_" + identifierFor(info.param.greater); +} + +constexpr auto kOrderCases = std::to_array({ + {.lesser = "1.0.0-alpha", .greater = "1.0.0-alpha.1"}, + {.lesser = "1.0.0-alpha.1", .greater = "1.0.0-alpha.beta"}, + {.lesser = "1.0.0-alpha.beta", .greater = "1.0.0-beta"}, + {.lesser = "1.0.0-beta", .greater = "1.0.0-beta.2"}, + {.lesser = "1.0.0-beta.2", .greater = "1.0.0-beta.11"}, + {.lesser = "1.0.0-beta.11", .greater = "1.0.0-rc.1"}, + {.lesser = "1.0.0-rc.1", .greater = "1.0.0"}, + {.lesser = "0.9.9", .greater = "1.0.0"}, +}); + +} // namespace + +class SemanticVersionParse : public ::testing::TestWithParam +{ +}; + +// Exercises the base string on its own and with every combination of appended +// pre-release identifiers and metadata. +TEST_P(SemanticVersionParse, pre_release_and_metadata_combinations) +{ + auto const& [testName, base, shouldPass] = GetParam(); + + for (auto const preRelease : kValidPreRelease) + { + for (auto const metaData : kValidMetaData) + expectParse(base, preRelease, metaData, shouldPass); + + for (auto const metaData : kInvalidMetaData) + expectParse(base, preRelease, metaData, false); + } + + // A malformed pre-release section poisons the whole string, whatever + // metadata follows it. + for (auto const preRelease : kInvalidPreRelease) + { + for (auto const metaData : kValidMetaData) + expectParse(base, preRelease, metaData, false); + + for (auto const metaData : kInvalidMetaData) + expectParse(base, preRelease, metaData, false); + } +} + +INSTANTIATE_TEST_SUITE_P( + Inputs, + SemanticVersionParse, + ::testing::ValuesIn(kParseCases), + parseCaseName); + +class SemanticVersionValues : public ::testing::TestWithParam +{ +}; + +TEST_P(SemanticVersionValues, decomposes_into_components) +{ + auto const& expected = GetParam(); + + SemanticVersion v; + EXPECT_TRUE(v.parse(expected.input)); + + EXPECT_EQ(v.majorVersion, expected.majorVersion); + EXPECT_EQ(v.minorVersion, expected.minorVersion); + EXPECT_EQ(v.patchVersion, expected.patchVersion); + + EXPECT_EQ(v.preReleaseIdentifiers, expected.preReleaseIdentifiers); + EXPECT_EQ(v.metaData, expected.metaData); +} + +INSTANTIATE_TEST_SUITE_P( + Inputs, + SemanticVersionValues, + ::testing::ValuesIn(kValuesCases), + valuesCaseName); + +class SemanticVersionOrder : public ::testing::TestWithParam +{ +}; + +TEST_P(SemanticVersionOrder, lesser_precedes_greater) +{ + auto const& [lesser, greater] = GetParam(); + + // Metadata takes no part in precedence, so attaching it to either side must + // leave the ordering untouched. + static constexpr auto kMetaData = std::to_array({"", "+meta"}); + + for (auto const lesserMetaData : kMetaData) + { + for (auto const greaterMetaData : kMetaData) + { + auto const lesserInput = std::string{lesser}.append(lesserMetaData); + auto const greaterInput = std::string{greater}.append(greaterMetaData); + SCOPED_TRACE( + ::testing::Message() << '"' << lesserInput << "\" < \"" << greaterInput << '"'); + + SemanticVersion lesserVersion; + SemanticVersion greaterVersion; + EXPECT_TRUE(lesserVersion.parse(lesserInput)); + EXPECT_TRUE(greaterVersion.parse(greaterInput)); + + EXPECT_EQ(compare(lesserVersion, lesserVersion), 0); + EXPECT_EQ(compare(greaterVersion, greaterVersion), 0); + EXPECT_LT(compare(lesserVersion, greaterVersion), 0); + EXPECT_GT(compare(greaterVersion, lesserVersion), 0); + + EXPECT_LT(lesserVersion, greaterVersion); + EXPECT_GT(greaterVersion, lesserVersion); + EXPECT_EQ(lesserVersion, lesserVersion); + EXPECT_EQ(greaterVersion, greaterVersion); + } + } +} + +INSTANTIATE_TEST_SUITE_P( + Pairs, + SemanticVersionOrder, + ::testing::ValuesIn(kOrderCases), + orderCaseName); + +} // namespace beast From e290005db5a43bc99e7a87e43c1cc337f5be2e70 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 28 Jul 2026 14:02:49 -0400 Subject: [PATCH 47/86] 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 48/86] 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 49/86] 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 6ddad54985b89320f6ee03ca1344be317dbcf4bf Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Wed, 29 Jul 2026 23:54:46 +0100 Subject: [PATCH 50/86] chore: Move lexical cast tests to gtest (#7873) --- include/xrpl/beast/core/LexicalCast.h | 16 +- src/test/beast/LexicalCast_test.cpp | 280 ------------------- src/tests/libxrpl/beast/LexicalCast.cpp | 339 ++++++++++++++++++++++++ 3 files changed, 347 insertions(+), 288 deletions(-) delete mode 100644 src/test/beast/LexicalCast_test.cpp create mode 100644 src/tests/libxrpl/beast/LexicalCast.cpp diff --git a/include/xrpl/beast/core/LexicalCast.h b/include/xrpl/beast/core/LexicalCast.h index 7cf21892bd..288c5d6673 100644 --- a/include/xrpl/beast/core/LexicalCast.h +++ b/include/xrpl/beast/core/LexicalCast.h @@ -58,7 +58,7 @@ struct LexicalCast "beast::LexicalCast can only be used with integral types"); template - bool + constexpr bool operator()(Integral& out, std::string_view in) const requires(std::is_integral_v && !std::is_same_v) { @@ -110,7 +110,7 @@ struct LexicalCast> { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, boost::core::basic_string_view in) const { return LexicalCast()(out, in); @@ -123,7 +123,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, std::string in) const { return LexicalCast()(out, in); @@ -136,7 +136,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, char const* in) const { XRPL_ASSERT(in, "beast::detail::LexicalCast(char const*) : non-null input"); @@ -151,7 +151,7 @@ struct LexicalCast { explicit LexicalCast() = default; - bool + constexpr bool operator()(Out& out, char* in) const { XRPL_ASSERT(in, "beast::detail::LexicalCast(char*) : non-null input"); @@ -177,7 +177,7 @@ struct BadLexicalCast : public std::bad_cast * @return `false` if there was a parsing or range error */ template -bool +constexpr bool lexicalCastChecked(Out& out, In in) { return detail::LexicalCast()(out, in); @@ -191,7 +191,7 @@ lexicalCastChecked(Out& out, In in) * @return The new type. */ template -Out +constexpr Out lexicalCastThrow(In in) { if (Out out; lexicalCastChecked(out, in)) @@ -207,7 +207,7 @@ lexicalCastThrow(In in) * @return The new type. */ template -Out +constexpr Out lexicalCast(In in, Out defaultValue = Out()) { if (Out out; lexicalCastChecked(out, in)) diff --git a/src/test/beast/LexicalCast_test.cpp b/src/test/beast/LexicalCast_test.cpp deleted file mode 100644 index b1d37daab8..0000000000 --- a/src/test/beast/LexicalCast_test.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -namespace beast { - -class LexicalCast_test : public unit_test::Suite -{ -public: - template - static IntType - nextRandomInt(xor_shift_engine& r) - { - return static_cast(r()); - } - - template - void - testInteger(IntType in) - { - std::string s; - auto out = static_cast(~in); // Ensure out != in - - expect(lexicalCastChecked(s, in)); - expect(lexicalCastChecked(out, s)); - expect(out == in); - } - - template - void - testIntegers(xor_shift_engine& r) - { - { - std::stringstream ss; - ss << "random " << typeid(IntType).name(); - testcase(ss.str()); - - for (int i = 0; i < 1000; ++i) - { - auto const value = nextRandomInt(r); - testInteger(value); - } - } - - { - std::stringstream ss; - ss << "numeric_limits <" << typeid(IntType).name() << ">"; - testcase(ss.str()); - - testInteger(std::numeric_limits::min()); - testInteger(std::numeric_limits::max()); - } - } - - void - testPathologies() - { - testcase("pathologies"); - try - { - lexicalCastThrow("\xef\xbc\x91\xef\xbc\x90"); // utf-8 encoded - } - catch (BadLexicalCast const&) - { - pass(); - } - } - - template - void - tryBadConvert(std::string const& s) - { - T out; - expect(!lexicalCastChecked(out, s), s); - } - - void - testConversionOverflows() - { - testcase("conversion overflows"); - - tryBadConvert("99999999999999999999"); - tryBadConvert("4294967300"); - tryBadConvert("75821"); - } - - void - testConversionUnderflows() - { - testcase("conversion underflows"); - - tryBadConvert("-1"); - - tryBadConvert("-99999999999999999999"); - tryBadConvert("-4294967300"); - tryBadConvert("-75821"); - } - - template - bool - tryEdgeCase(std::string const& s) - { - T ret; - - bool const result = lexicalCastChecked(ret, s); - - if (!result) - return false; - - return s == std::to_string(ret); - } - - void - testEdgeCases() - { - testcase("conversion edge cases"); - - expect(tryEdgeCase("18446744073709551614")); - expect(tryEdgeCase("18446744073709551615")); - expect(!tryEdgeCase("18446744073709551616")); - - expect(tryEdgeCase("9223372036854775806")); - expect(tryEdgeCase("9223372036854775807")); - expect(!tryEdgeCase("9223372036854775808")); - - expect(tryEdgeCase("-9223372036854775807")); - expect(tryEdgeCase("-9223372036854775808")); - expect(!tryEdgeCase("-9223372036854775809")); - - expect(tryEdgeCase("4294967294")); - expect(tryEdgeCase("4294967295")); - expect(!tryEdgeCase("4294967296")); - - expect(tryEdgeCase("2147483646")); - expect(tryEdgeCase("2147483647")); - expect(!tryEdgeCase("2147483648")); - - expect(tryEdgeCase("-2147483647")); - expect(tryEdgeCase("-2147483648")); - expect(!tryEdgeCase("-2147483649")); - - expect(tryEdgeCase("65534")); - expect(tryEdgeCase("65535")); - expect(!tryEdgeCase("65536")); - - expect(tryEdgeCase("32766")); - expect(tryEdgeCase("32767")); - expect(!tryEdgeCase("32768")); - - expect(tryEdgeCase("-32767")); - expect(tryEdgeCase("-32768")); - expect(!tryEdgeCase("-32769")); - } - - template - void - testThrowConvert(std::string const& s, bool success) - { - bool result = !success; - T out; - - try - { - out = lexicalCastThrow(s); - result = true; - } - catch (BadLexicalCast const&) - { - result = false; - } - - expect(result == success, s); - } - - void - testThrowingConversions() - { - testcase("throwing conversion"); - - testThrowConvert("99999999999999999999", false); - testThrowConvert("9223372036854775806", true); - - testThrowConvert("4294967290", true); - testThrowConvert("42949672900", false); - testThrowConvert("429496729000", false); - testThrowConvert("4294967290000", false); - - testThrowConvert("5294967295", false); - testThrowConvert("-2147483644", true); - - testThrowConvert("66666", false); - testThrowConvert("-5711", true); - } - - void - testZero() - { - testcase("zero conversion"); - - { - std::int32_t out = 0; - - expect(lexicalCastChecked(out, "-0"), "0"); - expect(lexicalCastChecked(out, "0"), "0"); - expect(lexicalCastChecked(out, "+0"), "0"); - } - - { - std::uint32_t out = 0; - - expect(!lexicalCastChecked(out, "-0"), "0"); - expect(lexicalCastChecked(out, "0"), "0"); - expect(lexicalCastChecked(out, "+0"), "0"); - } - } - - void - testEntireRange() - { - testcase("entire range"); - - std::int32_t i = std::numeric_limits::min(); - std::string const empty; - - while (i <= std::numeric_limits::max()) - { - auto const j = static_cast(i); - - auto actual = std::to_string(j); - - auto result = lexicalCast(j, empty); - - expect(result == actual, actual + " (string to integer)"); - - if (result == actual) - { - auto number = lexicalCast(result); - - if (number != j) - expect(false, actual + " (integer to string)"); - } - - i++; - } - } - - void - run() override - { - std::int64_t const seedValue = 50; - - xor_shift_engine r(seedValue); - - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - testIntegers(r); - - testPathologies(); - testConversionOverflows(); - testConversionUnderflows(); - testThrowingConversions(); - testZero(); - testEdgeCases(); - testEntireRange(); - } -}; - -BEAST_DEFINE_TESTSUITE(LexicalCast, beast, beast); - -} // namespace beast diff --git a/src/tests/libxrpl/beast/LexicalCast.cpp b/src/tests/libxrpl/beast/LexicalCast.cpp new file mode 100644 index 0000000000..d18af4e1cd --- /dev/null +++ b/src/tests/libxrpl/beast/LexicalCast.cpp @@ -0,0 +1,339 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace { + +template +[[nodiscard]] constexpr bool +parses(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text); +} + +template +[[nodiscard]] constexpr T +parsed(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text) ? out : T{}; +} + +template +constexpr T kMax = std::numeric_limits::max(); + +template +constexpr T kMin = std::numeric_limits::min(); + +template +constexpr T kUnderMax = kMax - 1; + +template +constexpr T kOverMin = kMin + 1; + +// Comfortably inside the range, not boundary values. +constexpr auto kNearMax32 = kMax - 5; +constexpr auto kNearMin32 = kMin + 4; +constexpr auto kUnderInt64Max = uint64_t{kMax} - 1; +constexpr auto kInRangeInt16 = int16_t{-5711}; + +// No wider integer type can hold these, so ToString cannot produce them. +constexpr auto kAboveUint64Max = "18446744073709551616"; +constexpr auto kBelowInt64Min = "-9223372036854775809"; + +// Out of range for every integer type we test. +constexpr auto kTwentyNines = "99999999999999999999"; +constexpr auto kNegativeTwentyNines = "-99999999999999999999"; + +// Arbitrary values chosen to sit well outside a type's range, not just over it. +constexpr auto kAboveUint16Max = "75821"; +constexpr auto kBelowInt16Min = "-75821"; +constexpr auto kAboveInt32Max = "5294967295"; +constexpr auto kAboveInt16Max = "66666"; + +constexpr auto kPositiveInt32 = int32_t{42}; +constexpr auto kNegativeInt32 = int32_t{-42}; + +constexpr auto kPositiveInt32Text = "+42"; +constexpr auto kNegativeInt32Text = "-42"; + +constexpr auto kNegativeOne = "-1"; +constexpr auto kNegativeZero = "-0"; +constexpr auto kBareZero = "0"; +constexpr auto kPositiveZero = "+0"; + +// Full-width digits one and zero, not ASCII ones. +constexpr std::string_view kFullWidthDigits = "\xef\xbc\x91\xef\xbc\x90"; + +// The decimal text of a value, usable in a constant expression. +template +struct ToString +{ + std::array buffer{}; + std::size_t length{}; + + constexpr explicit ToString(T value) + { + auto const result = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + length = static_cast(result.ptr - buffer.data()); + } + + constexpr + operator std::string_view() const + { + return {buffer.data(), length}; + } +}; + +template +constexpr auto kMaxText = ToString{kMax}; + +template +constexpr auto kUnderMaxText = ToString{kUnderMax}; + +template +constexpr auto kOverMaxText = ToString{Wider{kMax} + 1}; + +template +constexpr auto kMinText = ToString{kMin}; + +template +constexpr auto kOverMinText = ToString{kOverMin}; + +template +constexpr auto kUnderMinText = ToString{Wider{kMin} - 1}; + +constexpr auto kOverUint32MaxText = ToString{uint64_t{kMax} + 5}; +constexpr auto kNegatedOverUint32MaxText = ToString{-(int64_t{kMax} + 5)}; + +// lexicalCastThrow deduces its input type, so the text has to be an explicit +// string_view rather than a ToString. +template +[[nodiscard]] constexpr T +castThrow(Value value) +{ + return lexicalCastThrow(std::string_view{ToString{value}}); +} + +template +[[nodiscard]] bool +roundTrips(std::string_view text) +{ + T out{}; + return lexicalCastChecked(out, text) && std::to_string(out) == text; +} + +template +void +expectRoundTrip(T value) +{ + SCOPED_TRACE(::testing::Message() << "value: " << value); + + auto const text = lexicalCast(value); + EXPECT_EQ(text, std::to_string(value)); + + auto decoded = static_cast(~value); // ensure decoded != value + EXPECT_TRUE(lexicalCastChecked(decoded, text)); + EXPECT_EQ(decoded, value); +} + +} // namespace + +// int/unsigned/short/unsigned short are covered by the list below — they are +// these exact types everywhere we build. +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +using IntegerTypes = ::testing::Types< // + int16_t, + uint16_t, + int32_t, + uint32_t, + int64_t, + uint64_t>; + +struct IntegerTypeNames +{ + template + static std::string + // NOLINTNEXTLINE(readability-identifier-naming) - required by gtest + GetName(int) + { + return (std::is_signed_v ? "int" : "uint") + std::to_string(sizeof(T) * 8) + "_t"; + } +}; + +template +class LexicalCastIntegers : public ::testing::Test +{ +}; + +TYPED_TEST_SUITE(LexicalCastIntegers, IntegerTypes, IntegerTypeNames); + +TYPED_TEST(LexicalCastIntegers, round_trips_random_values) +{ + static constexpr auto kSampleCount = 1000uz; + + xor_shift_engine r{50}; // seeded per test so a failure reproduces on its own + + for (auto i = 0uz; i < kSampleCount; ++i) + expectRoundTrip(static_cast(r())); +} + +TYPED_TEST(LexicalCastIntegers, round_trips_numeric_limits) +{ + expectRoundTrip(std::numeric_limits::min()); + expectRoundTrip(std::numeric_limits::max()); +} + +TEST(LexicalCast, round_trips_every_int16_value) +{ + for (int32_t i = kMin; i <= kMax; ++i) + { + auto const value = static_cast(i); + + // ASSERT, or a broken cast reports all 65536 iterations. + auto const text = lexicalCast(value); + ASSERT_EQ(text, std::to_string(value)); + ASSERT_EQ(lexicalCast(text), value); + } +} + +TEST(LexicalCast, rejects_overflow) +{ + static_assert(not parses(kOverUint32MaxText)); + static_assert(not parses(kTwentyNines)); + static_assert(not parses(kAboveUint16Max)); +} + +TEST(LexicalCast, rejects_underflow) +{ + static_assert(not parses(kNegativeOne)); + static_assert(not parses(kNegatedOverUint32MaxText)); + static_assert(not parses(kNegativeTwentyNines)); + static_assert(not parses(kBelowInt16Min)); +} + +TEST(LexicalCast, accepts_up_to_the_maximum) +{ + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kOverMaxText)); + + static_assert(parsed(kUnderMaxText) == kUnderMax); + static_assert(parsed(kMaxText) == kMax); + static_assert(not parses(kAboveUint64Max)); +} + +TEST(LexicalCast, accepts_down_to_the_minimum) +{ + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kUnderMinText)); + + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kUnderMinText)); + + static_assert(parsed(kOverMinText) == kOverMin); + static_assert(parsed(kMinText) == kMin); + static_assert(not parses(kBelowInt64Min)); +} + +TEST(LexicalCast, limits_round_trip_through_to_string) +{ + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); + EXPECT_TRUE(roundTrips(kMaxText)); + EXPECT_TRUE(roundTrips(kMinText)); +} + +TEST(LexicalCast, accepts_signed_zero_in_every_form) +{ + static_assert(parsed(kNegativeZero) == 0); + static_assert(parsed(kBareZero) == 0); + static_assert(parsed(kPositiveZero) == 0); +} + +TEST(LexicalCast, rejects_negative_zero_when_unsigned) +{ + static_assert(not parses(kNegativeZero)); + static_assert(parsed(kBareZero) == 0); + static_assert(parsed(kPositiveZero) == 0); +} + +TEST(LexicalCast, accepts_char_pointer_and_std_string_input) +{ + int32_t fromLiteral = 0; + EXPECT_TRUE(lexicalCastChecked(fromLiteral, kPositiveInt32Text)); + EXPECT_EQ(fromLiteral, kPositiveInt32); + + int32_t fromString = 0; + EXPECT_TRUE(lexicalCastChecked(fromString, std::string{kNegativeInt32Text})); + EXPECT_EQ(fromString, kNegativeInt32); +} + +TEST(LexicalCast, throwing_cast_returns_in_range_values) +{ + static_assert(castThrow(kUnderInt64Max) == kUnderInt64Max); + static_assert(castThrow(kNearMax32) == kNearMax32); + static_assert(castThrow(kNearMin32) == kNearMin32); + static_assert(castThrow(kInRangeInt16) == kInRangeInt16); +} + +TEST(LexicalCast, throwing_cast_throws_on_out_of_range) +{ + EXPECT_THROW(lexicalCastThrow(kTwentyNines), BadLexicalCast); + + // kNearMax32 with digits appended, so each is further past uint32_t's range. + for (auto const scale : {10, 100, 1000}) + { + auto const tooBig = ToString{uint64_t{kNearMax32} * scale}; + EXPECT_THROW(lexicalCastThrow(std::string_view{tooBig}), BadLexicalCast); + } + + EXPECT_THROW(lexicalCastThrow(kAboveInt32Max), BadLexicalCast); + EXPECT_THROW(lexicalCastThrow(kAboveInt16Max), BadLexicalCast); +} + +// Full-width digits, not ASCII ones. +TEST(LexicalCast, throwing_cast_throws_on_utf8_digits) +{ + EXPECT_THROW(lexicalCastThrow(kFullWidthDigits), BadLexicalCast); +} + +} // namespace beast From 8a5eded4f10ef7df02b9d5135fa5a7837f9070f8 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Thu, 30 Jul 2026 13:55:39 +0200 Subject: [PATCH 51/86] feat: Implement LoanBroker cash-basis accounting (#7817) --- include/xrpl/ledger/helpers/LendingHelpers.h | 71 ++ include/xrpl/ledger/helpers/VaultHelpers.h | 16 + include/xrpl/protocol/Protocol.h | 11 + .../xrpl/protocol/detail/ledger_entries.macro | 1 + include/xrpl/protocol/detail/sfields.macro | 1 + .../protocol_autogen/ledger_entries/Vault.h | 35 + src/libxrpl/ledger/helpers/LendingHelpers.cpp | 122 ++ src/libxrpl/ledger/helpers/VaultHelpers.cpp | 20 + .../tx/transactors/lending/LoanManage.cpp | 23 +- .../tx/transactors/lending/LoanPay.cpp | 43 +- .../tx/transactors/lending/LoanSet.cpp | 15 +- .../tx/transactors/vault/VaultCreate.cpp | 3 + src/test/app/LendingHelpers_test.cpp | 333 ++++++ src/test/app/Loan_test.cpp | 1011 ++++++++++++++++- src/test/app/Vault_test.cpp | 78 ++ .../ledger_entries/VaultTests.cpp | 27 + 16 files changed, 1749 insertions(+), 61 deletions(-) diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index 8e0d11cccb..fef18e3e09 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -286,6 +286,77 @@ computeFullPaymentInterest( std::uint32_t startDate, TenthBips32 closeInterestRate); +// Deltas applied to Vault.AssetsTotal and LoanBroker.DebtTotal at a single +// accounting touch point (origination, payment, impair/unimpair/default). +struct AccountingDeltas +{ + Number assetsTotalDelta; + Number debtTotalDelta; +}; + +// Whole-life (pre-LendingProtocolV1_1) recognition model: interest is +// recognized into AssetsTotal/DebtTotal up front, at origination. +namespace Accrual { + +// LoanSet origination: what's added to Vault.AssetsTotal and LoanBroker.DebtTotal +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested, Number const& interestDue); + +// LoanSet origination: would recognizing this loan's interest push +// Vault.AssetsTotal past Vault.AssetsMaximum? +bool +loanOriginationExceedsVaultMaximum( + Number const& vaultMaximum, + Number const& vaultTotal, + Number const& interestDue); + +// LoanManage impair/unimpair/default: the vault's exposure to this loan +Number +loanVaultExposure(SLE::const_ref loanSle); + +// LoanPay: what's added to Vault.AssetsTotal and subtracted from LoanBroker.DebtTotal for a payment +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts); + +} // namespace Accrual + +// Cash-basis (LendingProtocolV1_1) recognition model: AssetsTotal/DebtTotal +// are principal-only, interest is recognized only as it's actually paid. +namespace CashBasis { + +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested); + +Number +loanVaultExposure(SLE::const_ref loanSle); + +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts); + +} // namespace CashBasis + +// Public dispatchers: pick CashBasis:: if featureLendingProtocolV1_1 is +// enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is +// VaultVersion::CashBasis, else Accrual::. These are the only entry points +// transactors call. +AccountingDeltas +loanOriginationDeltas( + SLE::const_ref vaultSle, + Number const& principalRequested, + Number const& interestDue); + +bool +loanOriginationExceedsVaultMaximum( + SLE::const_ref vaultSle, + Number const& vaultTotal, + Number const& interestDue); + +Number +loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle); + +AccountingDeltas +loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const& parts); + namespace detail { // These classes and functions should only be accessed by LendingHelper // functions and unit tests diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 1bd1663314..5681cc57e8 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -107,4 +108,19 @@ sharesToAssetsWithdraw( [[nodiscard]] bool isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref issuance); +/** + * Resolves a Vault's LEVersion, the single point every accounting touch + * point should call to determine which recognition model (accrual vs. + * cash-basis) a Vault uses. Vaults created before featureLendingProtocolV1_1 + * activated never have sfLEVersion set, which resolves here to + * VaultVersion::Legacy. + * + * @param vault The vault SLE. + * + * @return The Vault's LEVersion, or VaultVersion::Legacy if the field is + * absent. + */ +[[nodiscard]] VaultVersion +getVaultVersion(SLE::const_ref vault); + } // namespace xrpl diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index e83e1c97b6..9938a9b768 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -316,6 +316,17 @@ constexpr std::uint8_t kVaultDefaultIouScale = 6; */ constexpr std::uint8_t kVaultMaximumIouScale = 18; +/** + * Vault ledger-entry schema versions. Assigned to newly created + * Vaults once featureLendingProtocolV1_1 is enabled. Vaults created before + * activation are left without LEVersion (implicit legacy version 0, + * accrual-basis accounting). + */ +enum class VaultVersion : uint8_t { + Legacy = 0, + CashBasis, +}; + /** * Maximum recursion depth for vault shares being put as an asset inside * another vault; counted from 0 diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 90810e06d2..b6408581a9 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -505,6 +505,7 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfShareMPTID, SoeRequired}, {sfWithdrawalPolicy, SoeRequired}, {sfScale, SoeDefault}, + {sfLEVersion, SoeDefault}, // no SharesTotal ever (use MPTIssuance.sfOutstandingAmount) // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 4ef76c8b75..16defe3ba3 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -18,6 +18,7 @@ TYPED_SFIELD(sfMethod, UINT8, 2) TYPED_SFIELD(sfTransactionResult, UINT8, 3) TYPED_SFIELD(sfScale, UINT8, 4) TYPED_SFIELD(sfAssetScale, UINT8, 5) +TYPED_SFIELD(sfLEVersion, UINT8, 6) // 8-bit integers (uncommon) TYPED_SFIELD(sfTickSize, UINT8, 16) diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h b/include/xrpl/protocol_autogen/ledger_entries/Vault.h index 2bf92b4f5d..a6ab54cb0a 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Vault.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h @@ -287,6 +287,30 @@ public: { return this->sle_->isFieldPresent(sfScale); } + + /** + * @brief Get sfLEVersion (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getLEVersion() const + { + if (hasLEVersion()) + return this->sle_->at(sfLEVersion); + return std::nullopt; + } + + /** + * @brief Check if sfLEVersion is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasLEVersion() const + { + return this->sle_->isFieldPresent(sfLEVersion); + } }; /** @@ -508,6 +532,17 @@ public: return *this; } + /** + * @brief Set sfLEVersion (SoeDefault) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setLEVersion(std::decay_t const& value) + { + object_[sfLEVersion] = value; + return *this; + } + /** * @brief Build and return the completed Vault wrapper. * @param index The ledger entry index. diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index e6c3d632c1..dac2c67181 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -130,6 +131,127 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale) roundToAsset(asset, value, scale, Number::RoundingMode::Upward); } +namespace Accrual { + +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested, Number const& interestDue) +{ + return {.assetsTotalDelta = interestDue, .debtTotalDelta = principalRequested + interestDue}; +} + +bool +loanOriginationExceedsVaultMaximum( + Number const& vaultMaximum, + Number const& vaultTotal, + Number const& interestDue) +{ + return vaultMaximum != 0 && interestDue > vaultMaximum - vaultTotal; +} + +/* +XLS-66 section 3.2.3.2, defines the default amount as + +DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) + +Which is equivalent to (Loan.TotalValueOutstanding - Loan.ManagementFeeOutstanding) +*/ +Number +loanVaultExposure(SLE::const_ref loanSle) +{ + return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfManagementFeeOutstanding); +} + +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts) +{ + return { + .assetsTotalDelta = parts.valueChange, + .debtTotalDelta = (parts.principalPaid + parts.interestPaid) - parts.valueChange}; +} + +} // namespace Accrual + +namespace CashBasis { + +AccountingDeltas +loanOriginationDeltas(Number const& principalRequested) +{ + return {.assetsTotalDelta = kNumZero, .debtTotalDelta = principalRequested}; +} + +/* + * Under CashBasis accounting, Loan default amount is: + * + * DefaultAmount = Loan.PrincipalOutstanding + */ +Number +loanVaultExposure(SLE::const_ref loanSle) +{ + return loanSle->at(sfPrincipalOutstanding); +} + +AccountingDeltas +loanPaymentDeltas(LoanPaymentParts const& parts) +{ + return {.assetsTotalDelta = parts.interestPaid, .debtTotalDelta = parts.principalPaid}; +} + +} // namespace CashBasis + +namespace { + +// Cash-basis accounting applies only when featureLendingProtocolV1_1 is +// enabled AND the specific Vault was created under it (LEVersion == +// VaultVersion::CashBasis). Vaults created before activation keep accrual-basis +// accounting forever, even after the amendment later turns on. +bool +cashBasisEnabled(SLE::const_ref vaultSle) +{ + return getVaultVersion(vaultSle) == VaultVersion::CashBasis; +} + +} // namespace + +AccountingDeltas +loanOriginationDeltas( + SLE::const_ref vaultSle, + Number const& principalRequested, + Number const& interestDue) +{ + return cashBasisEnabled(vaultSle) + ? CashBasis::loanOriginationDeltas(principalRequested) + : Accrual::loanOriginationDeltas(principalRequested, interestDue); +} + +bool +loanOriginationExceedsVaultMaximum( + SLE::const_ref vaultSle, + Number const& vaultTotal, + Number const& interestDue) +{ + // Cash-basis origination doesn't recognize interest into AssetsTotal, so + // interest due can never push the vault past AssetsMaximum at origination. + if (cashBasisEnabled(vaultSle)) + return false; + + auto const vaultMaximum = vaultSle->at(sfAssetsMaximum); + return Accrual::loanOriginationExceedsVaultMaximum(vaultMaximum, vaultTotal, interestDue); +} + +Number +loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle) +{ + return cashBasisEnabled(vaultSle) ? CashBasis::loanVaultExposure(loanSle) + : Accrual::loanVaultExposure(loanSle); +} + +AccountingDeltas +loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const& parts) +{ + return cashBasisEnabled(vaultSle) ? CashBasis::loanPaymentDeltas(parts) + : Accrual::loanPaymentDeltas(parts); +} + namespace detail { void diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp index b5b076d1cb..78f64d2077 100644 --- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp @@ -6,6 +6,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include @@ -13,6 +14,7 @@ #include #include +#include namespace xrpl { @@ -137,4 +139,22 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref return sleToken->getFieldU64(sfMPTAmount) == outstanding; } +[[nodiscard]] VaultVersion +getVaultVersion(SLE::const_ref vault) +{ + XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultVersion : valid Vault sle"); + if (!vault->isFieldPresent(sfLEVersion)) + return VaultVersion::Legacy; + + auto const version = vault->at(sfLEVersion); + if (version > std::to_underlying(VaultVersion::CashBasis)) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::getVaultVersion : invalid vault version"); + return VaultVersion::Legacy; + // LCOV_EXCL_STOP + } + return static_cast(version); +} + } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index a0aa948876..a312dba3b3 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -127,23 +127,6 @@ LoanManage::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -static Number -owedToVault(SLE::ref loanSle) -{ - // Spec section 3.2.3.2, defines the default amount as - // - // DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding) - // - // Loan.InterestOutstanding is not stored directly on ledger. - // It is computed as - // - // Loan.TotalValueOutstanding - Loan.PrincipalOutstanding - - // Loan.ManagementFeeOutstanding - // - // Add that to the original formula, and you get this: - return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfManagementFeeOutstanding); -} - TER LoanManage::defaultLoan( ApplyView& view, @@ -158,7 +141,7 @@ LoanManage::defaultLoan( std::int32_t const loanScale = loanSle->at(sfLoanScale); auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); - Number const totalDefaultAmount = owedToVault(loanSle); + Number const totalDefaultAmount = loanVaultExposure(vaultSle, loanSle); // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; @@ -304,7 +287,7 @@ LoanManage::impairLoan( Asset const& vaultAsset, beast::Journal j) { - Number const lossUnrealized = owedToVault(loanSle); + Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle); // The vault may be at a different scale than the loan. Reduce rounding // errors during the accounting by rounding some of the values to that @@ -353,7 +336,7 @@ LoanManage::unimpairLoan( // Update the Vault object(clear "paper loss") auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized); - Number const lossReversed = owedToVault(loanSle); + Number const lossReversed = loanVaultExposure(vaultSle, loanSle); if (vaultLossUnrealizedProxy < lossReversed) { // LCOV_EXCL_START diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index 54ee85b186..0053ed496e 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -420,10 +420,13 @@ LoanPay::doApply() // LCOV_EXCL_STOP } + auto const [assetsTotalDelta, debtTotalDelta] = loanPaymentDeltas(vaultSle, *paymentParts); + JLOG(j_.debug()) << "Loan Pay: principal paid: " << paymentParts->principalPaid << ", interest paid: " << paymentParts->interestPaid << ", fee paid: " << paymentParts->feePaid - << ", value change: " << paymentParts->valueChange; + << ", assets total delta: " << assetsTotalDelta + << ", debt total delta: " << debtTotalDelta; //------------------------------------------------------ // LoanBroker object state changes @@ -439,13 +442,6 @@ LoanPay::doApply() !asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded, "xrpl::LoanPay::doApply", "rounding does nothing for integral asset"); - // Account for value changes when reducing the broker's debt: - // - Positive value change (from full/late/overpayments): Subtract from the - // amount credited toward debt to avoid over-reducing the debt. - // - Negative value change (from full/overpayments): Add to the amount - // credited toward debt,effectively increasing the debt reduction. - auto const totalPaidToVaultForDebt = totalPaidToVaultRaw - paymentParts->valueChange; - auto const totalPaidToBroker = paymentParts->feePaid; XRPL_ASSERT_PARTS( @@ -455,16 +451,16 @@ LoanPay::doApply() "payments add up"); // Decrease LoanBroker Debt by the amount paid, add the Loan value change - // (which might be negative). totalPaidToVaultForDebt may be negative, - // increasing the debt + // (which might be negative). debtTotalDelta may be negative, increasing the + // debt XRPL_ASSERT_PARTS( - isRounded(asset, totalPaidToVaultForDebt, loanScale), + isRounded(asset, debtTotalDelta, loanScale), "xrpl::LoanPay::doApply", - "totalPaidToVaultForDebt rounding good"); + "debtTotalDelta rounding good"); // Despite our best efforts, it's possible for rounding errors to accumulate // in the loan broker's debt total. This is because the broker may have more // than one loan with significantly different scales. - adjustImpreciseNumber(debtTotalProxy, -totalPaidToVaultForDebt, asset, vaultScale); + adjustImpreciseNumber(debtTotalProxy, -debtTotalDelta, asset, vaultScale); //------------------------------------------------------ // Vault object state changes @@ -490,7 +486,7 @@ LoanPay::doApply() #endif assetsAvailableProxy += totalPaidToVaultRounded; - assetsTotalProxy += paymentParts->valueChange; + assetsTotalProxy += assetsTotalDelta; XRPL_ASSERT_PARTS( *assetsAvailableProxy <= *assetsTotalProxy, @@ -543,11 +539,11 @@ LoanPay::doApply() return tecPRECISION_LOSS; // LCOV_EXCL_STOP } - if (paymentParts->valueChange != beast::kZero && assetsTotalAfter == assetsTotalBefore) + if (assetsTotalDelta != beast::kZero && assetsTotalAfter == assetsTotalBefore) { - // Non-zero valueChange with an unchanged assetsTotal indicates that the - // actual value change rounded to zero. That should be impossible, but I - // can't rule it out for extreme edge cases, so fail gracefully if it + // Non-zero assetsTotalDelta with an unchanged assetsTotal indicates that + // the actual value change rounded to zero. That should be impossible, but + // I can't rule it out for extreme edge cases, so fail gracefully if it // happens. // // LCOV_EXCL_START @@ -555,20 +551,21 @@ LoanPay::doApply() << "LoanPay: Vault assets expected change, but unchanged after rounding: " // << "Before: " << assetsTotalBefore // << ", After: " << assetsTotalAfter // - << ", ValueChange: " << paymentParts->valueChange; + << ", AssetsTotalDelta: " << assetsTotalDelta; return tecPRECISION_LOSS; // LCOV_EXCL_STOP } - if (paymentParts->valueChange == beast::kZero && assetsTotalAfter != assetsTotalBefore) + if (assetsTotalDelta == beast::kZero && assetsTotalAfter != assetsTotalBefore) { - // A change in assetsTotal when there was no valueChange indicates that - // something really weird happened. That should be flat out impossible. + // A change in assetsTotal when there was no assetsTotalDelta indicates + // that something really weird happened. That should be flat out + // impossible. // // LCOV_EXCL_START JLOG(j_.fatal()) << "LoanPay: Vault assets changed unexpectedly after rounding: " // << "Before: " << assetsTotalBefore // << ", After: " << assetsTotalAfter // - << ", ValueChange: " << paymentParts->valueChange; + << ", AssetsTotalDelta: " << assetsTotalDelta; return tecINTERNAL; // LCOV_EXCL_STOP } diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index 694d01c69f..bafadd7c1d 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -439,12 +439,12 @@ LoanSet::doApply() principalRequested, properties.loanState.managementFeeDue); - auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum); XRPL_ASSERT_PARTS( - vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy, + *vaultSle->at(sfAssetsMaximum) == 0 || *vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy, "xrpl::LoanSet::doApply", "Vault is below maximum limit"); - if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy) + + if (loanOriginationExceedsVaultMaximum(vaultSle, vaultTotalProxy, state.interestDue)) { JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault"; return tecLIMIT_EXCEEDED; @@ -490,8 +490,9 @@ LoanSet::doApply() auto const loanAssetsToBorrower = principalRequested - originationFee; - auto const newDebtDelta = principalRequested + state.interestDue; - auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta; + auto const [assetsTotalDelta, debtTotalDelta] = + loanOriginationDeltas(vaultSle, principalRequested, state.interestDue); + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + debtTotalDelta; if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); debtMaximum != 0 && debtMaximum < newDebtTotal) { @@ -634,7 +635,7 @@ LoanSet::doApply() // Update the balances in the vault vaultAvailableProxy -= principalRequested; - vaultTotalProxy += state.interestDue; + vaultTotalProxy += assetsTotalDelta; XRPL_ASSERT_PARTS( *vaultAvailableProxy <= *vaultTotalProxy, "xrpl::LoanSet::doApply", @@ -642,7 +643,7 @@ LoanSet::doApply() view.update(vaultSle); // Update the balances in the loan broker - adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale); + adjustImpreciseNumber(brokerSle->at(sfDebtTotal), debtTotalDelta, vaultAsset, vaultScale); adjustLoanBrokerOwnerCount(view, brokerSle, 1, j_); loanSequenceProxy += 1; // The sequence should be extremely unlikely to roll over, but fail if it diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp index e1f5873a89..a522f62788 100644 --- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace xrpl { @@ -241,6 +242,8 @@ VaultCreate::doApply() } if (scale != 0u) vault->at(sfScale) = scale; + if (view().rules().enabled(featureLendingProtocolV1_1)) + vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis); view().insert(vault); // Explicitly create MPToken for the vault owner diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/LendingHelpers_test.cpp index ac8e0764fc..1235920fab 100644 --- a/src/test/app/LendingHelpers_test.cpp +++ b/src/test/app/LendingHelpers_test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include namespace xrpl::test { @@ -1470,6 +1472,326 @@ class LendingHelpers_test : public beast::unit_test::Suite Number{-18304, -5})); } + void + testAccrualLoanOriginationDeltas() + { + using namespace xrpl::Accrual; + + struct TestCase + { + std::string name; + Number principalRequested; + Number interestDue; + }; + + auto const testCases = std::vector{ + {.name = "Zero interest", + .principalRequested = Number{1'000}, + .interestDue = Number{0}}, + {.name = "Nonzero interest", + .principalRequested = Number{1'000}, + .interestDue = Number{75}}, + }; + + for (auto const& tc : testCases) + { + testcase("Accrual::loanOriginationDeltas: " + tc.name); + + auto const deltas = loanOriginationDeltas(tc.principalRequested, tc.interestDue); + BEAST_EXPECTS( + deltas.assetsTotalDelta == tc.interestDue, + "assetsTotalDelta mismatch: expected " + to_string(tc.interestDue) + ", got " + + to_string(deltas.assetsTotalDelta)); + BEAST_EXPECTS( + deltas.debtTotalDelta == tc.principalRequested + tc.interestDue, + "debtTotalDelta mismatch: expected " + + to_string(tc.principalRequested + tc.interestDue) + ", got " + + to_string(deltas.debtTotalDelta)); + } + } + + void + testCashBasisLoanOriginationDeltas() + { + using namespace xrpl::CashBasis; + + testcase("CashBasis::loanOriginationDeltas: interestDue is ignored"); + + Number const principalRequested{1'000}; + Number const interestDue{75}; + + auto const deltas = loanOriginationDeltas(principalRequested); + BEAST_EXPECTS( + deltas.assetsTotalDelta == 0, + "assetsTotalDelta mismatch: expected 0, got " + to_string(deltas.assetsTotalDelta)); + BEAST_EXPECTS( + deltas.debtTotalDelta == principalRequested, + "debtTotalDelta mismatch: expected " + to_string(principalRequested) + ", got " + + to_string(deltas.debtTotalDelta)); + } + + void + testAccrualLoanOriginationExceedsVaultMaximum() + { + using namespace xrpl::Accrual; + + struct TestCase + { + std::string name; + Number vaultMaximum; + Number vaultTotal; + Number interestDue; + bool expected; + }; + + auto const testCases = std::vector{ + {.name = "No maximum configured", + .vaultMaximum = Number{0}, + .vaultTotal = Number{900}, + .interestDue = Number{1'000}, + .expected = false}, + {.name = "Interest fits under headroom", + .vaultMaximum = Number{1'000}, + .vaultTotal = Number{900}, + .interestDue = Number{50}, + .expected = false}, + {.name = "Interest exactly fills headroom", + .vaultMaximum = Number{1'000}, + .vaultTotal = Number{900}, + .interestDue = Number{100}, + .expected = false}, + {.name = "Interest exceeds headroom", + .vaultMaximum = Number{1'000}, + .vaultTotal = Number{900}, + .interestDue = Number{101}, + .expected = true}, + }; + + for (auto const& tc : testCases) + { + testcase("Accrual::loanOriginationExceedsVaultMaximum: " + tc.name); + BEAST_EXPECT( + loanOriginationExceedsVaultMaximum( + tc.vaultMaximum, tc.vaultTotal, tc.interestDue) == tc.expected); + } + } + + // Constructs a minimal ltLOAN SLE with just the fields needed by + // loanVaultExposure. Mirrors the bare-SLE pattern used by + // testCanApplyToBrokerCover for ltLOAN_BROKER. + static std::shared_ptr + makeLoanSle( + Number const& totalValueOutstanding, + Number const& principalOutstanding, + Number const& managementFeeOutstanding) + { + auto sle = std::make_shared(ltLOAN, uint256{1u}); + sle->at(sfTotalValueOutstanding) = totalValueOutstanding; + sle->at(sfPrincipalOutstanding) = principalOutstanding; + sle->at(sfManagementFeeOutstanding) = managementFeeOutstanding; + return sle; + } + + // Constructs a minimal ltVAULT SLE with just LEVersion set (or left + // absent), for exercising the dispatchers' per-Vault gating. + static std::shared_ptr + makeVaultSle( + std::optional leVersion = std::nullopt, + std::optional assetsMaximum = std::nullopt, + std::optional assetsTotal = std::nullopt) + { + auto sle = std::make_shared(ltVAULT, uint256{2u}); + if (leVersion) + sle->at(sfLEVersion) = std::to_underlying(*leVersion); + if (assetsMaximum) + sle->at(sfAssetsMaximum) = *assetsMaximum; + if (assetsTotal) + sle->at(sfAssetsTotal) = *assetsTotal; + return sle; + } + + void + testAccrualLoanVaultExposure() + { + testcase("Accrual::loanVaultExposure"); + + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT(xrpl::Accrual::loanVaultExposure(sle) == Number{950}); + } + + void + testCashBasisLoanVaultExposure() + { + testcase("CashBasis::loanVaultExposure"); + + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT(xrpl::CashBasis::loanVaultExposure(sle) == Number{800}); + } + + void + testLoanPaymentDeltas() + { + // principalPaid, interestPaid, feePaid, valueChange are all distinct + // and nonzero, with a nonzero valueChange simulating a late-payment + // penalty, so Accrual's formula is meaningfully exercised. + LoanPaymentParts const parts{ + .principalPaid = Number{100}, + .interestPaid = Number{20}, + .valueChange = Number{5}, + .feePaid = Number{3}}; + + { + testcase("Accrual::loanPaymentDeltas: nonzero valueChange"); + auto const deltas = xrpl::Accrual::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == parts.valueChange); + BEAST_EXPECT( + deltas.debtTotalDelta == + (parts.principalPaid + parts.interestPaid) - parts.valueChange); + } + + { + testcase("CashBasis::loanPaymentDeltas: nonzero valueChange ignored"); + auto const deltas = xrpl::CashBasis::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == parts.interestPaid); + BEAST_EXPECT(deltas.debtTotalDelta == parts.principalPaid); + } + } + + void + testLoanOriginationDeltasDispatcher() + { + using namespace jtx; + + Number const principalRequested{1'000}; + Number const interestDue{75}; + + auto const legacyVault = makeVaultSle(); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis); + + { + testcase( + "loanOriginationDeltas dispatcher: amendment enabled, legacy vault picks " + "Accrual"); + Env const env{*this}; + auto const deltas = loanOriginationDeltas(legacyVault, principalRequested, interestDue); + auto const expected = + xrpl::Accrual::loanOriginationDeltas(principalRequested, interestDue); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + + { + testcase( + "loanOriginationDeltas dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis picks CashBasis"); + Env const env{*this}; + auto const deltas = + loanOriginationDeltas(cashBasisVault, principalRequested, interestDue); + auto const expected = xrpl::CashBasis::loanOriginationDeltas(principalRequested); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + } + + void + testLoanOriginationExceedsVaultMaximumDispatcher() + { + using namespace jtx; + + Number const vaultMaximum{1'000}; + Number const vaultTotal{900}; + // Exceeds Accrual's headroom (100), but must never trip CashBasis. + Number const interestDue{101}; + + auto const legacyVault = makeVaultSle(std::nullopt, vaultMaximum, vaultTotal); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis, vaultMaximum, vaultTotal); + + { + testcase( + "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, legacy vault " + "picks Accrual"); + Env const env{*this}; + BEAST_EXPECT( + loanOriginationExceedsVaultMaximum(legacyVault, vaultTotal, interestDue) == + xrpl::Accrual::loanOriginationExceedsVaultMaximum( + vaultMaximum, vaultTotal, interestDue)); + } + + { + testcase( + "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis picks CashBasis"); + Env const env{*this}; + BEAST_EXPECT( + loanOriginationExceedsVaultMaximum(cashBasisVault, vaultTotal, interestDue) == + false); + } + } + + void + testLoanVaultExposureDispatcher() + { + using namespace jtx; + + auto const legacyVault = makeVaultSle(); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis); + + { + testcase("loanVaultExposure dispatcher: amendment enabled, legacy vault picks Accrual"); + Env const env{*this}; + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT( + loanVaultExposure(legacyVault, sle) == xrpl::Accrual::loanVaultExposure(sle)); + } + + { + testcase( + "loanVaultExposure dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis " + "picks CashBasis"); + Env const env{*this}; + auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); + BEAST_EXPECT( + loanVaultExposure(cashBasisVault, sle) == xrpl::CashBasis::loanVaultExposure(sle)); + } + } + + void + testLoanPaymentDeltasDispatcher() + { + using namespace jtx; + + LoanPaymentParts const parts{ + .principalPaid = Number{100}, + .interestPaid = Number{20}, + .valueChange = Number{5}, + .feePaid = Number{3}}; + + auto const legacyVault = makeVaultSle(); + auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis); + + { + testcase("loanPaymentDeltas dispatcher: amendment enabled, legacy vault picks Accrual"); + Env const env{*this}; + auto const deltas = loanPaymentDeltas(legacyVault, parts); + auto const expected = xrpl::Accrual::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + + { + testcase( + "loanPaymentDeltas dispatcher: amendment enabled, LEVersion == " + "VaultVersion::CashBasis " + "picks CashBasis"); + Env const env{*this}; + auto const deltas = loanPaymentDeltas(cashBasisVault, parts); + auto const expected = xrpl::CashBasis::loanPaymentDeltas(parts); + BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); + BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); + } + } + public: void testCanApplyToBrokerCover() @@ -1573,6 +1895,17 @@ public: testComputeOverpaymentComponents(); testComputeInterestAndFeeParts(); testCanApplyToBrokerCover(); + + testAccrualLoanOriginationDeltas(); + testCashBasisLoanOriginationDeltas(); + testAccrualLoanOriginationExceedsVaultMaximum(); + testAccrualLoanVaultExposure(); + testCashBasisLoanVaultExposure(); + testLoanPaymentDeltas(); + testLoanOriginationDeltasDispatcher(); + testLoanOriginationExceedsVaultMaximumDispatcher(); + testLoanVaultExposureDispatcher(); + testLoanPaymentDeltasDispatcher(); } }; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 231a3b405a..8a6f1669df 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -92,8 +93,13 @@ class Loan_test : public beast::unit_test::Suite protected: // Ensure that all the features needed for Lending Protocol are included, // even if they are set to unsupported. - - FeatureBitset const all_{jtx::testableAmendments()}; + // + // featureLendingProtocolV1_1 is excluded from the default set: it changes + // Vault/LoanBroker accounting (AssetsTotal/DebtTotal/LossUnrealized), and + // most of this file's tests assert whole-life-specific expected values + // for those fields. Tests that specifically exercise the amendment opt + // it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`). + FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1}; std::string const iouCurrency_{"IOU"}; void @@ -363,16 +369,21 @@ protected: { TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto const brokerDebt = brokerSle->at(sfDebtTotal); - auto const expectedDebt = principalOutstanding + interestOwed; - env.test.BEAST_EXPECT(brokerDebt == expectedDebt); - env.test.BEAST_EXPECT( - env.balance(pseudoAccount, broker.asset).number() == - brokerSle->at(sfCoverAvailable)); - env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount); if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); env.test.BEAST_EXPECT(vaultSle)) { + auto const expectedDebt = + env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? principalOutstanding + : principalOutstanding + interestOwed; + env.test.BEAST_EXPECT(brokerDebt == expectedDebt); + env.test.BEAST_EXPECT( + env.balance(pseudoAccount, broker.asset).number() == + brokerSle->at(sfCoverAvailable)); + env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount); + Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)}; env.test.BEAST_EXPECT( vaultSle->at(sfAssetsAvailable) == @@ -468,7 +479,10 @@ protected: { env.test.BEAST_EXPECT( vaultSle->at(sfLossUnrealized) == - totalValue - managementFeeOutstanding); + (env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? principalOutstanding + : totalValue - managementFeeOutstanding)); } else { @@ -635,8 +649,11 @@ protected: // log << vaultSle->getJson() << std::endl; auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); - auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + state.totalValue - - state.managementFeeOutstanding; + auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + + (env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? state.principalOutstanding + : state.totalValue - state.managementFeeOutstanding); if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable)) { @@ -8547,6 +8564,972 @@ protected: }); } + // LendingProtocolV1_1 ("cash-basis" accounting) dedicated coverage. + // + // Existing tests never enable featureLendingProtocolV1_1 (see `all_` + // above), so these are the only tests in this file that exercise the + // amendment. They are called once, directly, from + // runAmendmentIndependent() -- not looped through + // runAmendmentSensitive()/amendmentCombinations(), since doing so would + // require re-deriving whole-life-specific expected values for ~15 + // unrelated regression tests. + + // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas, + // and the AssetsMaximum/DebtMaximum guards (which always check against + // principal + interestDue, regardless of the amendment). + void + testCashBasisLoanSetOrigination() + { + testcase("cash-basis: LoanSet origination"); + + using namespace jtx; + using namespace loan; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(10)}; + std::uint32_t const paymentTotal = 2; + std::uint32_t const paymentInterval = 86400; + + // Creates a broker/vault, submits a single LoanSet with a nonzero + // interest rate, and returns the observed Vault.AssetsTotal / + // LoanBroker.DebtTotal deltas plus the loan's own computed + // interestDue and principalOutstanding. + auto runOrigination = [&](FeatureBitset features) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBefore && brokerBefore); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + + auto const loanSequence = brokerBefore->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding); + Number const interestDue = totalValueOutstanding - principalOutstanding; + BEAST_EXPECT(interestDue > beast::kZero); + BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value()); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfter && brokerAfter); + Number const assetsTotalDelta = + Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; + Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; + + return std::make_tuple( + assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding); + }; + + Number interestDueCash{}; + Number principalOutstandingCash{}; + { + auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = + runOrigination(all_ | featureLendingProtocolV1_1); + interestDueCash = interestDue; + principalOutstandingCash = principalOutstanding; + + BEAST_EXPECTS( + assetsTotalDelta == beast::kZero, + "cash-basis origination must not change AssetsTotal; delta=" + + to_string(assetsTotalDelta)); + BEAST_EXPECTS( + debtTotalDelta == principalOutstanding, + "cash-basis origination must add principal-only to DebtTotal; delta=" + + to_string(debtTotalDelta) + " principal=" + to_string(principalOutstanding)); + } + + { + auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = + runOrigination(all_); + + BEAST_EXPECTS( + assetsTotalDelta == interestDue, + "whole-life origination must add interestDue to AssetsTotal; delta=" + + to_string(assetsTotalDelta) + " interestDue=" + to_string(interestDue)); + BEAST_EXPECTS( + debtTotalDelta == principalOutstanding + interestDue, + "whole-life origination must add principal+interest to DebtTotal; delta=" + + to_string(debtTotalDelta)); + } + + // AssetsMaximum guard checks interestDue headroom only under + // whole-life accounting; DebtMaximum guard also varies by model. + auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal); + + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = assetsTotalBefore + slack; + env(tx); + env.close(); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(expected)); + env.close(); + }; + + auto runBrokerGuard = [&](FeatureBitset features, Number const& debtMaximum, TER expected) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + env(loanBroker::set(lender, broker.vaultID), + loanBroker::kLoanBrokerId(broker.brokerID), + loanBroker::kDebtMaximum(debtMaximum), + Fee(env.current()->fees().base * 2)); + env.close(); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(expected)); + env.close(); + }; + + Number const oneDrop = xrpAsset(1).value(); + { + testcase("whole-life: LoanSet AssetsMaximum guard checks interestDue headroom"); + // Guard rejects when there's not quite enough headroom for the + // interest. + runVaultGuard(all_, interestDueCash - oneDrop, tecLIMIT_EXCEEDED); + // Guard accepts at the exact boundary. + runVaultGuard(all_, interestDueCash, tesSUCCESS); + } + + { + testcase("cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom"); + // Even far less headroom than interestDue still succeeds, since + // cash-basis origination never adds interest to AssetsTotal. + runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS); + } + + // DebtMaximum guard: cash-basis projects principal-only DebtTotal; + // whole-life projects principal + interestDue. + for (auto const cashBasis : {true, false}) + { + testcase( + std::string("LoanSet DebtMaximum guard (") + + (cashBasis ? "cash-basis)" : "whole-life)")); + auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_; + Number const newDebtTotal = + principalOutstandingCash + (cashBasis ? Number{} : interestDueCash); + runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED); + runBrokerGuard(features, newDebtTotal, tesSUCCESS); + } + } + + // 2. LoanPay: regular, late, overpayment, and full-payment types. + // Assert Vault.AssetsTotal/LoanBroker.DebtTotal deltas match + // interestPaid/principalPaid under cash-basis, and cross-check the + // amendment-disabled run's deltas against the documented whole-life + // formula (AssetsTotal += valueChange; DebtTotal mirrors the loan's own + // TotalValueOutstanding delta exactly, since whole-life debt recognition + // tracks total loan value). + void + testCashBasisLoanPay() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + using tp = NetClock::time_point; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + Number const principalRequest{12'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 300; + + struct PaymentDeltas + { + Number principalPaid; + Number assetsTotalDelta; + Number debtTotalDelta; + Number totalValueDelta; + }; + + // Sets up a fresh broker + loan, advances time, submits a single + // payment of the given type/amount, and returns the observed deltas. + auto runPayment = [&](FeatureBitset features, + std::uint32_t loanSetFlags, + std::uint32_t payFlags, + std::function const& advanceTime, + std::function const& paymentAmount) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + .flags = loanSetFlags, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(loanParams(env, broker)); + env.close(); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + + advanceTime(env, state.startDate); + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + auto const loanBefore = env.le(loanKeylet); + BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore); + + Number const principalBefore = loanBefore->at(sfPrincipalOutstanding); + Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + + STAmount const amount = paymentAmount(state); + env(pay(borrower, loanKeylet.key, amount, payFlags), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + auto const loanAfter = env.le(loanKeylet); + BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter); + + Number const principalAfter = loanAfter->at(sfPrincipalOutstanding); + Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding); + Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); + Number const debtTotalAfter = brokerAfter->at(sfDebtTotal); + + return PaymentDeltas{ + .principalPaid = principalBefore - principalAfter, + .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore, + .debtTotalDelta = debtTotalAfter - debtTotalBefore, + .totalValueDelta = totalValueAfter - totalValueBefore}; + }; + + // Compares the disabled (whole-life) and enabled (cash-basis) runs + // of the same payment scenario, and asserts the documented + // relationships between them. + auto checkScenario = [&](std::string const& label, + PaymentDeltas const& off, + PaymentDeltas const& on) { + testcase("cash-basis: LoanPay " + label); + + // The loan's own PrincipalOutstanding field is untouched by + // the amendment. + BEAST_EXPECTS( + off.principalPaid == on.principalPaid, + "principalPaid must be amendment-independent; off=" + to_string(off.principalPaid) + + " on=" + to_string(on.principalPaid)); + + // Whole-life structural invariant: DebtTotal (which + // recognizes a loan's full remaining value as debt) must + // change exactly as the loan's own TotalValueOutstanding + // does. + BEAST_EXPECTS( + off.debtTotalDelta == off.totalValueDelta, + "whole-life DebtTotal delta must mirror TotalValueOutstanding delta; " + "debtTotalDelta=" + + to_string(off.debtTotalDelta) + + " totalValueDelta=" + to_string(off.totalValueDelta)); + + // Derive interestPaid from the whole-life run's independent + // ledger deltas: + // assetsTotalDelta_off == valueChange + // debtTotalDelta_off == valueChange - (principalPaid + interestPaid) + // => interestPaid == assetsTotalDelta_off - debtTotalDelta_off - principalPaid + Number const interestPaid = + off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid; + BEAST_EXPECTS( + interestPaid >= beast::kZero, + "derived interestPaid must be non-negative: " + to_string(interestPaid)); + + BEAST_EXPECTS( + on.assetsTotalDelta == interestPaid, + "cash-basis AssetsTotal delta must equal interestPaid; delta=" + + to_string(on.assetsTotalDelta) + " interestPaid=" + to_string(interestPaid)); + BEAST_EXPECTS( + on.debtTotalDelta == -on.principalPaid, + "cash-basis DebtTotal delta must equal -principalPaid; delta=" + + to_string(on.debtTotalDelta) + " principalPaid=" + to_string(on.principalPaid)); + }; + + // ---- Regular, on-time payment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const regularAmount = [&](LoanState const& state) { + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * + Number{3, -1} * 5}; // 1.5x, so only a single period is paid + }; + + auto const off = runPayment(all_, 0, 0, noAdvance, regularAmount); + auto const on = + runPayment(all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount); + + // Regular, on-time payments never change the loan's value beyond + // normal amortization (production asserts valueChange == 0), so + // AssetsTotal must be unaffected in the whole-life run. + BEAST_EXPECTS( + off.assetsTotalDelta == beast::kZero, + "regular on-time payment must not change AssetsTotal under whole-life; delta=" + + to_string(off.assetsTotalDelta)); + + checkScenario("regular payment", off, on); + } + + // ---- Late payment ---- + { + auto const advancePastDue = [&](Env& env, tp const& startDate) { + env.close(startDate + std::chrono::seconds(paymentInterval + 1)); + }; + auto const lateAmount = [&](LoanState const& state) { + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * + Number{3}}; // generous; excess is not withdrawn + }; + + auto const off = runPayment(all_, 0, tfLoanLatePayment, advancePastDue, lateAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, + 0, + tfLoanLatePayment, + advancePastDue, + lateAmount); + + checkScenario("late payment", off, on); + } + + // ---- Overpayment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const overpayAmount = [&](LoanState const& state) { + // One regular period, plus a generous extra principal + // paydown. + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) + + xrpAsset(2'000).value()}; + }; + + auto const off = + runPayment(all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, + tfLoanOverpayment, + tfLoanOverpayment, + noAdvance, + overpayAmount); + + checkScenario("overpayment", off, on); + } + + // ---- Full payment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const fullAmount = [&](LoanState const&) { + // Generously large: full payment only ever consumes exactly + // what's due (principal + accrued interest; close fee/ + // prepayment penalty are 0 here), excess is not withdrawn. + return STAmount{xrpAsset, xrpAsset(principalRequest).value() * Number{2}}; + }; + + auto const off = runPayment(all_, 0, tfLoanFullPayment, noAdvance, fullAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount); + + checkScenario("full payment", off, on); + } + } + + // 3. LoanManage: impair, unimpair, and default. + void + testCashBasisLoanManage() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{percentageToTenthBips(10)}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + auto setupLoan = [&](Env& env) { + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(loanParams(env, broker)); + env.close(); + + return std::make_tuple(broker, loanKeylet, lender, borrower); + }; + + // ---- impair / unimpair ---- + auto runImpairUnimpair = [&](FeatureBitset features) { + Env env(*this, features); + auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); + + auto const loanBefore = env.le(loanKeylet); + BEAST_EXPECT(loanBefore); + Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding); + Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding); + + Number const expectedExposure = + env.current()->rules().enabled(featureLendingProtocolV1_1) + ? principalOutstanding + : totalValueOutstanding - managementFeeOutstanding; + + auto const vaultBeforeImpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultBeforeImpair); + Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized); + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterImpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultAfterImpair); + Number const impairDelta = Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore; + + env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterUnimpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultAfterUnimpair); + Number const netDelta = Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore; + + return std::make_tuple(expectedExposure, impairDelta, netDelta); + }; + + for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + { + testcase( + std::string("cash-basis: LoanManage impair/unimpair (") + + (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); + auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features); + + BEAST_EXPECTS( + impairDelta == expectedExposure, + "impair must add loanVaultExposure to LossUnrealized; delta=" + + to_string(impairDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + netDelta == beast::kZero, + "unimpair must be an exact reversal of impair; net=" + to_string(netDelta)); + } + + // ---- impair, then default ---- + auto runDefault = [&](FeatureBitset features) { + Env env(*this, features); + auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); + + auto const loanBeforeImpair = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeImpair); + Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding); + Number const managementFeeOutstanding = + loanBeforeImpair->at(sfManagementFeeOutstanding); + + Number const expectedExposure = + env.current()->rules().enabled(featureLendingProtocolV1_1) + ? principalOutstanding + : totalValueOutstanding - managementFeeOutstanding; + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + env.close( + state.startDate + std::chrono::seconds(paymentInterval) + + std::chrono::seconds(gracePeriod) + 60s); + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBefore && brokerBefore); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + Number const lossBefore = vaultBefore->at(sfLossUnrealized); + Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfter && brokerAfter); + Number const assetsTotalDelta = + Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; + Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; + Number const lossDelta = Number(vaultAfter->at(sfLossUnrealized)) - lossBefore; + Number const coverAvailableDelta = + Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore; + + Number const defaultCovered = -coverAvailableDelta; + Number const vaultDefaultAmount = expectedExposure - defaultCovered; + + return std::make_tuple( + expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount); + }; + + for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + { + testcase( + std::string("cash-basis: LoanManage default (") + + (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); + auto const + [expectedExposure, + assetsTotalDelta, + debtTotalDelta, + lossDelta, + vaultDefaultAmount] = runDefault(features); + + BEAST_EXPECTS( + debtTotalDelta == -expectedExposure, + "default must reduce DebtTotal by the unified default amount; delta=" + + to_string(debtTotalDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + lossDelta == -expectedExposure, + "default must reverse the earlier impair's LossUnrealized exactly; delta=" + + to_string(lossDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + assetsTotalDelta == -vaultDefaultAmount, + "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" + + to_string(assetsTotalDelta) + " expected=" + to_string(-vaultDefaultAmount)); + } + } + + // 3b. LEVersion regression: a Vault created before featureLendingProtocolV1_1 + // activates (LEVersion absent) must keep whole-life (accrual) accounting + // forever, even after the amendment is later enabled -- the switch is + // per-Vault (LEVersion == VaultVersion::CashBasis), not a single global amendment + // flag. + void + testLegacyVaultKeepsAccrualAfterAmendmentEnabled() + { + testcase("LEVersion: legacy vault keeps accrual after amendment enabled"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{percentageToTenthBips(10)}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + // Amendment disabled at Vault creation time: LEVersion stays absent. + Env env(*this, all_); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + { + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); + } + + // Now enable the amendment -- production dispatch must still treat + // this specific Vault as accrual-basis, since its LEVersion is + // (and remains) absent. + env.enableFeature(featureLendingProtocolV1_1); + env.close(); + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + // ---- LoanSet origination: whole-life formulas expected ---- + auto const vaultBeforeSet = env.le(broker.vaultKeylet()); + auto const brokerBeforeSet = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet); + Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal); + Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal); + + env(loanParams(env, broker)); + env.close(); + + auto const loanAfterSet = env.le(loanKeylet); + BEAST_EXPECT(loanAfterSet); + Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding); + Number const interestDue = totalValueOutstanding - principalOutstanding; + BEAST_EXPECT(interestDue > beast::kZero); + + auto const vaultAfterSet = env.le(broker.vaultKeylet()); + auto const brokerAfterSet = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfterSet && brokerAfterSet); + Number const assetsTotalDeltaSet = + Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet; + Number const debtTotalDeltaSet = + Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet; + + BEAST_EXPECTS( + assetsTotalDeltaSet == interestDue, + "legacy vault origination must still add interestDue to AssetsTotal; delta=" + + to_string(assetsTotalDeltaSet) + " interestDue=" + to_string(interestDue)); + BEAST_EXPECTS( + debtTotalDeltaSet == principalOutstanding + interestDue, + "legacy vault origination must still add principal+interest to DebtTotal; delta=" + + to_string(debtTotalDeltaSet)); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + env.close(); + + // ---- LoanPay: whole-life formulas expected ---- + auto const vaultBeforePay = env.le(broker.vaultKeylet()); + auto const brokerBeforePay = env.le(broker.brokerKeylet()); + auto const loanBeforePay = env.le(loanKeylet); + BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay); + Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding); + Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal); + Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal); + + STAmount const paymentAmount{ + xrpAsset, roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale)}; + env(pay(borrower, loanKeylet.key, paymentAmount), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterPay = env.le(broker.vaultKeylet()); + auto const brokerAfterPay = env.le(broker.brokerKeylet()); + auto const loanAfterPay = env.le(loanKeylet); + BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay); + Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding); + Number const assetsTotalDeltaPay = + Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay; + Number const debtTotalDeltaPay = + Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay; + Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay; + + // A regular, on-time payment has valueChange == 0, so whole-life + // AssetsTotal is untouched and DebtTotal mirrors TotalValueOutstanding. + BEAST_EXPECTS( + assetsTotalDeltaPay == beast::kZero, + "legacy vault regular payment must not change AssetsTotal; delta=" + + to_string(assetsTotalDeltaPay)); + BEAST_EXPECTS( + debtTotalDeltaPay == totalValueDeltaPay, + "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; " + "debtTotalDelta=" + + to_string(debtTotalDeltaPay) + " totalValueDelta=" + to_string(totalValueDeltaPay)); + + // ---- LoanManage: impair, then default -- whole-life exposure expected ---- + auto const loanBeforeImpair = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeImpair); + Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding); + Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding); + Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair; + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + LoanState const stateAtImpair = getCurrentState(env, broker, loanKeylet); + env.close( + stateAtImpair.startDate + std::chrono::seconds(paymentInterval) + + std::chrono::seconds(gracePeriod) + 60s); + + auto const vaultBeforeDefault = env.le(broker.vaultKeylet()); + auto const brokerBeforeDefault = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault); + Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal); + Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterDefault = env.le(broker.vaultKeylet()); + auto const brokerAfterDefault = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault); + Number const debtTotalDeltaDefault = + Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault; + Number const lossDeltaDefault = + Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault; + + BEAST_EXPECTS( + debtTotalDeltaDefault == -expectedExposure, + "legacy vault default must reduce DebtTotal by whole-life exposure; delta=" + + to_string(debtTotalDeltaDefault) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + lossDeltaDefault == -expectedExposure, + "legacy vault default must reverse the earlier impair's LossUnrealized exactly; " + "delta=" + + to_string(lossDeltaDefault) + " expected=" + to_string(expectedExposure)); + + // Confirm the Vault's LEVersion truly never got set, throughout. + { + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); + BEAST_EXPECT(getVaultVersion(vaultSle) == VaultVersion::Legacy); + } + } + + // 4. End-to-end trajectory: LoanSet -> 2 LoanPays -> LoanManage(default), + // entirely under the amendment, with independently hand-computed + // expected AssetsTotal/DebtTotal/LossUnrealized/CoverAvailable values at + // each step. 0% interest keeps the arithmetic exact and tractable; the + // divergence from whole-life accounting is already covered directly by + // testCashBasisLoanSetOrigination/LoanPay/LoanManage above, so this test + // focuses purely on an independent, from-scratch trajectory check. + void + testCashBasisEndToEndTrajectory() + { + testcase("cash-basis: end-to-end trajectory"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, .managementFeeRate = TenthBips16{0}}; + + Env env(*this, all_ | featureLendingProtocolV1_1); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + // Hand computation (all values in XRP, drops == 1e-6 XRP): + // Vault: AssetsTotal starts at 100'000 (the deposit). + // Broker: DebtTotal starts at 0, CoverAvailable starts at 1'000 + // (BrokerParameters::defaults().coverDeposit). + auto const vaultKeylet = broker.vaultKeylet(); + auto const brokerKeylet = broker.brokerKeylet(); + + // All the "human XRP unit" constants below (e.g. `100'000`) are + // converted to raw native (drops) values via xrpAsset(...), since + // that's how the ledger fields are actually denominated. + auto const checkVaultBroker = [&](Number const& assetsTotalUnits, + Number const& debtTotalUnits, + Number const& lossUnrealizedUnits, + Number const& coverAvailableUnits, + char const* step) { + Number const assetsTotal = xrpAsset(assetsTotalUnits).value(); + Number const debtTotal = xrpAsset(debtTotalUnits).value(); + Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value(); + Number const coverAvailable = xrpAsset(coverAvailableUnits).value(); + + auto const vaultSle = env.le(vaultKeylet); + auto const brokerSle = env.le(brokerKeylet); + BEAST_EXPECT(vaultSle && brokerSle); + BEAST_EXPECTS( + vaultSle->at(sfAssetsTotal) == assetsTotal, + std::string(step) + ": AssetsTotal expected " + to_string(assetsTotal) + " got " + + to_string(Number(vaultSle->at(sfAssetsTotal)))); + BEAST_EXPECTS( + brokerSle->at(sfDebtTotal) == debtTotal, + std::string(step) + ": DebtTotal expected " + to_string(debtTotal) + " got " + + to_string(Number(brokerSle->at(sfDebtTotal)))); + BEAST_EXPECTS( + vaultSle->at(sfLossUnrealized) == lossUnrealized, + std::string(step) + ": LossUnrealized expected " + to_string(lossUnrealized) + + " got " + to_string(Number(vaultSle->at(sfLossUnrealized)))); + BEAST_EXPECTS( + brokerSle->at(sfCoverAvailable) == coverAvailable, + std::string(step) + ": CoverAvailable expected " + to_string(coverAvailable) + + " got " + to_string(Number(brokerSle->at(sfCoverAvailable)))); + }; + + checkVaultBroker(100'000, 0, 0, 1'000, "before LoanSet"); + + // Loan: principal=1200, 0% interest, 12 payments of 100 each, no fees. + Number const principalRequest{1'200}; + std::uint32_t const paymentTotal = 12; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + auto const brokerBeforeLoan = env.le(brokerKeylet); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = TenthBips32{0}, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + env(loanParams(env, broker)); + env.close(); + + // Origination (cash-basis): AssetsTotal += 0, DebtTotal += principal. + checkVaultBroker(100'000, 1'200, 0, 1'000, "after LoanSet"); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + BEAST_EXPECT(state.periodicPayment == xrpAsset(100).value()); + + // Payment 1: principalPaid=100, interestPaid=0. + // AssetsTotal += 0; DebtTotal -= 100. + env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); + env.close(); + checkVaultBroker(100'000, 1'100, 0, 1'000, "after payment 1"); + + // Payment 2: same as above. + env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); + env.close(); + checkVaultBroker(100'000, 1'000, 0, 1'000, "after payment 2"); + + // Default (no impair): principalOutstanding remaining is 1'000. + // totalDefaultAmount (cash-basis) = PrincipalOutstanding = 1'000. + // minimumCover = DebtTotal(1'000) * coverRateMin(10%) = 100. + // covered = min(minimumCover * coverRateLiquidation(25%), totalDefaultAmount) + // = min(25, 1'000) = 25. + // defaultCovered = min(covered, CoverAvailable(1'000)) = 25. + // vaultDefaultAmount = 1'000 - 25 = 975. + // DebtTotal -= 1'000 -> 0. CoverAvailable -= 25 -> 975. + // AssetsTotal -= 975 -> 99'025. LossUnrealized unaffected (never impaired). + auto const loanBeforeDefault = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeDefault); + BEAST_EXPECT( + Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value()); + + env.close(state.startDate + std::chrono::seconds((3 * paymentInterval) + gracePeriod) + 1s); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + checkVaultBroker(99'025, 0, 0, 975, "after LoanManage(default)"); + } + void runAmendmentIndependent() { @@ -8570,6 +9553,12 @@ protected: testBugInterestDueDeltaCrash(); testFullLifecycleVaultPnLNearZeroRate(); testLoanSetNearZeroInterestRateSucceeds(); + + testCashBasisLoanSetOrigination(); + testCashBasisLoanPay(); + testCashBasisLoanManage(); + testLegacyVaultKeepsAccrualAfterAmendmentEnabled(); + testCashBasisEndToEndTrajectory(); } // Tests run under each entry in amendmentCombinations(). diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 12ad7e6782..bd596d6149 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -7645,6 +7645,83 @@ class Vault_test : public beast::unit_test::Suite } } + void + testVaultCreateLEVersion() + { + using namespace test::jtx; + + Account const owner{"owner"}; + PrettyAsset const xrpAsset = xrpIssue(); + + { + testcase("VaultCreate LEVersion: featureLendingProtocolV1_1 disabled, field absent"); + Env env{*this}; + env.disableFeature(featureLendingProtocolV1_1); + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(tx, Ter(tesSUCCESS)); + env.close(); + + auto const sleVault = env.le(keylet); + BEAST_EXPECT(sleVault); + BEAST_EXPECT(!sleVault->isFieldPresent(sfLEVersion)); + } + + { + testcase( + "VaultCreate LEVersion: featureLendingProtocolV1_1 enabled, LEVersion == " + "VaultVersion::CashBasis"); + Env env{*this}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(tx, Ter(tesSUCCESS)); + env.close(); + + auto const sleVault = env.le(keylet); + BEAST_EXPECT(sleVault); + BEAST_EXPECT(sleVault->isFieldPresent(sfLEVersion)); + BEAST_EXPECT(sleVault->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis)); + } + + { + testcase("VaultCreate rejects LEVersion set in the transaction"); + Env env{*this}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + tx[sfLEVersion] = 2; + env(tx, Ter(temMALFORMED)); + env.close(); + + BEAST_EXPECT(!env.le(keylet)); + } + + { + testcase("VaultSet rejects LEVersion set in the transaction"); + Env env{*this}; + env.fund(XRP(1'000'000), owner); + env.close(); + + Vault const vault{env}; + auto const [createTx, keylet] = vault.create({.owner = owner, .asset = xrpAsset}); + env(createTx, Ter(tesSUCCESS)); + env.close(); + + auto setTx = vault.set({.owner = owner, .id = keylet.key}); + setTx[sfLEVersion] = 2; + env(setTx, Ter(temMALFORMED)); + env.close(); + } + } + void testVaultDepositFreezeIOU() { @@ -8317,6 +8394,7 @@ public: testVaultEscrowedMPT(); testAssetsMaximum(); testVaultDeleteMemoData(); + testVaultCreateLEVersion(); testBug6LimitBypassWithShares(); testRemoveEmptyHoldingLockedAmount(); testRemoveEmptyHoldingConfidentialBalances(); diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp index 2697924d37..f55d01f606 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp @@ -35,6 +35,7 @@ TEST(VaultTests, BuilderSettersRoundTrip) auto const shareMPTIDValue = canonical_UINT192(); auto const withdrawalPolicyValue = canonical_UINT8(); auto const scaleValue = canonical_UINT8(); + auto const lEVersionValue = canonical_UINT8(); VaultBuilder builder{ previousTxnIDValue, @@ -54,6 +55,7 @@ TEST(VaultTests, BuilderSettersRoundTrip) builder.setAssetsMaximum(assetsMaximumValue); builder.setLossUnrealized(lossUnrealizedValue); builder.setScale(scaleValue); + builder.setLEVersion(lEVersionValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -166,6 +168,14 @@ TEST(VaultTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasScale()); } + { + auto const& expected = lEVersionValue; + auto const actualOpt = entry.getLEVersion(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfLEVersion"); + EXPECT_TRUE(entry.hasLEVersion()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -194,6 +204,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip) auto const shareMPTIDValue = canonical_UINT192(); auto const withdrawalPolicyValue = canonical_UINT8(); auto const scaleValue = canonical_UINT8(); + auto const lEVersionValue = canonical_UINT8(); auto sle = std::make_shared(Vault::entryType, index); @@ -212,6 +223,7 @@ TEST(VaultTests, BuilderFromSleRoundTrip) sle->at(sfShareMPTID) = shareMPTIDValue; sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue; sle->at(sfScale) = scaleValue; + sle->at(sfLEVersion) = lEVersionValue; VaultBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -390,6 +402,19 @@ TEST(VaultTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfScale"); } + { + auto const& expected = lEVersionValue; + + auto const fromSleOpt = entryFromSle.getLEVersion(); + auto const fromBuilderOpt = entryFromBuilder.getLEVersion(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfLEVersion"); + expectEqualField(expected, *fromBuilderOpt, "sfLEVersion"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -472,5 +497,7 @@ TEST(VaultTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getLossUnrealized().has_value()); EXPECT_FALSE(entry.hasScale()); EXPECT_FALSE(entry.getScale().has_value()); + EXPECT_FALSE(entry.hasLEVersion()); + EXPECT_FALSE(entry.getLEVersion().has_value()); } } From 532506541ff521e1b1d336156d828a4ec6d676ed Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:17:32 -0400 Subject: [PATCH 52/86] fix: Apply asfDisallowIncomingTrustline blocker to OfferCreate (#6307) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Mayukha Vadari Co-authored-by: Mayukha Vadari --- .../tx/transactors/dex/OfferCreate.cpp | 19 ++- src/test/app/Offer_test.cpp | 160 ++++++++++++++++++ 2 files changed, 175 insertions(+), 4 deletions(-) diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index fb47cf0f97..b95d1001e1 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -283,10 +283,23 @@ OfferCreate::checkAcceptAsset( return asset.visit( [&](Issue const& issue) -> TER { auto const& issuer = issue.getIssuer(); + auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency)); + + // Check if the issuer has lsfDisallowIncomingTrustline set. + // If so, the account must already have a trustline to receive tokens. + if (view.rules().enabled(fixCleanup3_4_0) && + issuerAccount->isFlag(lsfDisallowIncomingTrustline)) + { + if (!trustLine) + { + JLOG(j.debug()) << "delay: can't receive IOUs from issuer with " + "DisallowIncomingTrustline set"; + return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE}; + } + } + if (issuerAccount->isFlag(lsfRequireAuth)) { - auto const trustLine = view.read(keylet::trustLine(id, issuer, issue.currency)); - if (!trustLine) { return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE}; @@ -309,8 +322,6 @@ OfferCreate::checkAcceptAsset( } } - auto const trustLine = view.read(keylet::trustLine(id, issue.account, issue.currency)); - if (!trustLine) { return tesSUCCESS; diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index 7fc7161e36..33721e91d8 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -4324,6 +4324,165 @@ public: env.require(Balance(bob, gwUSD(10))); } + void + testDisallowIncomingTrustline(FeatureBitset features) + { + testcase("DisallowIncomingTrustline in OfferCreate"); + + // Test that asfDisallowIncomingTrustline flag prevents offer crossing + // when the taker doesn't have a trustline. + // + // 1. alice creates a trustline and sells USD/gw tokens. + // + // 2. gw sets asfDisallowIncomingTrustline flag. + // + // 3. An account without a trustline tries to create an offer for USD/gw. + // Without amendment: succeeds and crosses alice's offer (backward compatible). + // With amendment: fails with tecNO_LINE (new behavior). + // + // 4. An account WITH an existing trustline can create an offer. + // The offer succeeds and crosses alice's offer. + // + // Note: The DisallowIncomingTrustline flag also prevents NEW trustlines + // from being created via TrustSet (enforced by fixDisallowIncomingV1). + // So accounts must create trustlines BEFORE the issuer sets the flag. + + using namespace jtx; + auto const gw = Account("gw"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dan = Account("dan"); + auto const eve = Account("eve"); + auto const gwUSD = gw["USD"]; + + // Test without fixCleanup3_4_0 amendment + { + Env env{*this, features - fixCleanup3_4_0}; + + env.fund(XRP(400000), gw, alice, bob); + env.close(); + + // Alice creates trustline and gets some USD + env(trust(alice, gwUSD(100))); + env.close(); + env(pay(gw, alice, gwUSD(50))); + env.close(); + + // Alice creates sell offer + env(offer(alice, XRP(4000), gwUSD(40))); + env.close(); + env.require(offers(alice, 1)); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Without the amendment, bob can still create offer without trustline + // and the offer should cross (old behavior) + env(offer(bob, gwUSD(40), XRP(4000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(bob, 0)); + env.require(Balance(bob, gwUSD(40))); + } + + // Test with fixCleanup3_4_0 amendment + { + Env env{*this, features}; + + env.fund(XRP(400000), gw, alice, bob, carol, dan); + env.close(); + + // Alice creates trustline and gets some USD + env(trust(alice, gwUSD(100))); + env.close(); + env(pay(gw, alice, gwUSD(50))); + env.close(); + + // Bob and carol create trustlines BEFORE the flag is set + env(trust(bob, gwUSD(100))); + env.close(); + env(trust(carol, gwUSD(100))); + env.close(); + + // Alice creates sell offer + env(offer(alice, XRP(4000), gwUSD(40))); + env.close(); + env.require(offers(alice, 1)); + env.require(Balance(alice, gwUSD(50))); + + // GW sets DisallowIncomingTrustline flag + env(fset(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Dan tries to create offer without trustline - should fail + env(offer(dan, gwUSD(40), XRP(4000)), Ter(tecNO_LINE)); + env.close(); + + // Alice's offer should still exist + env.require(offers(alice, 1)); + env.require(Balance(alice, gwUSD(50))); + + // Dan shouldn't have any offers or balance + env.require(offers(dan, 0)); + BEAST_EXPECT(env.le(keylet::trustLine(dan, gwUSD)) == nullptr); + + // Bob already has trustline, so his offer should succeed and cross + env(offer(bob, gwUSD(40), XRP(4000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(bob, 0)); + env.require(Balance(alice, gwUSD(10))); + env.require(Balance(bob, gwUSD(40))); + + // Test scenario where carol already has a trustline (created before flag was set) + // Carol should be able to create offer since trustline already exists + env(pay(gw, alice, gwUSD(50))); + env.close(); + env(offer(alice, XRP(1000), gwUSD(10))); + env.close(); + env.require(offers(alice, 1)); + + env(offer(carol, gwUSD(10), XRP(1000))); + env.close(); + + // Offer should have crossed + env.require(offers(alice, 0)); + env.require(offers(carol, 0)); + env.require(Balance(alice, gwUSD(50))); + env.require(Balance(carol, gwUSD(10))); + + // Test that gw can clear the flag + env(fclear(gw, asfDisallowIncomingTrustline)); + env.close(); + + // Create new account eve without trustline + env.fund(XRP(400000), eve); + env.close(); + + // Bob creates another sell offer + env(pay(gw, bob, gwUSD(50))); + env.close(); + env(offer(bob, XRP(5000), gwUSD(50))); + env.close(); + env.require(offers(bob, 1)); + + // Eve should now be able to create offer without trustline (flag is cleared) + env(offer(eve, gwUSD(50), XRP(5000))); + env.close(); + + // Offer should have crossed + env.require(offers(bob, 0)); + env.require(offers(eve, 0)); + env.require(Balance(eve, gwUSD(50))); + } + } + void testRCSmoketest(FeatureBitset features) { @@ -5167,6 +5326,7 @@ public: testSelfPayUnlimitedFunds(features); testRequireAuth(features); testMissingAuth(features); + testDisallowIncomingTrustline(features); testRCSmoketest(features); testSelfAuth(features); testDeletedOfferIssuer(features); From 3c0659d26b6b9a3342d79b7034ae3677e4b3bc3b Mon Sep 17 00:00:00 2001 From: Olek <115580134+oleks-rip@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:18:33 -0400 Subject: [PATCH 53/86] test: Add confidential mpt bulletproof tests (#7816) --- sanitizers/suppressions/ubsan.supp | 1 + src/test/app/ConfidentialTransfer_test.cpp | 425 +++++++++++++++++++++ src/test/jtx/ConfidentialTransfer.h | 30 ++ 3 files changed, 456 insertions(+) diff --git a/sanitizers/suppressions/ubsan.supp b/sanitizers/suppressions/ubsan.supp index 7e3e02f855..a67a4a0ca3 100644 --- a/sanitizers/suppressions/ubsan.supp +++ b/sanitizers/suppressions/ubsan.supp @@ -192,6 +192,7 @@ unsigned-integer-overflow:rpc/handlers/orderbook/GetAggregatePrice.cpp # Test-only intentional overflow/underflow in fixture and unit-test arithmetic. unsigned-integer-overflow:tests/libxrpl/basics/RangeSet.cpp unsigned-integer-overflow:test/app/Batch_test.cpp +unsigned-integer-overflow:test/app/ConfidentialTransfer_test.cpp unsigned-integer-overflow:test/app/Invariants_test.cpp unsigned-integer-overflow:test/app/Loan_test.cpp unsigned-integer-overflow:test/app/NFToken_test.cpp diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index d3e0182db5..0fc5d6f845 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -5469,6 +5469,429 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase } } + void + testSendOverdraftBulletproof(FeatureBitset features) + { + uint64_t const balance = 100; + testSendOverdraftBulletproofImpl(features, balance, balance); // SUCCEED + testSendOverdraftBulletproofImpl(features, balance, balance + 1); // FAIL + } + + void + testSendOverdraftBulletproofImpl(FeatureBitset features, unsigned balance, unsigned amt) + { + testcase("Send: overdraft prevention via bulletproof"); + using namespace test::jtx; + + // Attack scenario: Alice has 100 tokens, tries to send 101 to Bob. + // The client-side check in mpt-crypto:mpt_utility.cpp:743 prevents honest + // clients from creating this proof. We bypass it by manually + // constructing a forged proof to demonstrate that the ledger's + // range proof verification catches the overdraft. + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), issuer("issuer"); + + uint64_t const aliceBalance = balance; + uint64_t const aliceAmount = amt; + uint64_t const aliceRemaining = aliceBalance - aliceAmount; + + // Setup: Alice has 100 tokens converted to confidential + ConfidentialEnv confEnv{ + env, + issuer, + {{.account = alice, .payAmount = 1000, .convertAmount = aliceBalance}, + {.account = bob, .payAmount = 1000, .convertAmount = 30}}}; + auto& mptIssuer = confEnv.mpt; + + std::pair errors = aliceAmount > aliceBalance + ? std::make_pair(-1, TER(tecBAD_PROOF)) + : std::make_pair(0, TER(tesSUCCESS)); + + unsigned const numParticipants = 3; + + // Verify Alice's actual balance before attack + { + auto const balance = requireOptional( + mptIssuer.getDecryptedBalance(alice, MPTTester::holderEncryptedSpending), + "Missing Alice's balance"); + BEAST_EXPECT(balance == aliceBalance); + } + + // We cannot use ConfidentialSendSetup directly because it would + // call mpt_get_confidential_send_proof which has a client-side + // check (amount > balance) at line 743 in mpt_utility.cpp. + // Instead, we manually construct the transaction components. + + Buffer const randomElgamal = generateBlindingFactor(); + Buffer const randomBalance = generateBlindingFactor(); + + // Create encrypted amounts (using the OVERDRAFT amount) + Buffer const aliceEncAmt = mptIssuer.encryptAmount(alice, aliceAmount, randomElgamal); + Buffer const bobEncAmt = mptIssuer.encryptAmount(bob, aliceAmount, randomElgamal); + Buffer const issuerEncAmt = mptIssuer.encryptAmount(issuer, aliceAmount, randomElgamal); + + // Create commitments + // IMPORTANT: Amount commitment uses same randomness as ElGamal encryption! + Buffer const amtCommit = mptIssuer.getPedersenCommitment(aliceAmount, randomElgamal); + Buffer const balanceCommit = mptIssuer.getPedersenCommitment(aliceBalance, randomBalance); + + // Get Alice's current encrypted spending balance + Buffer const aliceEncBalance = requireOptional( + mptIssuer.getEncryptedBalance(alice, MPTTester::holderEncryptedSpending), + "Missing Alice's encrypted spending balance"); + + uint32_t const version = mptIssuer.getMPTokenVersion(alice); + auto const ctxHash = getSendContextHash( + alice.id(), mptIssuer.issuanceID(), env.seq(alice), bob.id(), version); + + // Now we need to manually generate the sigma proof part. + // The sigma proof verifies ciphertext consistency and commitments, + // but doesn't check the range. We'll construct it with the overdraft + // amount to bypass the client-side check. + + // Generate the sigma proof manually using the lower-level secp256k1 API + auto* ctx = mpt_secp256k1_context(); + Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + + // Parse all public keys and ciphertexts + secp256k1_pubkey c1, c2Alice, c2Bob, c2Issuer; + // Parse sender's ciphertext C1 (first 33(kCompressedEcPointLength) bytes) + auto x = secp256k1_ec_pubkey_parse(ctx, &c1, aliceEncAmt.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1, "Failed to parse C1")) + return; + // Parse C2 components for all recipients + x = secp256k1_ec_pubkey_parse( + ctx, &c2Alice, aliceEncAmt.data() + kCompressedEcPointLength, kCompressedEcPointLength); + auto y = secp256k1_ec_pubkey_parse( + ctx, &c2Bob, bobEncAmt.data() + kCompressedEcPointLength, kCompressedEcPointLength); + auto z = secp256k1_ec_pubkey_parse( + ctx, + &c2Issuer, + issuerEncAmt.data() + kCompressedEcPointLength, + kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1 && z == 1, "Failed to parse C2 components")) + return; + secp256k1_pubkey c2Vec[] = {c2Alice, c2Bob, c2Issuer}; + + // Parse public keys + secp256k1_pubkey pkAlice, pkBob, pkIssuer; + auto alicePubKey = requireOptional(mptIssuer.getPubKey(alice), "Missing alice pubkey"); + auto bobPubKey = requireOptional(mptIssuer.getPubKey(bob), "Missing bob pubkey"); + auto issuerPubKey = requireOptional(mptIssuer.getPubKey(issuer), "Missing issuer pubkey"); + x = secp256k1_ec_pubkey_parse(ctx, &pkAlice, alicePubKey.data(), kCompressedEcPointLength); + y = secp256k1_ec_pubkey_parse(ctx, &pkBob, bobPubKey.data(), kCompressedEcPointLength); + z = secp256k1_ec_pubkey_parse( + ctx, &pkIssuer, issuerPubKey.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1 && z == 1, "Failed to parse public keys")) + return; + secp256k1_pubkey pkVec[] = {pkAlice, pkBob, pkIssuer}; + + // Parse commitments + secp256k1_pubkey pcAmount, pcBalance, b1, b2; + x = secp256k1_ec_pubkey_parse(ctx, &pcAmount, amtCommit.data(), kCompressedEcPointLength); + y = secp256k1_ec_pubkey_parse( + ctx, &pcBalance, balanceCommit.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1, "Failed to parse commitments")) + return; + // Parse balance ciphertext + x = secp256k1_ec_pubkey_parse(ctx, &b1, aliceEncBalance.data(), kCompressedEcPointLength); + y = secp256k1_ec_pubkey_parse( + ctx, &b2, aliceEncBalance.data() + kCompressedEcPointLength, kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1, "Failed to parse balance ciphertext")) + return; + + // Get Alice's private key + auto alicePrivKey = requireOptional(mptIssuer.getPrivKey(alice), "Missing alice privkey"); + + // Generate the compact sigma proof (part of mpt_get_confidential_send_proof) + // This will succeed because sigma proof doesn't check amount vs balance + x = secp256k1_compact_standard_prove( + ctx, + sigmaProof.data(), + aliceAmount, + aliceBalance, + randomElgamal.data(), + alicePrivKey.data(), + randomBalance.data(), + numParticipants, + &c1, + c2Vec, + pkVec, + &pcAmount, + &pkAlice, + &pcBalance, + &b1, + &b2, + ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Failed to generate sigma proof")) + return; + + // Direct verification + x = secp256k1_compact_standard_verify( + ctx, + sigmaProof.data(), + numParticipants, + &c1, + c2Vec, + pkVec, + &pcAmount, + &pkAlice, + &pcBalance, + &b1, + &b2, + ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Sigma verification failed")) + return; + + // Compute the remaining blinding factor: r_remaining = r_balance - r_amount + // This is required because the ledger homomorphically computes: + // C_remaining = C_balance - C_amount = Commit(remaining, r_balance - r_amount) + Buffer randomRemaining(kEcBlindingFactorLength); + Buffer negRandomElgamal(kEcBlindingFactorLength); + secp256k1_mpt_scalar_negate(negRandomElgamal.data(), randomElgamal.data()); + secp256k1_mpt_scalar_add( + randomRemaining.data(), randomBalance.data(), negRandomElgamal.data()); + + // Now forge the bulletproof claiming + auto const forgedBulletproof = getForgedBulletproof( + {aliceAmount, aliceRemaining}, {randomElgamal, randomRemaining}, ctxHash); + + // Combine sigma proof + forged bulletproof + Buffer combinedProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE + kEcDoubleBulletproofLength); + std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + std::memcpy( + combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, + forgedBulletproof.data(), + kEcDoubleBulletproofLength); + + // Direct verification + x = mpt_verify_send_range_proof( + combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, + amtCommit.data(), + balanceCommit.data(), + ctxHash.data()); + if (!BEAST_EXPECTS(x == errors.first, "Forged proof passed validation")) + return; + + // Attempt the transaction with forged proof + // Expected to FAIL with tecBAD_PROOF + mptIssuer.send({ + .account = alice, + .dest = bob, + .amt = aliceAmount, + .proof = strHex(combinedProof), + .senderEncryptedAmt = aliceEncAmt, + .destEncryptedAmt = bobEncAmt, + .issuerEncryptedAmt = issuerEncAmt, + .amountCommitment = amtCommit, + .balanceCommitment = balanceCommit, + .err = errors.second, + }); + + // Verify Alice's balance unchanged (attack prevented!) + { + auto const balance = requireOptional( + mptIssuer.getDecryptedBalance(alice, MPTTester::holderEncryptedSpending), + "Missing post-attack balance"); + if (aliceAmount > aliceBalance) + { + BEAST_EXPECT(balance == aliceBalance); + } + else + { + BEAST_EXPECT(balance < aliceBalance); + } + } + } + + void + testConvertBackOverdraftBulletproof(FeatureBitset features) + { + uint64_t const balance = 100; + testConvertBackOverdraftBulletproofImpl(features, balance, balance); // SUCCEED + testConvertBackOverdraftBulletproofImpl(features, balance, balance + 1); // FAIL + } + + void + testConvertBackOverdraftBulletproofImpl(FeatureBitset features, uint64_t balance, uint64_t amt) + { + testcase("Convert back: overdraft prevention via bulletproof"); + using namespace test::jtx; + + // Attack scenario: Bob has 100 confidential tokens, tries to convert back 101. + // The client-side check in mpt_get_convert_back_proof would prevent honest + // clients from creating this proof. We bypass it by manually constructing + // a forged proof to demonstrate that the ledger's bulletproof verification + // catches the overdraft. + + Env env{*this, features}; + Account const alice("alice"), bob("bob"), carol("carol"); + + uint64_t const bobBalance = balance; + uint64_t const convertAmount = amt; + uint64_t const bobRemaining = bobBalance - convertAmount; + + // Setup: Bob and Carol both have confidential balance + // Carol ensures outstanding amount >= convertAmount (bypass preclaim check) + // This allows us to test the bulletproof specifically + ConfidentialEnv confEnv{ + env, + alice, + { + {.account = bob, .payAmount = 1000, .convertAmount = bobBalance}, + {.account = carol, + .payAmount = 1000, + .convertAmount = std::max(convertAmount, bobBalance + 1)}, + }}; + auto& mptAlice = confEnv.mpt; + + std::pair errors = convertAmount > bobBalance + ? std::make_pair(-1, TER(tecBAD_PROOF)) + : std::make_pair(0, TER(tesSUCCESS)); + + // Verify Bob's actual balance before attack + { + auto const balance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing Bob's balance"); + BEAST_EXPECT(balance == bobBalance); + } + + // We cannot use the standard getConvertBackProof because it calls + // mpt_get_convert_back_proof which has client-side validation. + // Instead, we manually construct the sigma proof and forge the bulletproof. + + Buffer const blindingFactor = generateBlindingFactor(); + Buffer const pcBlindingFactor = generateBlindingFactor(); + + // Create encrypted amounts for the conversion + Buffer const bobEncAmt = mptAlice.encryptAmount(bob, convertAmount, blindingFactor); + Buffer const issuerEncAmt = mptAlice.encryptAmount(alice, convertAmount, blindingFactor); + + // Create Pedersen commitment to the current balance + Buffer const balanceCommit = mptAlice.getPedersenCommitment(bobBalance, pcBlindingFactor); + + // Get Bob's current encrypted spending balance + Buffer const bobEncBalance = requireOptional( + mptAlice.getEncryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing Bob's encrypted spending balance"); + + uint32_t const version = mptAlice.getMPTokenVersion(bob); + auto const ctxHash = + getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version); + + // Now manually generate the compact sigma proof for ConvertBack + auto* ctx = mpt_secp256k1_context(); + Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + + // Parse the holder's public key + secp256k1_pubkey pkBob; + auto bobPubKey = requireOptional(mptAlice.getPubKey(bob), "Missing bob pubkey"); + auto x = secp256k1_ec_pubkey_parse(ctx, &pkBob, bobPubKey.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1, "Failed to parse Bob's public key")) + return; + + // Parse balance commitment + secp256k1_pubkey pcBalance; + x = secp256k1_ec_pubkey_parse( + ctx, &pcBalance, balanceCommit.data(), kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1, "Failed to parse balance commitment")) + return; + + // Parse balance ciphertext (B1, B2) + secp256k1_pubkey b1, b2; + x = secp256k1_ec_pubkey_parse(ctx, &b1, bobEncBalance.data(), kCompressedEcPointLength); + auto y = secp256k1_ec_pubkey_parse( + ctx, &b2, bobEncBalance.data() + kCompressedEcPointLength, kCompressedEcPointLength); + if (!BEAST_EXPECTS(x == 1 && y == 1, "Failed to parse balance ciphertext")) + return; + + // Get Bob's private key + auto bobPrivKey = requireOptional(mptAlice.getPrivKey(bob), "Missing bob privkey"); + + // Generate the compact sigma proof for ConvertBack + // This verifies balance ownership and commitment linkage + x = secp256k1_compact_convertback_prove( + ctx, + sigmaProof.data(), + bobBalance, + bobPrivKey.data(), + pcBlindingFactor.data(), + &pkBob, + &b1, + &b2, + &pcBalance, + ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Failed to generate convertback sigma proof")) + return; + + // Verify the sigma proof passes (it doesn't check range) + x = secp256k1_compact_convertback_verify( + ctx, sigmaProof.data(), &pkBob, &b1, &b2, &pcBalance, ctxHash.data()); + if (!BEAST_EXPECTS(x == 1, "Sigma verification failed")) + return; + + // Now forge the single bulletproof claiming the remaining balance is valid + // For ConvertBack, we need to prove: (balance - convertAmount) >= 0 + // We create a commitment to the remainder and generate a bulletproof for it + + // The bulletproof needs the blinding factor for the remainder commitment + // The ledger computes: C_remainder = C_balance - convertAmount*G + // So the blinding factor is just pcBlindingFactor (no randomness in convertAmount*G) + + auto const forgedBulletproof = + getForgedSingleBulletproof(bobRemaining, pcBlindingFactor, ctxHash); + + // Combine sigma proof + forged bulletproof + Buffer combinedProof(kEcConvertBackProofLength); + std::memcpy( + combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + std::memcpy( + combinedProof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE, + forgedBulletproof.data(), + kEcSingleBulletproofLength); + + // Direct verification of the full proof + x = mpt_verify_convert_back_proof( + combinedProof.data(), + bobPubKey.data(), + bobEncBalance.data(), + balanceCommit.data(), + convertAmount, + ctxHash.data()); + if (!BEAST_EXPECTS(x == errors.first, "Forged proof verification mismatch")) + return; + + // Attempt the transaction with forged proof + // Expected to FAIL with tecBAD_PROOF when convertAmount > bobBalance + mptAlice.convertBack({ + .account = bob, + .amt = convertAmount, + .proof = combinedProof, + .holderEncryptedAmt = bobEncAmt, + .issuerEncryptedAmt = issuerEncAmt, + .blindingFactor = blindingFactor, + .pedersenCommitment = balanceCommit, + .err = errors.second, + }); + + // Verify Bob's balance unchanged (attack prevented!) + { + auto const postBalance = requireOptional( + mptAlice.getDecryptedBalance(bob, MPTTester::holderEncryptedSpending), + "Missing post-attack balance"); + if (convertAmount > bobBalance) + { + BEAST_EXPECT(postBalance == bobBalance); + } + else + { + BEAST_EXPECT(postBalance < bobBalance); + } + } + } + void testConvertBackBulletproof(FeatureBitset features) { @@ -8143,6 +8566,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase testConvertBackWithAuditor(features); testConvertBackPedersenProof(features); testConvertBackBulletproof(features); + testConvertBackOverdraftBulletproof(features); // Homomorphic operation tests testSendHomomorphicOverflow(features); @@ -8177,6 +8601,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase testSendInvalidProofContextBinding(features); testSendForgedEqualityProof(features); testSendForgedRangeProof(features); + testSendOverdraftBulletproof(features); testSendNegativeValueMalleability(features); testSendFiatShamirBinding(features); testSendProofComponentReuse(features); diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h index 404ddbe31d..465bac03db 100644 --- a/src/test/jtx/ConfidentialTransfer.h +++ b/src/test/jtx/ConfidentialTransfer.h @@ -94,6 +94,36 @@ protected: return proof; } + // Generate a forged single bulletproof for a single value and blinding factor. + // Used to test ConvertBack overdraft prevention via bulletproof verification. + static Buffer + getForgedSingleBulletproof( + uint64_t value, + Buffer const& blindingFactor, + uint256 const& contextHash) + { + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey h; + secp256k1_mpt_get_h_generator(ctx, &h); + + Buffer proof(kEcSingleBulletproofLength); + size_t proofLen = kEcSingleBulletproofLength; + + if (secp256k1_bulletproof_prove_agg( + ctx, + proof.data(), + &proofLen, + &value, + blindingFactor.data(), + 1, // m = 1 (single bulletproof) + &h, + contextHash.data()) == 0) + Throw("Failed to generate forged single bulletproof"); + + return proof; + } + // Get a bad ciphertext with valid structure but cryptographic invalid for // testing purposes. For preflight test purposes. static Buffer const& From 21cd6154076a2a9f58471b76f47d14c3c2416a38 Mon Sep 17 00:00:00 2001 From: Bart Date: Thu, 30 Jul 2026 11:02:05 -0400 Subject: [PATCH 54/86] perf: Replace node ID by depth in `TMLedgerNode` (#6353) Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com> --- include/xrpl/basics/Slice.h | 8 + include/xrpl/proto/xrpl.proto | 10 +- include/xrpl/shamap/SHAMap.h | 59 +++- include/xrpl/shamap/SHAMapLeafNode.h | 15 + src/libxrpl/shamap/SHAMapNodeID.cpp | 3 +- src/libxrpl/shamap/SHAMapSync.cpp | 109 ++++---- src/test/app/LedgerNodeHelpers_test.cpp | 260 ++++++++++++++++++ src/test/overlay/ProtocolVersion_test.cpp | 4 +- src/tests/libxrpl/shamap/SHAMapSync.cpp | 18 +- src/xrpld/app/ledger/InboundLedger.h | 14 +- src/xrpld/app/ledger/LedgerNodeHelpers.h | 52 ++++ src/xrpld/app/ledger/detail/InboundLedger.cpp | 125 ++++++--- .../app/ledger/detail/InboundLedgers.cpp | 18 +- .../app/ledger/detail/InboundTransactions.cpp | 38 ++- .../app/ledger/detail/LedgerNodeHelpers.cpp | 89 ++++++ .../app/ledger/detail/TransactionAcquire.cpp | 18 +- .../app/ledger/detail/TransactionAcquire.h | 6 +- src/xrpld/overlay/Peer.h | 1 + src/xrpld/overlay/detail/PeerImp.cpp | 239 +++++++++++++--- src/xrpld/overlay/detail/PeerImp.h | 5 +- src/xrpld/overlay/detail/ProtocolVersion.cpp | 1 + 21 files changed, 898 insertions(+), 194 deletions(-) create mode 100644 src/test/app/LedgerNodeHelpers_test.cpp create mode 100644 src/xrpld/app/ledger/LedgerNodeHelpers.h create mode 100644 src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 36e7615c3a..75c9b8c7bd 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -251,4 +252,11 @@ makeSlice(std::basic_string const& s) return Slice(s.data(), s.size()); } +template +Slice +makeSlice(std::basic_string_view s) +{ + return Slice(s.data(), s.size()); +} + } // namespace xrpl diff --git a/include/xrpl/proto/xrpl.proto b/include/xrpl/proto/xrpl.proto index d49920201e..bef5ec1d76 100644 --- a/include/xrpl/proto/xrpl.proto +++ b/include/xrpl/proto/xrpl.proto @@ -246,7 +246,15 @@ message TMGetObjectByHash { message TMLedgerNode { required bytes nodedata = 1; - optional bytes nodeid = 2; // missing for ledger base data + + // Used when protocol version <2.3. Not set for ledger base data. + optional bytes nodeid = 2; + + // Used when protocol version >=2.3. Neither value is set for ledger base data. + oneof reference { + bytes id = 3; // Set for inner nodes. + uint32 depth = 4; // Set for leaf nodes. + } } enum TMLedgerInfoType { diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index a1194ccfd3..e198c472fa 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -95,6 +94,21 @@ enum class SHAMapState { * * See https://en.wikipedia.org/wiki/Merkle_tree */ + +/** + * Holds a SHAMap node's identity, leaf status, and serialized data. Used by + * getNodeFat to return node data for peer synchronization. + */ +struct SHAMapNodeData +{ + SHAMapNodeID nodeID; + // The `data` field (a Blob, 8-byte aligned) needs 4 bytes of padding after the `nodeID` field + // (36 bytes, 4-byte aligned) regardless of what comes between them, so `isLeaf` costs nothing + // extra here. Moving it after `data` would add 8 bytes to the size of this struct instead. + bool isLeaf; + Blob data; +}; + class SHAMap { private: @@ -289,10 +303,10 @@ public: std::vector> getMissingNodes(int maxNodes, SHAMapSyncFilter const* filter); - bool + [[nodiscard]] bool getNodeFat( SHAMapNodeID const& wanted, - std::vector>& data, + std::vector& data, bool fatLeaves, std::uint32_t depth) const; @@ -321,10 +335,45 @@ public: void serializeRoot(Serializer& s) const; + /** + * Add a root node to the SHAMap during synchronization. + * + * This function is used when receiving the root node of a SHAMap from a peer during ledger + * synchronization. The node must already have been deserialized. + * + * @param hash The expected hash of the root node. + * @param rootNode A deserialized root node to add. + * @param filter Optional sync filter to track received nodes. + * @return Status indicating whether the node was useful, duplicate, or invalid. + * + * @note This function expects the rootNode to be a valid, deserialized SHAMapTreeNode. The + * caller is responsible for deserialization and basic validation before calling this + * function. + */ SHAMapAddNode - addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter); + addRootNode(SHAMapHash const& hash, SHAMapTreeNodePtr rootNode, SHAMapSyncFilter const* filter); + + /** + * Add a known node at a specific position in the SHAMap during synchronization. + * + * This function is used when receiving nodes from peers during ledger synchronization. The node + * is inserted at the position specified by nodeID. The node must already have been + * deserialized. + * + * @param nodeID The position in the tree where this node belongs. + * @param treeNode A deserialized tree node to add. + * @param filter Optional sync filter to track received nodes. + * @return Status indicating whether the node was useful, duplicate, or invalid. + * + * @note This function expects the treeNode to be a valid, deserialized SHAMapTreeNode. The + * caller is responsible for deserialization and basic validation before calling this + * function. This also means that the nodeID must be consistent with the node's content. + */ SHAMapAddNode - addKnownNode(SHAMapNodeID const& nodeID, Slice const& rawNode, SHAMapSyncFilter const* filter); + addKnownNode( + SHAMapNodeID const& nodeID, + SHAMapTreeNodePtr treeNode, + SHAMapSyncFilter const* filter); // status functions void diff --git a/include/xrpl/shamap/SHAMapLeafNode.h b/include/xrpl/shamap/SHAMapLeafNode.h index 26cfde9fe8..ab5bd574ed 100644 --- a/include/xrpl/shamap/SHAMapLeafNode.h +++ b/include/xrpl/shamap/SHAMapLeafNode.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -60,4 +63,16 @@ public: getString(SHAMapNodeID const&) const final; }; +/** + * Return the key of the item held by a SHAMap leaf node. + * + * @param node a node known to be a leaf (see SHAMapTreeNode::isLeaf). + */ +inline uint256 const& +leafKey(SHAMapTreeNode const& node) +{ + XRPL_ASSERT(node.isLeaf(), "xrpl::leafKey : node is a leaf"); + return safeDowncast(node).peekItem()->key(); +} + } // namespace xrpl diff --git a/src/libxrpl/shamap/SHAMapNodeID.cpp b/src/libxrpl/shamap/SHAMapNodeID.cpp index 16aaafe709..a511fc038c 100644 --- a/src/libxrpl/shamap/SHAMapNodeID.cpp +++ b/src/libxrpl/shamap/SHAMapNodeID.cpp @@ -129,7 +129,8 @@ selectBranch(SHAMapNodeID const& id, uint256 const& hash) SHAMapNodeID SHAMapNodeID::createID(int depth, uint256 const& key) { - XRPL_ASSERT((depth >= 0) && (depth < 65), "xrpl::SHAMapNodeID::createID : valid branch input"); + XRPL_ASSERT( + depth >= 0 && depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth"); return SHAMapNodeID(depth, key & depthMask(depth)); } diff --git a/src/libxrpl/shamap/SHAMapSync.cpp b/src/libxrpl/shamap/SHAMapSync.cpp index cc30426f9d..cbed6885c9 100644 --- a/src/libxrpl/shamap/SHAMapSync.cpp +++ b/src/libxrpl/shamap/SHAMapSync.cpp @@ -107,7 +107,7 @@ SHAMap::visitNodes(std::function const& function) const void SHAMap::visitDifferences( - SHAMap const* have, + SHAMap const* map, std::function const& function) const { // Visit every node in this SHAMap that is not present @@ -118,13 +118,13 @@ SHAMap::visitDifferences( if (root_->getHash().isZero()) return; - if ((have != nullptr) && (root_->getHash() == have->root_->getHash())) + if ((map != nullptr) && (root_->getHash() == map->root_->getHash())) return; if (root_->isLeaf()) { auto leaf = intr_ptr::staticPointerCast(root_); - if ((have == nullptr) || !have->hasLeafNode(leaf->peekItem()->key(), leaf->getHash())) + if ((map == nullptr) || !map->hasLeafNode(leaf->peekItem()->key(), leaf->getHash())) function(*root_); return; } @@ -149,18 +149,15 @@ SHAMap::visitDifferences( if (!node->isEmptyBranch(i)) { auto const& childHash = node->getChildHash(i); - SHAMapNodeID const childID = nodeID.getChildNodeID(i); + auto const childID = nodeID.getChildNodeID(i); auto next = descendThrow(node, i); if (next->isInner()) { - if ((have == nullptr) || !have->hasInnerNode(childID, childHash)) + if ((map == nullptr) || !map->hasInnerNode(childID, childHash)) stack.emplace(safeDowncast(next), childID); } - else if ( - (have == nullptr) || - !have->hasLeafNode( - safeDowncast(next)->peekItem()->key(), childHash)) + else if ((map == nullptr) || !map->hasLeafNode(leafKey(*next), childHash)) { if (!function(*next)) return; @@ -414,7 +411,7 @@ SHAMap::getMissingNodes(int max, SHAMapSyncFilter const* filter) bool SHAMap::getNodeFat( SHAMapNodeID const& wanted, - std::vector>& data, + std::vector& data, bool fatLeaves, std::uint32_t depth) const { @@ -460,7 +457,7 @@ SHAMap::getNodeFat( // Add this node to the reply s.erase(); node->serializeForWire(s); - data.emplace_back(nodeID, s.getData()); + data.emplace_back(nodeID, node->isLeaf(), s.getData()); if (node->isInner()) { @@ -490,7 +487,7 @@ SHAMap::getNodeFat( // Just include this node s.erase(); childNode->serializeForWire(s); - data.emplace_back(childID, s.getData()); + data.emplace_back(childID, childNode->isLeaf(), s.getData()); } } } @@ -508,25 +505,33 @@ SHAMap::serializeRoot(Serializer& s) const } SHAMapAddNode -SHAMap::addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter const* filter) +SHAMap::addRootNode( + SHAMapHash const& hash, + SHAMapTreeNodePtr rootNode, + SHAMapSyncFilter const* filter) { + XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid"); + XRPL_ASSERT(rootNode, "xrpl::SHAMap::addRootNode : non-null root node"); + // we already have a root_ node if (root_->getHash().isNonZero()) { - JLOG(journal_.trace()) << "got root node, already have one"; - XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash input"); + JLOG(journal_.trace()) << "Got root node, already have one"; + XRPL_ASSERT(root_->getHash() == hash, "xrpl::SHAMap::addRootNode : valid hash"); return SHAMapAddNode::duplicate(); } - XRPL_ASSERT(cowid_ >= 1, "xrpl::SHAMap::addRootNode : valid cowid"); - auto node = SHAMapTreeNode::makeFromWire(rootNode); - if (!node || node->getHash() != hash) + if (rootNode->getHash() != hash) + { + JLOG(journal_.warn()) << "Corrupt root node received: expected hash " << hash << ", got " + << rootNode->getHash(); return SHAMapAddNode::invalid(); + } if (backed_) - canonicalize(hash, node); + canonicalize(hash, rootNode); - root_ = node; + root_ = std::move(rootNode); if (root_->isLeaf()) clearSynching(); @@ -543,9 +548,18 @@ SHAMap::addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFil } SHAMapAddNode -SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncFilter const* filter) +SHAMap::addKnownNode( + SHAMapNodeID const& nodeID, + SHAMapTreeNodePtr treeNode, + SHAMapSyncFilter const* filter) { - XRPL_ASSERT(!node.isRoot(), "xrpl::SHAMap::addKnownNode : valid node input"); + XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node"); + XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node"); + XRPL_ASSERT( + !treeNode->isLeaf() || + SHAMapNodeID::createID(nodeID.getDepth(), leafKey(*treeNode)).getNodeID() == + nodeID.getNodeID(), + "xrpl::SHAMap::addKnownNode : leaf position consistent with node ID"); if (!isSynching()) { @@ -559,14 +573,15 @@ SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncF while (currNode->isInner() && !safeDowncast(currNode)->isFullBelow(generation) && - (currNodeID.getDepth() < node.getDepth())) + (currNodeID.getDepth() < nodeID.getDepth())) { - int const branch = selectBranch(currNodeID, node.getNodeID()); + int const branch = selectBranch(currNodeID, nodeID.getNodeID()); XRPL_ASSERT(branch >= 0, "xrpl::SHAMap::addKnownNode : valid branch"); auto inner = safeDowncast(currNode); if (inner->isEmptyBranch(branch)) { - JLOG(journal_.warn()) << "Add known node for empty branch" << node; + JLOG(journal_.warn()) << "Add known node " << nodeID << " for empty branch " << branch + << " at " << currNodeID; return SHAMapAddNode::invalid(); } @@ -582,67 +597,45 @@ SHAMap::addKnownNode(SHAMapNodeID const& node, Slice const& rawNode, SHAMapSyncF if (currNode != nullptr) continue; - auto newNode = SHAMapTreeNode::makeFromWire(rawNode); - - if (!newNode || childHash != newNode->getHash()) + if (childHash != treeNode->getHash()) { - JLOG(journal_.warn()) << "Corrupt node received"; + JLOG(journal_.warn()) << "Corrupt node " << nodeID << " received: expected hash " + << childHash << ", got " << treeNode->getHash(); return SHAMapAddNode::invalid(); } - // In rare cases, a node can still be corrupt even after hash - // validation. For leaf nodes, we perform an additional check to - // ensure the node's position in the tree is consistent with its - // content to prevent inconsistencies that could - // propagate further down the line. - if (newNode->isLeaf()) - { - auto const& actualKey = - safeDowncast(newNode.get())->peekItem()->key(); - - // Validate that this leaf belongs at the target position - auto const expectedNodeID = SHAMapNodeID::createID(node.getDepth(), actualKey); - if (expectedNodeID.getNodeID() != node.getNodeID()) - { - JLOG(journal_.debug()) - << "Leaf node position mismatch: " - << "expected=" << expectedNodeID.getNodeID() << ", actual=" << node.getNodeID(); - return SHAMapAddNode::invalid(); - } - } - // Inner nodes must be at a level strictly less than 64 // but leaf nodes (while notionally at level 64) can be // at any depth up to and including 64: if ((currNodeID.getDepth() > kLeafDepth) || - (newNode->isInner() && currNodeID.getDepth() == kLeafDepth)) + (treeNode->isInner() && currNodeID.getDepth() == kLeafDepth)) { // Map is provably invalid state_ = SHAMapState::Invalid; return SHAMapAddNode::useful(); } - if (currNodeID != node) + if (currNodeID != nodeID) { // Either this node is broken or we didn't request it (yet) - JLOG(journal_.warn()) << "unable to hook node " << node; + JLOG(journal_.warn()) << "unable to hook node " << nodeID; JLOG(journal_.info()) << " stuck at " << currNodeID; - JLOG(journal_.info()) << "got depth=" << node.getDepth() + JLOG(journal_.info()) << "got depth=" << nodeID.getDepth() << ", walked to= " << currNodeID.getDepth(); return SHAMapAddNode::useful(); } if (backed_) - canonicalize(childHash, newNode); + canonicalize(childHash, treeNode); - newNode = prevNode->canonicalizeChild(branch, std::move(newNode)); + treeNode = prevNode->canonicalizeChild(branch, std::move(treeNode)); if (filter != nullptr) { Serializer s; - newNode->serializeWithPrefix(s); + treeNode->serializeWithPrefix(s); filter->gotNode( - false, childHash, ledgerSeq_, std::move(s.modData()), newNode->getType()); + false, childHash, ledgerSeq_, std::move(s.modData()), treeNode->getType()); } return SHAMapAddNode::useful(); diff --git a/src/test/app/LedgerNodeHelpers_test.cpp b/src/test/app/LedgerNodeHelpers_test.cpp new file mode 100644 index 0000000000..a9e4e3ebfc --- /dev/null +++ b/src/test/app/LedgerNodeHelpers_test.cpp @@ -0,0 +1,260 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +namespace xrpl::tests { + +class LedgerNodeHelpers_test : public beast::unit_test::Suite +{ + static boost::intrusive_ptr + makeTestItem(std::uint32_t seed) + { + Serializer s; + s.add32(seed); + s.add32(seed + 1); + s.add32(seed + 2); + return makeShamapitem(s.getSHA512Half(), s.slice()); + } + + static std::string + serializeNode(SHAMapTreeNodePtr const& node) + { + Serializer s; + node->serializeForWire(s); + auto const slice = s.slice(); + return std::string(slice.begin(), slice.end()); + } + + void + testGetTreeNode() + { + testcase("getTreeNode"); + + // Valid: inner node. It must have at least one child for `serializeNode` to work. + { + auto const innerNode = intr_ptr::makeShared(1); + auto const childNode = intr_ptr::makeShared(1); + innerNode->setChild(0, childNode); + auto const innerData = serializeNode(innerNode); + auto const result = getTreeNode(innerData); + BEAST_EXPECT(result && result->isInner()); + } + + // Valid: leaf node. + { + auto const leafItem = makeTestItem(12345); + auto const leafNode = intr_ptr::makeShared(leafItem, 1); + auto const leafData = serializeNode(leafNode); + auto const result = getTreeNode(leafData); + BEAST_EXPECT(result && result->isLeaf()); + } + + // Invalid: empty data. + { + auto const result = getTreeNode(""); + BEAST_EXPECT(!result); + } + + // Invalid: garbage data. + { + auto const result = getTreeNode("invalid"); + BEAST_EXPECT(!result); + } + + // Invalid: truncated data. + { + auto const leafItem = makeTestItem(54321); + auto const leafNode = intr_ptr::makeShared(leafItem, 1); + // Truncate the data to trigger an exception in SHAMapTreeNode::makeAccountState when + // the data is used to deserialize the node. + uint256 const tag; + auto const leafData = serializeNode(leafNode).substr(0, tag.kBytes - 1); + auto const result = getTreeNode(leafData); + BEAST_EXPECT(!result); + } + } + + void + testGetSHAMapNodeID() + { + testcase("getSHAMapNodeID"); + + { + // Tests using inner nodes at various depths. + auto const innerNode = intr_ptr::makeShared(1); + auto const childNode = intr_ptr::makeShared(1); + innerNode->setChild(0, childNode); + auto const innerData = serializeNode(innerNode); + + // Valid: legacy `nodeid` field at arbitrary depth. + { + auto const innerDepth = 3; + auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{}); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_nodeid(innerID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(result == innerID); + } + + // Valid: new `id` field at minimum depth. + { + auto const innerDepth = 0; + auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{}); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_id(innerID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(result == innerID); + } + + // Invalid: new `depth` field should not be used for inner nodes. + { + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_depth(10); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(!result); + } + + // Invalid: both legacy `nodeid` and new `id` fields set for an inner node. + { + auto const innerDepth = 9; + auto const innerID = SHAMapNodeID::createID(innerDepth, uint256{}); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(innerData); + ledgerNode.set_nodeid(innerID.getRawString()); + ledgerNode.set_id(innerID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(!result); + } + } + + { + // Tests using leaf nodes at various depths. + auto const leafItem = makeTestItem(12345); + auto const leafNode = intr_ptr::makeShared(leafItem, 1); + auto const leafData = serializeNode(leafNode); + auto const leafKey = leafItem->key(); + + // Valid: legacy `nodeid` field at arbitrary depth. + { + auto const kLeafDepth = 5; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_nodeid(leafID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Invalid: new `id` field should not be used for leaf nodes. + { + auto const kLeafDepth = 5; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_id(leafID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(!result); + } + + // Valid: new `depth` field at minimum depth. + { + auto const kLeafDepth = 0; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_depth(kLeafDepth); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Valid: new `depth` field at arbitrary depth between minimum and maximum. + { + auto const kLeafDepth = 10; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_depth(kLeafDepth); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Valid: new `depth` field at maximum depth. + // Note that we do not test a depth greater than the maximum depth, because the proto + // message is assumed to have been validated by the time the getSHAMapNodeID function is + // called. + { + auto const kLeafDepth = SHAMap::kLeafDepth; + auto const leafID = SHAMapNodeID::createID(kLeafDepth, leafKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(leafData); + ledgerNode.set_depth(kLeafDepth); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(result == leafID); + } + + // Invalid: legacy `nodeid` field where the node ID is inconsistent with the key. + { + auto const otherItem = makeTestItem(54321); + auto const otherNode = + intr_ptr::makeShared(otherItem, 1); + auto const otherData = serializeNode(otherNode); + auto const otherKey = otherItem->key(); + auto const otherDepth = 1; + auto const otherID = SHAMapNodeID::createID(otherDepth, otherKey); + + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata(otherData); + ledgerNode.set_nodeid(otherID.getRawString()); + auto const result = getSHAMapNodeID(ledgerNode, *leafNode); + BEAST_EXPECT(!result); + } + } + + // Invalid: no field set. + { + auto const innerNode = intr_ptr::makeShared(1); + protocol::TMLedgerNode ledgerNode; + ledgerNode.set_nodedata("test_data"); + auto const result = getSHAMapNodeID(ledgerNode, *innerNode); + BEAST_EXPECT(!result); + } + } + +public: + void + run() override + { + testGetTreeNode(); + testGetSHAMapNodeID(); + } +}; + +BEAST_DEFINE_TESTSUITE(LedgerNodeHelpers, app, xrpl); + +} // namespace xrpl::tests diff --git a/src/test/overlay/ProtocolVersion_test.cpp b/src/test/overlay/ProtocolVersion_test.cpp index 2fc8e4447d..e31a574502 100644 --- a/src/test/overlay/ProtocolVersion_test.cpp +++ b/src/test/overlay/ProtocolVersion_test.cpp @@ -63,8 +63,8 @@ public: negotiateProtocolVersion("RTXP/1.2, XRPL/2.0, XRPL/2.1") == makeProtocol(2, 1)); BEAST_EXPECT(negotiateProtocolVersion("XRPL/2.2") == makeProtocol(2, 2)); BEAST_EXPECT( - negotiateProtocolVersion("RTXP/1.2, XRPL/2.2, XRPL/2.3, XRPL/999.999") == - makeProtocol(2, 2)); + negotiateProtocolVersion("RTXP/1.2, XRPL/2.3, XRPL/2.4, XRPL/999.999") == + makeProtocol(2, 3)); BEAST_EXPECT(negotiateProtocolVersion("XRPL/999.999, WebSocket/1.0") == std::nullopt); BEAST_EXPECT(negotiateProtocolVersion("") == std::nullopt); } diff --git a/src/tests/libxrpl/shamap/SHAMapSync.cpp b/src/tests/libxrpl/shamap/SHAMapSync.cpp index 5cefbae8a1..400509d217 100644 --- a/src/tests/libxrpl/shamap/SHAMapSync.cpp +++ b/src/tests/libxrpl/shamap/SHAMapSync.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -112,14 +111,16 @@ TEST_F(SHAMapSyncTest, sync) destination.setSynching(); { - std::vector> a; + std::vector a; ASSERT_TRUE(source.getNodeFat(SHAMapNodeID(), a, randBool(eng_), randInt(eng_, 2))); ASSERT_FALSE(a.empty()) << "NodeSize"; - ASSERT_TRUE( - destination.addRootNode(source.getHash(), makeSlice(a[0].second), nullptr).isGood()); + auto node = SHAMapTreeNode::makeFromWire(makeSlice(a[0].data)); + if (!node) + FAIL() << "Could not create node"; + ASSERT_TRUE(destination.addRootNode(source.getHash(), std::move(node), nullptr).isGood()); } do @@ -133,7 +134,7 @@ TEST_F(SHAMapSyncTest, sync) break; // get as many nodes as possible based on this information - std::vector> b; + std::vector b; for (auto& it : nodesMissing) { @@ -155,7 +156,12 @@ TEST_F(SHAMapSyncTest, sync) // Keep failures fatal here because this loop is data-dependent. // non-deterministic number of times and the number of tests run // should be deterministic - if (!destination.addKnownNode(i.first, makeSlice(i.second), nullptr).isUseful()) + auto node = SHAMapTreeNode::makeFromWire(makeSlice(i.data)); + if (!node) + FAIL() << "Could not create node"; + if (i.isLeaf != node->isLeaf()) + FAIL() << "Node is not a leaf"; + if (!destination.addKnownNode(i.nodeID, std::move(node), nullptr).isUseful()) FAIL() << "Known node was not useful"; } } while (true); diff --git a/src/xrpld/app/ledger/InboundLedger.h b/src/xrpld/app/ledger/InboundLedger.h index 31ca4169ce..9a7ee510f6 100644 --- a/src/xrpld/app/ledger/InboundLedger.h +++ b/src/xrpld/app/ledger/InboundLedger.h @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -24,7 +23,7 @@ #include #include #include -#include +#include #include #include @@ -154,16 +153,19 @@ private: processData(std::shared_ptr peer, protocol::TMLedgerData const& data); bool - takeHeader(std::string const& data); + takeHeader(std::string_view data); void - receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode&); + receiveNode( + std::shared_ptr const& peer, + protocol::TMLedgerData const& packet, + SHAMapAddNode& san); bool - takeTxRootNode(Slice const& data, SHAMapAddNode&); + takeTxRootNode(std::string_view data, SHAMapAddNode& san); bool - takeAsRootNode(Slice const& data, SHAMapAddNode&); + takeAsRootNode(std::string_view data, SHAMapAddNode& san); std::vector neededTxHashes(int max, SHAMapSyncFilter const* filter) const; diff --git a/src/xrpld/app/ledger/LedgerNodeHelpers.h b/src/xrpld/app/ledger/LedgerNodeHelpers.h new file mode 100644 index 0000000000..9df9ab06c7 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerNodeHelpers.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include +#include + +namespace protocol { +class TMLedgerNode; +} // namespace protocol + +namespace xrpl { + +/** + * @brief Deserializes a SHAMapTreeNode from wire format data. + * + * This function attempts to create a SHAMapTreeNode from the provided data string. If the data is + * malformed or deserialization fails, the function returns a nullptr instead of throwing an + * exception. + * + * @param data The serialized node data in wire format. + * @return The deserialized tree node if successful, or a nullptr if deserialization fails. + */ +[[nodiscard]] SHAMapTreeNodePtr +getTreeNode(std::string_view data); + +/** + * @brief Extracts or reconstructs the SHAMapNodeID from a ledger node proto message. + * + * This function retrieves the SHAMapNodeID for a tree node, with behavior that depends on which + * field is set and the node type (inner vs. leaf). + * + * When the legacy `nodeid` field is set in the message: + * - For all nodes: Deserializes the node ID from the field. + * - For leaf nodes: Validates that the node ID is consistent with the leaf's key. + * + * When the new `id` or `depth` field is set in the message: + * - For inner nodes: Deserializes the node ID from the `id` field. + * - For leaf nodes: Reconstructs the node ID using both the depth from the `depth` field and the + * key from the leaf node's item. + * Note that root nodes may be inner nodes or leaf nodes. + * + * @param ledgerNode The validated protocol message containing the ledger node data. + * @param treeNode The deserialized tree node (inner or leaf node). + * @return An optional containing the node ID if extraction/reconstruction succeeds, or std::nullopt + * if the required fields are missing or validation fails. + */ +[[nodiscard]] std::optional +getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const& treeNode); + +} // namespace xrpl diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index 55a2a9d283..b3dafcf5e6 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -44,8 +45,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -779,7 +780,7 @@ InboundLedger::filterNodes( */ // data must not have hash prefix bool -InboundLedger::takeHeader(std::string const& data) +InboundLedger::takeHeader(std::string_view data) { // Return value: true=normal, false=bad data JLOG(journal_.trace()) << "got header acquiring ledger " << hash_; @@ -825,7 +826,10 @@ InboundLedger::takeHeader(std::string const& data) * Call with a lock */ void -InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& san) +InboundLedger::receiveNode( + std::shared_ptr const& peer, + protocol::TMLedgerData const& packet, + SHAMapAddNode& san) { if (!haveHeader_) { @@ -868,32 +872,47 @@ InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& { auto const f = filter.get(); - for (auto const& node : packet.nodes()) + for (auto const& ledgerNode : packet.nodes()) { - auto const nodeID = deserializeSHAMapNodeID(node.nodeid()); + auto treeNode = getTreeNode(ledgerNode.nodedata()); + if (!treeNode) + { + JLOG(journal_.warn()) + << "Got invalid node data for ledger " << hash_ << " from peer " << peer->id(); + peer->charge(Resource::kFeeInvalidData, "ledger_node.node_data invalid"); + san.incInvalid(); + return; + } + auto const nodeID = getSHAMapNodeID(ledgerNode, *treeNode); if (!nodeID) - throw std::runtime_error("data does not properly deserialize"); - - if (nodeID->isRoot()) { - san += map.addRootNode(rootHash, makeSlice(node.nodedata()), f); - } - else - { - san += map.addKnownNode(*nodeID, makeSlice(node.nodedata()), f); + JLOG(journal_.warn()) + << "Got invalid node id for ledger " << hash_ << " from peer " << peer->id(); + peer->charge(Resource::kFeeInvalidData, "ledger_node.node_id invalid"); + san.incInvalid(); + return; } - if (!san.isGood()) + auto const result = nodeID->isRoot() + ? map.addRootNode(rootHash, std::move(treeNode), f) + : map.addKnownNode(*nodeID, std::move(treeNode), f); + san += result; + + if (result.isInvalid()) { - JLOG(journal_.warn()) << "Received bad node data"; + JLOG(journal_.warn()) << "Got invalid node " << *nodeID << " for ledger " << hash_ + << " from peer " << peer->id(); + peer->charge(Resource::kFeeInvalidData, "ledger_node invalid"); return; } } } catch (std::exception const& e) { - JLOG(journal_.error()) << "Received bad node data: " << e.what(); + // If we get here it is not necessarily because the node was bad, so don't charge the peer. + JLOG(journal_.error()) << "Could not process node for ledger " << hash_ << " from peer " + << peer->id() << ": " << e.what(); san.incInvalid(); return; } @@ -922,7 +941,7 @@ InboundLedger::receiveNode(protocol::TMLedgerData const& packet, SHAMapAddNode& * Call with a lock */ bool -InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) +InboundLedger::takeAsRootNode(std::string_view data, SHAMapAddNode& san) { if (failed_ || haveState_) { @@ -938,10 +957,19 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) // LCOV_EXCL_STOP } + auto treeNode = getTreeNode(data); + if (!treeNode) + { + JLOG(journal_.warn()) << "Got invalid AS root node data for ledger " << hash_; + san.incInvalid(); + return false; + } + AccountStateSF filter(ledger_->stateMap().family().db(), app_.getLedgerMaster()); - san += - ledger_->stateMap().addRootNode(SHAMapHash{ledger_->header().accountHash}, data, &filter); - return san.isGood(); + auto const result = ledger_->stateMap().addRootNode( + SHAMapHash{ledger_->header().accountHash}, std::move(treeNode), &filter); + san += result; + return !result.isInvalid(); } /** @@ -949,7 +977,7 @@ InboundLedger::takeAsRootNode(Slice const& data, SHAMapAddNode& san) * Call with a lock */ bool -InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san) +InboundLedger::takeTxRootNode(std::string_view data, SHAMapAddNode& san) { if (failed_ || haveTransactions_) { @@ -965,9 +993,19 @@ InboundLedger::takeTxRootNode(Slice const& data, SHAMapAddNode& san) // LCOV_EXCL_STOP } + auto treeNode = getTreeNode(data); + if (!treeNode) + { + JLOG(journal_.warn()) << "Got invalid TX root node data for ledger " << hash_; + san.incInvalid(); + return false; + } + TransactionStateSF filter(ledger_->txMap().family().db(), app_.getLedgerMaster()); - san += ledger_->txMap().addRootNode(SHAMapHash{ledger_->header().txHash}, data, &filter); - return san.isGood(); + auto const result = ledger_->txMap().addRootNode( + SHAMapHash{ledger_->header().txHash}, std::move(treeNode), &filter); + san += result; + return !result.isInvalid(); } std::vector @@ -1065,20 +1103,33 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co } if (!haveState_ && (packet.nodes().size() > 1) && - !takeAsRootNode(makeSlice(packet.nodes(1).nodedata()), san)) + !takeAsRootNode(packet.nodes(1).nodedata(), san)) { - JLOG(journal_.warn()) << "Included AS root invalid"; + JLOG(journal_.warn()) << "Included AS root invalid for ledger " << hash_ + << " from peer " << peer->id(); + if (san.isInvalid()) + { + peer->charge(Resource::kFeeInvalidData, "ledger_data invalid AS root"); + return -1; + } } if (!haveTransactions_ && (packet.nodes().size() > 2) && - !takeTxRootNode(makeSlice(packet.nodes(2).nodedata()), san)) + !takeTxRootNode(packet.nodes(2).nodedata(), san)) { - JLOG(journal_.warn()) << "Included TX root invalid"; + JLOG(journal_.warn()) << "Included TX root invalid for ledger " << hash_ + << " from peer " << peer->id(); + if (san.isInvalid()) + { + peer->charge(Resource::kFeeInvalidData, "ledger_data invalid TX root"); + return -1; + } } } catch (std::exception const& ex) { - JLOG(journal_.warn()) << "Included AS/TX root invalid: " << ex.what(); + JLOG(journal_.warn()) << "Included AS/TX root invalid for ledger " << hash_ + << " from peer " << peer->id() << ": " << ex.what(); using namespace std::string_literals; peer->charge(Resource::kFeeInvalidData, "ledger_data "s + ex.what()); return -1; @@ -1102,24 +1153,18 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co ScopedLockType const sl(mtx_); - // Verify node IDs and data are complete - for (auto const& node : packet.nodes()) - { - if (!node.has_nodeid() || !node.has_nodedata()) - { - JLOG(journal_.warn()) << "Got bad node"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data bad node"); - return -1; - } - } - SHAMapAddNode san; - receiveNode(packet, san); + receiveNode(peer, packet, san); JLOG(journal_.debug()) << "Ledger " << ((packet.type() == protocol::liTX_NODE) ? "TX" : "AS") << " node stats: " << san.get(); + // `san` accumulates across the whole packet, so `isInvalid()` (bad_ > 0) does not mean the + // packet had no useful nodes: credit whatever good/useful nodes were sent rather than + // discarding everything because one node in an otherwise-good packet was bad. + // Note: Peer charges for invalid/malformed data are issued from within receiveNode at the + // exact failure site, so the peer is only charged for problems they are responsible for. if (san.isUseful()) progress_ = true; diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp b/src/xrpld/app/ledger/detail/InboundLedgers.cpp index dc361694cf..4d565ca674 100644 --- a/src/xrpld/app/ledger/detail/InboundLedgers.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp @@ -2,13 +2,13 @@ #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -252,23 +252,17 @@ public: Serializer s; try { - for (int i = 0; i < packetPtr->nodes().size(); ++i) + for (auto const& ledgerNode : packetPtr->nodes()) { - auto const& node = packetPtr->nodes(i); - - if (!node.has_nodeid() || !node.has_nodedata()) - return; - - auto newNode = SHAMapTreeNode::makeFromWire(makeSlice(node.nodedata())); - - if (!newNode) + auto const treeNode = getTreeNode(ledgerNode.nodedata()); + if (!treeNode) return; s.erase(); - newNode->serializeWithPrefix(s); + treeNode->serializeWithPrefix(s); app_.getLedgerMaster().addFetchPack( - newNode->getHash().asUInt256(), std::make_shared(s.begin(), s.end())); + treeNode->getHash().asUInt256(), std::make_shared(s.begin(), s.end())); } } catch (std::exception const&) // NOLINT(bugprone-empty-catch) diff --git a/src/xrpld/app/ledger/detail/InboundTransactions.cpp b/src/xrpld/app/ledger/detail/InboundTransactions.cpp index 9b50a1584f..d735a97d28 100644 --- a/src/xrpld/app/ledger/detail/InboundTransactions.cpp +++ b/src/xrpld/app/ledger/detail/InboundTransactions.cpp @@ -1,11 +1,11 @@ #include +#include #include #include #include #include -#include #include #include #include @@ -14,6 +14,7 @@ #include #include #include +#include #include @@ -137,34 +138,45 @@ public: if (ta == nullptr) { - peer->charge(Resource::kFeeUselessData, "ledger_data"); + peer->charge(Resource::kFeeUselessData, "ledger_data useless"); return; } - std::vector> data; + std::vector> data; data.reserve(packet.nodes().size()); - for (auto const& node : packet.nodes()) + for (auto const& ledgerNode : packet.nodes()) { - if (!node.has_nodeid() || !node.has_nodedata()) + auto treeNode = getTreeNode(ledgerNode.nodedata()); + if (!treeNode) { - peer->charge(Resource::kFeeMalformedRequest, "ledger_data"); + JLOG(j_.warn()) << "Got invalid node data for TX set " << hash << " from peer " + << peer->id(); + peer->charge(Resource::kFeeInvalidData, "ledger_node.node_data invalid"); return; } - auto const id = deserializeSHAMapNodeID(node.nodeid()); - - if (!id) + auto const nodeID = getSHAMapNodeID(ledgerNode, *treeNode); + if (!nodeID) { - peer->charge(Resource::kFeeInvalidData, "ledger_data"); + JLOG(j_.warn()) << "Got invalid node id for TX set " << hash << " from peer " + << peer->id(); + peer->charge(Resource::kFeeInvalidData, "ledger_node.node_id invalid"); return; } - data.emplace_back(*id, makeSlice(node.nodedata())); + data.emplace_back(*nodeID, std::move(treeNode)); } - if (!ta->takeNodes(data, peer).isUseful()) - peer->charge(Resource::kFeeUselessData, "ledger_data not useful"); + auto const san = ta->takeNodes(std::move(data), peer); + if (san.isInvalid()) + { + peer->charge(Resource::kFeeInvalidData, "ledger_data invalid"); + } + else if (!san.isUseful()) + { + peer->charge(Resource::kFeeUselessData, "ledger_data useless"); + } } void diff --git a/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp b/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp new file mode 100644 index 0000000000..531dba59f9 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp @@ -0,0 +1,89 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace xrpl { + +SHAMapTreeNodePtr +getTreeNode(std::string_view data) +{ + auto const slice = makeSlice(data); + try + { + return SHAMapTreeNode::makeFromWire(slice); + } + catch (std::exception const&) + { + return {}; + } +} + +std::optional +getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const& treeNode) +{ + if (ledgerNode.has_id() || ledgerNode.has_depth()) + { + // Reject ambiguous messages that mix the legacy and new reference fields. + if (ledgerNode.has_nodeid()) + return std::nullopt; + + if (treeNode.isInner()) + { + if (!ledgerNode.has_id()) + return std::nullopt; + + REACHABLE("xrpl::getSHAMapNodeID : inner node ID from id field"); + return deserializeSHAMapNodeID(ledgerNode.id()); + } + + if (treeNode.isLeaf()) + { + SOMETIMES( + ledgerNode.has_depth() && ledgerNode.depth() > SHAMap::kLeafDepth, + "xrpl::getSHAMapNodeID : leaf depth exceeds max"); + if (!ledgerNode.has_depth() || ledgerNode.depth() > SHAMap::kLeafDepth) + return std::nullopt; + + auto const key = leafKey(treeNode); + REACHABLE("xrpl::getSHAMapNodeID : leaf node ID reconstructed from depth"); + return SHAMapNodeID::createID(ledgerNode.depth(), key); + } + // LCOV_EXCL_START + UNREACHABLE("xrpl::getSHAMapNodeID : tree node is neither inner nor leaf"); + return std::nullopt; + // LCOV_EXCL_STOP + } + + if (!ledgerNode.has_nodeid()) + return std::nullopt; + + auto nodeID = deserializeSHAMapNodeID(ledgerNode.nodeid()); + if (!nodeID.has_value()) + return std::nullopt; + + if (treeNode.isLeaf()) + { + auto const key = leafKey(treeNode); + auto const expectedID = SHAMapNodeID::createID(static_cast(nodeID->getDepth()), key); + SOMETIMES( + nodeID->getNodeID() != expectedID.getNodeID(), + "xrpl::getSHAMapNodeID : legacy leaf ID inconsistent with key"); + if (nodeID->getNodeID() != expectedID.getNodeID()) + return std::nullopt; + } + + return nodeID; +} + +} // namespace xrpl diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.cpp b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp index 62312b04d2..db99299fd6 100644 --- a/src/xrpld/app/ledger/detail/TransactionAcquire.cpp +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp @@ -7,13 +7,13 @@ #include #include -#include #include #include #include #include #include #include +#include #include @@ -171,7 +171,7 @@ TransactionAcquire::trigger(std::shared_ptr const& peer) SHAMapAddNode TransactionAcquire::takeNodes( - std::vector> const& data, + std::vector> data, std::shared_ptr const& peer) { ScopedLockType const sl(mtx_); @@ -195,7 +195,7 @@ TransactionAcquire::takeNodes( ConsensusTransSetSF sf(app_, app_.getTempNodeCache()); - for (auto const& d : data) + for (auto& d : data) { if (d.first.isRoot()) { @@ -203,18 +203,22 @@ TransactionAcquire::takeNodes( { JLOG(journal_.debug()) << "Got root TXS node, already have it"; } - else if (!map_->addRootNode(SHAMapHash{hash_}, d.second, nullptr).isGood()) + else if (!map_->addRootNode(SHAMapHash{hash_}, std::move(d.second), nullptr) + .isGood()) { - JLOG(journal_.warn()) << "TX acquire got bad root node"; + JLOG(journal_.warn()) << "TX acquire got bad root node for TX set " << hash_ + << " from peer " << peer->id(); + return SHAMapAddNode::invalid(); } else { haveRoot_ = true; } } - else if (!map_->addKnownNode(d.first, d.second, &sf).isGood()) + else if (!map_->addKnownNode(d.first, std::move(d.second), &sf).isGood()) { - JLOG(journal_.warn()) << "TX acquire got bad non-root node"; + JLOG(journal_.warn()) << "TX acquire got bad non-root node " << d.first + << " for TX set " << hash_ << " from peer " << peer->id(); return SHAMapAddNode::invalid(); } } diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.h b/src/xrpld/app/ledger/detail/TransactionAcquire.h index 5b33066390..2faf74b557 100644 --- a/src/xrpld/app/ledger/detail/TransactionAcquire.h +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.h @@ -6,10 +6,10 @@ #include #include -#include #include #include #include +#include #include #include @@ -32,8 +32,8 @@ public: SHAMapAddNode takeNodes( - std::vector> const& data, - std::shared_ptr const&); + std::vector> data, + std::shared_ptr const& peer); void init(int startPeers); diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h index 23a45dc512..20a8730cf1 100644 --- a/src/xrpld/overlay/Peer.h +++ b/src/xrpld/overlay/Peer.h @@ -23,6 +23,7 @@ enum class ProtocolFeature { ValidatorListPropagation, ValidatorList2Propagation, LedgerReplay, + LedgerNodeDepth, }; /** diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index d7a9a9e449..688d0ac314 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,7 @@ #include #include #include +#include #include #include @@ -543,6 +545,8 @@ PeerImp::supportsFeature(ProtocolFeature f) const return protocol_ >= makeProtocol(2, 1); case ProtocolFeature::ValidatorList2Propagation: return protocol_ >= makeProtocol(2, 2); + case ProtocolFeature::LedgerNodeDepth: + return protocol_ >= makeProtocol(2, 3); case ProtocolFeature::LedgerReplay: return ledgerReplayEnabled_; } @@ -1477,23 +1481,12 @@ PeerImp::onMessage(std::shared_ptr const& m) } } - // Verify ledger node IDs - if (itype != protocol::liBASE) + // Verify ledger node counts. Full parsing of the node IDs is deferred to the job, so the I/O + // thread is not burdened with SHAMapNodeID deserialization for every TMGetLedger message. + if (itype != protocol::liBASE && m->nodeids_size() <= 0) { - if (m->nodeids_size() <= 0) - { - badData("Invalid ledger node IDs"); - return; - } - - for (auto const& nodeId : m->nodeids()) - { - if (deserializeSHAMapNodeID(nodeId) == std::nullopt) - { - badData("Invalid SHAMap node ID"); - return; - } - } + badData("Invalid ledger node IDs"); + return; } // Verify query type @@ -1513,11 +1506,57 @@ PeerImp::onMessage(std::shared_ptr const& m) } } - // Queue a job to process the request + // Queue a job to process the request. std::weak_ptr const weak = shared_from_this(); - app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m]() { - if (auto peer = weak.lock()) - peer->processLedgerRequest(m); + app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m, itype]() { + auto peer = weak.lock(); + if (!peer) + return; + + std::vector nodeIDs; + bool tooManyNodeIds = false; + if (itype != protocol::liBASE) + { + nodeIDs.reserve(std::min(m->nodeids_size(), Tuning::kSoftMaxReplyNodes)); + for (auto const& nodeId : m->nodeids()) + { + if (nodeIDs.size() >= Tuning::kSoftMaxReplyNodes) + { + // The peer requested too many node IDs. Continue processing the received node + // IDs up to the limit. If the request is legitimate then at least they will get + // a response and won't have to resend these nodes in their next request. + tooManyNodeIds = true; + break; + } + auto parsed = deserializeSHAMapNodeID(nodeId); + if (!parsed) + { + peer->charge(Resource::kFeeInvalidData, "TMGetLedger: Invalid node ID"); + return; + } + nodeIDs.push_back(std::move(*parsed)); + } + } + + // These are two distinct infractions and are charged independently: requesting too many + // node IDs is charged even for a relay response, while the base "get ledger request" charge + // below is skipped for relay responses. + if (tooManyNodeIds) + { + peer->charge(Resource::kFeeModerateBurdenPeer, "TMGetLedger: too many node IDs"); + + // Truncate the request to what was actually parsed and charged for, so that if this + // request ends up being relayed to another peer, we don't forward the oversized list. + m->mutable_nodeids()->DeleteSubrange( + static_cast(nodeIDs.size()), + m->nodeids_size() - static_cast(nodeIDs.size())); + } + if (!m->has_requestcookie()) + { + peer->charge(Resource::kFeeModerateBurdenPeer, "TMGetLedger: get ledger request"); + } + + peer->processLedgerRequest(m, std::move(nodeIDs)); }); } @@ -1682,12 +1721,119 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - // If there is a request cookie, attempt to relay the message + // If there is a request cookie, attempt to relay the message. if (m->has_requestcookie()) { if (auto peer = overlay_.findPeerByShortID(m->requestcookie())) { m->clear_requestcookie(); + + // If the original requester doesn't support the new depth-based format, rewrite any + // nodes that use it back to the legacy nodeid format before relaying. Once all nodes + // have upgraded, the old protocol version and this code can be removed. Make sure that + // the format of the nodes is consistent - either all use the legacy format or the new + // format, unless it is liBASE data in which case none of these fields should be set. + auto const peerSupportsNodeDepth = + peer->supportsFeature(ProtocolFeature::LedgerNodeDepth); + enum class MessageType { Unknown, Base, Legacy, Depth }; + MessageType messageType = MessageType::Unknown; + for (int i = 0; i < m->nodes_size(); ++i) + { + auto* ledgerNode = m->mutable_nodes(i); + + // All nodes should have non-empty data. The field is required so we don't need to + // check for presence first. + if (ledgerNode->nodedata().empty()) + { + badData( + "Received node with empty data while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + + MessageType msgType = MessageType::Unknown; + if (m->type() == protocol::liBASE) + { + if (ledgerNode->has_nodeid() || ledgerNode->has_id() || ledgerNode->has_depth()) + { + badData( + "Received liBASE message with node reference while relaying ledger " + "data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + msgType = MessageType::Base; + } + else + { + msgType = ledgerNode->has_nodeid() ? MessageType::Legacy : MessageType::Depth; + } + if (messageType != MessageType::Unknown && messageType != msgType) + { + badData( + "Received mixed mode message while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + messageType = msgType; + + if (peerSupportsNodeDepth || msgType != MessageType::Depth) + continue; + + SOMETIMES( + !peerSupportsNodeDepth, + "xrpl::PeerImp : relaying depth-format ledger data to pre-2.3 peer"); + switch (ledgerNode->reference_case()) + { + case protocol::TMLedgerNode::kId: { + // We can directly copy the `id` field, because it uses the same wire format + // as the legacy `nodeid` field. + REACHABLE("xrpl::PeerImp : relay downgrade id to nodeid"); + ledgerNode->set_nodeid(ledgerNode->id()); + ledgerNode->clear_id(); + break; + } + case protocol::TMLedgerNode::kDepth: { + // We need to regenerate the node ID from the node data and depth. + auto treeNode = getTreeNode(ledgerNode->nodedata()); + if (!treeNode) + { + badData( + "Unable to get tree node while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + + auto const nodeID = getSHAMapNodeID(*ledgerNode, *treeNode); + if (!nodeID) + { + badData( + "Unable to get node ID while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + + REACHABLE("xrpl::PeerImp : relay downgrade depth to nodeid"); + ledgerNode->set_nodeid(nodeID->getRawString()); + ledgerNode->clear_depth(); + break; + } + default: { + SOMETIMES(true, "xrpl::PeerImp : relay node has empty reference"); + badData( + "Empty node reference while relaying ledger data for " + + to_string(uint256::fromRaw(m->ledgerhash())) + " to peer " + + std::to_string(peer->id())); + return; + } + } + } + peer->send(std::make_shared(*m, protocol::mtLEDGER_DATA)); } else @@ -3287,12 +3433,10 @@ PeerImp::getTxSet(std::shared_ptr const& m) const } void -PeerImp::processLedgerRequest(std::shared_ptr const& m) +PeerImp::processLedgerRequest( + std::shared_ptr const& m, + std::vector nodeIDs) { - // Do not resource charge a peer responding to a relay - if (!m->has_requestcookie()) - charge(Resource::kFeeModerateBurdenPeer, "received a get ledger request"); - std::shared_ptr ledger; std::shared_ptr sharedMap; SHAMap const* map{nullptr}; @@ -3372,26 +3516,25 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) } // Add requested node data to reply - if (m->nodeids_size() > 0) + if (!nodeIDs.empty()) { std::uint32_t const defaultDepth = isHighLatency() ? 2 : 1; auto const queryDepth{m->has_querydepth() ? m->querydepth() : defaultDepth}; - std::vector> data; + std::vector data; + data.reserve(Tuning::kSoftMaxReplyNodes); + auto const useLedgerNodeDepth = supportsFeature(ProtocolFeature::LedgerNodeDepth); - for (int i = 0; - i < m->nodeids_size() && ledgerData.nodes_size() < Tuning::kSoftMaxReplyNodes; - ++i) + for (auto const& nodeID : nodeIDs) { - auto const shaMapNodeId{deserializeSHAMapNodeID(m->nodeids(i))}; + if (ledgerData.nodes_size() >= Tuning::kSoftMaxReplyNodes) + break; data.clear(); - data.reserve(Tuning::kSoftMaxReplyNodes); try { - // NOLINTNEXTLINE(bugprone-unchecked-optional-access) nodeids checked in onGetLedger - if (map->getNodeFat(*shaMapNodeId, data, fatLeaves, queryDepth)) + if (map->getNodeFat(nodeID, data, fatLeaves, queryDepth)) { JLOG(pJournal_.trace()) << "processLedgerRequest: getNodeFat got " << data.size() << " nodes"; @@ -3400,9 +3543,27 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) { if (ledgerData.nodes_size() >= Tuning::kHardMaxReplyNodes) break; + protocol::TMLedgerNode* node{ledgerData.add_nodes()}; - node->set_nodeid(d.first.getRawString()); - node->set_nodedata(d.second.data(), d.second.size()); + node->set_nodedata(d.data.data(), d.data.size()); + + // When the LedgerNodeDepth protocol feature is not supported by the peer, + // we always set the `nodeid` field. However, when it is supported then we + // set the `id` field for inner nodes and the `depth` field for leaf nodes. + if (!useLedgerNodeDepth) + { + node->set_nodeid(d.nodeID.getRawString()); + } + else if (d.isLeaf) + { + REACHABLE("xrpl::PeerImp : emit leaf depth in reply"); + node->set_depth(d.nodeID.getDepth()); + } + else + { + REACHABLE("xrpl::PeerImp : emit inner id in reply"); + node->set_id(d.nodeID.getRawString()); + } } } else @@ -3441,13 +3602,13 @@ PeerImp::processLedgerRequest(std::shared_ptr const& m) info += ", no hash specified"; JLOG(pJournal_.warn()) - << "processLedgerRequest: getNodeFat with nodeId " << *shaMapNodeId + << "processLedgerRequest: getNodeFat with nodeId " << nodeID << " and ledger info type " << info << " throws exception: " << e.what(); } } JLOG(pJournal_.info()) << "processLedgerRequest: Got request for " << m->nodeids_size() - << " nodes at depth " << queryDepth << ", return " + << " node IDs at depth " << queryDepth << ", return " << ledgerData.nodes_size() << " nodes"; } diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 90f8a917f4..de90e60955 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 @@ -679,7 +680,9 @@ private: getTxSet(std::shared_ptr const& m) const; void - processLedgerRequest(std::shared_ptr const& m); + processLedgerRequest( + std::shared_ptr const& m, + std::vector nodeIDs); protected: // Kept `protected` so test subclasses (see diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp b/src/xrpld/overlay/detail/ProtocolVersion.cpp index 347e59accb..2d5d0a56f7 100644 --- a/src/xrpld/overlay/detail/ProtocolVersion.cpp +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp @@ -29,6 +29,7 @@ namespace xrpl { constexpr ProtocolVersion const kSupportedProtocolList[]{ {2, 1}, {2, 2}, + {2, 3}, }; // This ugly construct ensures that supportedProtocolList is sorted in strictly From 3ad6ce236eeeb72fd1208c8e225eedcca9b798c6 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 30 Jul 2026 16:29:37 +0100 Subject: [PATCH 55/86] 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 56/86] 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 85e73cbd32b9112ca5c524f256cb3581b86dd8c1 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 30 Jul 2026 20:28:31 +0100 Subject: [PATCH 57/86] ci: Run coverage first in CI (#7917) --- .github/scripts/strategy-matrix/linux.json | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 60f3da09f1..9510212344 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -2,12 +2,21 @@ "image_tag": "sha-fecfc0c", "configs": { "ubuntu": [ + { + "compiler": ["gcc"], + "build_type": ["Debug"], + "arch": ["amd64"], + "minimal": true, + "suffix": "coverage", + "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0" + }, { "compiler": ["clang"], "build_type": ["Release"], "arch": ["amd64"], "minimal": true }, + { "compiler": ["gcc"], "build_type": ["Release"], @@ -29,14 +38,6 @@ "sanitizers": ["address", "undefinedbehavior"] }, - { - "compiler": ["gcc"], - "build_type": ["Debug"], - "arch": ["amd64"], - "minimal": true, - "suffix": "coverage", - "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0" - }, { "compiler": ["clang"], "build_type": ["Debug"], From ecdd457f3598c7286a9af4aff358fbd30039173f Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Fri, 31 Jul 2026 00:04:38 +0100 Subject: [PATCH 58/86] chore: Gtest migration followups first pass (#7884) --- cmake/XrplAddBenchmark.cmake | 2 + src/benchmarks/libxrpl/nodestore/Backend.cpp | 39 +-- .../libxrpl/nodestore/NodeStoreBench.h | 31 +-- src/tests/libxrpl/basics/IntrusiveShared.cpp | 240 +++++++++++------- src/tests/libxrpl/basics/MallocTrim.cpp | 2 +- src/tests/libxrpl/basics/Number.cpp | 16 +- src/tests/libxrpl/basics/base58.cpp | 22 +- src/tests/libxrpl/basics/base_uint.cpp | 104 ++++---- src/tests/libxrpl/basics/join.cpp | 4 +- .../libxrpl/consensus/CensorshipDetector.cpp | 2 +- src/tests/libxrpl/csf/TrustGraph.h | 4 +- src/tests/libxrpl/csf/random.h | 9 +- src/tests/libxrpl/nodestore/Database.cpp | 6 +- src/tests/libxrpl/resource/Logic.cpp | 42 +-- src/tests/libxrpl/shamap/SHAMap.cpp | 17 +- src/tests/libxrpl/shamap/SHAMapSync.cpp | 29 ++- 16 files changed, 315 insertions(+), 254 deletions(-) diff --git a/cmake/XrplAddBenchmark.cmake b/cmake/XrplAddBenchmark.cmake index 1dd875dd61..921deb0658 100644 --- a/cmake/XrplAddBenchmark.cmake +++ b/cmake/XrplAddBenchmark.cmake @@ -1,3 +1,5 @@ +include_guard() + include(isolate_headers) # Define a benchmark executable for the module `name`. diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp index f854dbd3a7..cd3e15bd65 100644 --- a/src/benchmarks/libxrpl/nodestore/Backend.cpp +++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp @@ -20,21 +20,22 @@ namespace xrpl::node_store { namespace { -constexpr std::size_t kPoolSizes[] = {1000, 10000, 100000}; -constexpr int kThreadCounts[] = {1, 4, 8}; +constexpr auto kPoolSizes = std::to_array({1000, 10000, 100000}); +constexpr auto kThreadCounts = std::to_array({1, 4, 8}); constexpr std::size_t kBatchSize = 256; +constexpr std::size_t kMissRatio = 5; constexpr std::string_view kNamePrefix = "BM_Backend_"; constexpr std::string_view kNameSeparator = "/"; struct RunState { - std::unique_ptr harness; - Batch present; // prefix-1 objects, eligible to be stored - Batch recent; // prefix-1 objects in the "future" key space - std::vector missing; // prefix-2 keys that are never stored - std::vector shuffle; // [0, poolSize) permutation for random-like access - std::size_t avgPayload = 0; // mean getData().size() over `present` + std::unique_ptr harness; ///< backend under test, rebuilt per run + Batch present; ///< prefix-1 objects, eligible to be stored + Batch recent; ///< prefix-1 objects in the "future" key space + std::vector missing; ///< prefix-2 keys that are never stored + std::vector shuffle; ///< [0, poolSize) permutation for random-like access + std::size_t avgPayload = 0; ///< mean getData().size() over `present` void release() @@ -85,7 +86,7 @@ Workload const kInsert{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; backend.store(rs.present[index % poolSize]); }, .reportBytes = true, @@ -104,7 +105,7 @@ Workload const kFetch{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; std::shared_ptr result; backend.fetch(rs.present[index % poolSize]->getHash(), &result); benchmark::DoNotOptimize(result); @@ -118,7 +119,7 @@ Workload const kMissing{ .setup = [](SetupContext const& ctx) { ctx.rs.missing = makeMissingKeys(ctx.poolSize); }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; std::shared_ptr result; backend.fetch(rs.missing[index % poolSize], &result); benchmark::DoNotOptimize(result); @@ -139,10 +140,10 @@ Workload const kMixed{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; std::shared_ptr result; auto const pick = rs.shuffle[index % poolSize]; - if (index % 5 == 0) + if (index % kMissRatio == 0) { backend.fetch(rs.missing[pick], &result); } @@ -170,7 +171,7 @@ Workload const kWork{ }, .iterate = [](IterateContext const& ctx) { - auto& [rs, backend, index, poolSize] = ctx; + auto const& [rs, backend, index, poolSize] = ctx; auto const slot = index % poolSize; auto const pick = rs.shuffle[slot]; @@ -239,7 +240,7 @@ registerWorkload(BackendConfig const& bc, Workload const& w) { auto rs = std::make_shared(); auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)); - b->RangeMultiplier(10)->Range(kPoolSizes[0], kPoolSizes[std::size(kPoolSizes) - 1]); + b->RangeMultiplier(10)->Range(kPoolSizes.front(), kPoolSizes.back()); b->Threads(1)->Threads(4)->Threads(8)->UseRealTime(); return; @@ -249,14 +250,14 @@ registerWorkload(BackendConfig const& bc, Workload const& w) { for (auto const threads : kThreadCounts) { - if (poolSize % static_cast(threads) != 0) + if (poolSize % threads != 0) continue; auto rs = std::make_shared(); benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs)) ->Arg(poolSize) - ->Iterations(poolSize / static_cast(threads)) - ->Threads(threads) + ->Iterations(poolSize / threads) + ->Threads(static_cast(threads)) ->UseRealTime(); } } @@ -289,7 +290,7 @@ registerStoreBatch(BackendConfig const& bc) rs->harness = std::make_unique(cfg); rs->present = makePool(1, poolSize); rs->avgPayload = averagePayload(rs->present); - std::vector const batches = sliceBatches(rs->present, kBatchSize); + std::vector const batches = sliceFixedBatches(rs->present, kBatchSize); if (batches.empty()) { state.SkipWithError("pool smaller than one batch"); diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h index 57abf42e89..debdc5d47a 100644 --- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h +++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -40,18 +41,13 @@ inline void rngcpy(void* buffer, std::size_t bytes, Generator& g) { using result_type = typename Generator::result_type; - while (bytes >= sizeof(result_type)) + while (bytes > 0) { auto const v = g(); - std::memcpy(buffer, &v, sizeof(v)); - buffer = reinterpret_cast(buffer) + sizeof(v); - bytes -= sizeof(v); - } - - if (bytes > 0) - { - auto const v = g(); - std::memcpy(buffer, &v, bytes); + auto const chunk = std::min(bytes, sizeof(result_type)); + std::memcpy(buffer, &v, chunk); + buffer = reinterpret_cast(buffer) + chunk; + bytes -= chunk; } } @@ -145,7 +141,7 @@ makePool(std::uint8_t prefix, std::size_t count, std::size_t start = 0) Sequence seq(prefix); Batch pool; pool.reserve(count); - for (std::size_t i = 0; i < count; ++i) + for (auto i = 0uz; i < count; ++i) pool.push_back(seq.obj(start + i)); return pool; } @@ -158,7 +154,7 @@ makeMissingKeys(std::size_t count) Sequence seq(2); std::vector keys; keys.reserve(count); - for (std::size_t i = 0; i < count; ++i) + for (auto i = 0uz; i < count; ++i) keys.push_back(seq.key(i)); return keys; } @@ -206,16 +202,16 @@ inline std::vector makeShuffle(std::size_t size, std::uint64_t seed) { std::vector v(size); - std::iota(v.begin(), v.end(), std::size_t{0}); + std::ranges::iota(v, 0uz); beast::xor_shift_engine gen(seed); - std::shuffle(v.begin(), v.end(), gen); + std::ranges::shuffle(v, gen); return v; } // Partition a pool into fixed-size batches. Any trailing remainder shorter than // `batchSize` is dropped, so every returned batch has exactly `batchSize`. inline std::vector -sliceBatches(Batch const& pool, std::size_t batchSize) +sliceFixedBatches(Batch const& pool, std::size_t batchSize) { std::vector batches; if (batchSize == 0) @@ -228,13 +224,10 @@ sliceBatches(Batch const& pool, std::size_t batchSize) /** * @brief RAII owner of a NodeStore Backend opened on a private temporary directory. - * - * Member declaration order matters: `tempDir` is declared first so it is - * destroyed last, after the backend has closed and released its files. */ struct BackendHarness { - beast::TempDir tempDir; + beast::TempDir tempDir; ///< Declared first so it is destroyed last DummyScheduler scheduler; beast::Journal journal{beast::Journal::getNullSink()}; std::unique_ptr backend; diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp index e798cd1ccc..b9f8930b7b 100644 --- a/src/tests/libxrpl/basics/IntrusiveShared.cpp +++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp @@ -50,11 +50,11 @@ struct Barrier { std::mutex mtx; std::condition_variable cv; - int count; - int const initial; + std::size_t count; + std::size_t const initial; std::size_t generation{0}; - explicit Barrier(int n) : count(n), initial(n) + explicit Barrier(std::size_t n) : count(n), initial(n) { } @@ -217,7 +217,7 @@ TEST(IntrusiveSharedTest, basics) auto id = b->id; EXPECT_EQ(TIBase::getState(id), Alive); EXPECT_EQ(b->useCount(), 1); - for (int i = 0; i < 10; ++i) + for (auto i = 0uz; i < 10; ++i) { strong.push_back(b); } @@ -232,7 +232,7 @@ TEST(IntrusiveSharedTest, basics) id = b->id; EXPECT_EQ(TIBase::getState(id), Alive); EXPECT_EQ(b->useCount(), 1); - for (int i = 0; i < 10; ++i) + for (auto i = 0uz; i < 10; ++i) { weak.emplace_back(b); EXPECT_EQ(b->useCount(), 1); @@ -280,17 +280,17 @@ TEST(IntrusiveSharedTest, basics) TIBase::ResetStatesGuard const rsg{true}; using enum TrackedState; - using swu = SharedWeakUnion; - swu b = makeSharedIntrusive(); + using SharedWeak = SharedWeakUnion; + SharedWeak b = makeSharedIntrusive(); EXPECT_TRUE(b.isStrong() && b.useCount() == 1); auto id = b.get()->id; EXPECT_EQ(TIBase::getState(id), Alive); - swu w = b; + SharedWeak w = b; EXPECT_TRUE(TIBase::getState(id) == Alive); EXPECT_TRUE(w.isStrong() && b.useCount() == 2); w.convertToWeak(); EXPECT_TRUE(w.isWeak() && b.useCount() == 1); - swu s = w; + SharedWeak s = w; EXPECT_TRUE(s.isWeak() && b.useCount() == 1); s.convertToStrong(); EXPECT_TRUE(s.isStrong() && b.useCount() == 2); @@ -380,43 +380,57 @@ TEST(IntrusiveSharedTest, partial_delete) std::atomic destructorRan{false}; std::atomic partialDeleteRan{false}; std::latch partialDeleteStartedSyncPoint{2}; + strong->tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; - if (next == DeletedStarted) + if (!next) + return; + + switch (*next) { - // strong goes out of scope while weak is still in scope - // This checks that partialDelete has run to completion - // before the destructor is called. A sleep is inserted - // inside the partial delete to make sure the destructor is - // given an opportunity to run during partial delete. - EXPECT_EQ(cur, PartiallyDeleted); - } - if (next == PartiallyDeletedStarted) - { - partialDeleteStartedSyncPoint.arrive_and_wait(); - using namespace std::chrono_literals; - // Sleep and let the weak pointer go out of scope, - // potentially triggering a destructor while partial delete - // is running. The test is to make sure that doesn't happen. - std::this_thread::sleep_for(800ms); - } - if (next == PartiallyDeleted) - { - EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan.exchange(true)); + case DeletedStarted: + // strong goes out of scope while weak is still in scope + // This checks that partialDelete has run to completion + // before the destructor is called. A sleep is inserted + // inside the partial delete to make sure the destructor is + // given an opportunity to run during partial delete. + EXPECT_EQ(cur, PartiallyDeleted); + break; + + case PartiallyDeletedStarted: { + partialDeleteStartedSyncPoint.arrive_and_wait(); + using namespace std::chrono_literals; + // Sleep and let the weak pointer go out of scope, + // potentially triggering a destructor while partial delete + // is running. The test is to make sure that doesn't happen. + std::this_thread::sleep_for(800ms); + break; + } + + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + break; + + case Deleted: + EXPECT_FALSE(destructorRan.exchange(true)); + break; + + case Uninitialized: + case Alive: + break; } }; + std::thread t1{[&] { partialDeleteStartedSyncPoint.arrive_and_wait(); weak.reset(); // Trigger a full delete as soon as the partial // delete starts }}; + std::thread t2{[&] { strong.reset(); // Trigger a partial delete }}; + t1.join(); t2.join(); @@ -444,13 +458,24 @@ TEST(IntrusiveSharedTest, destructor) std::latch weakResetSyncPoint{2}; strong->tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan.exchange(true)); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan.exchange(true) || destructorRan.load()); + break; + + case Deleted: + EXPECT_FALSE(destructorRan.exchange(true)); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; std::thread t1{[&] { @@ -492,25 +517,36 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) auto tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan || destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan); - setDestructorRan(); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + break; + + case Deleted: + EXPECT_FALSE(destructorRan); + setDestructorRan(); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) -> std::vector, WeakIntrusive>> { std::vector, WeakIntrusive>> result; - std::uniform_int_distribution<> toCreateDist(4, 64); + std::uniform_int_distribution toCreateDist(4, 64); std::uniform_int_distribution<> isStrongDist(0, 1); auto numToCreate = toCreateDist(eng); result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) + for (auto i = 0uz; i < numToCreate; ++i) { if (isStrongDist(eng)) { @@ -523,8 +559,8 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) } return result; }; - constexpr int kLoopIters = 2 * 1024; - constexpr int kNumThreads = 16; + constexpr auto kLoopIters = 2uz * 1024; + constexpr auto kNumThreads = 16uz; std::vector> toClone; Barrier loopStartSyncPoint{kNumThreads}; Barrier postCreateToCloneSyncPoint{kNumThreads}; @@ -533,7 +569,7 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) std::random_device rd; std::vector result; result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) result.emplace_back(rd()); return result; }(); @@ -541,8 +577,8 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) // cloneAndDestroy clones the strong pointer into a vector of mixed // strong and weak pointers and destroys them all at once. // threadId==0 is special. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) + auto cloneAndDestroy = [&](std::size_t threadId) { + for (auto i = 0uz; i < kLoopIters; ++i) { // ------ Sync Point ------ loopStartSyncPoint.arriveAndWait(); @@ -582,11 +618,11 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_variant) }; std::vector threads; threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads.emplace_back(cloneAndDestroy, i); } - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads[i].join(); } @@ -623,31 +659,42 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) auto tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan || destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan); - setDestructorRan(); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + break; + + case Deleted: + EXPECT_FALSE(destructorRan); + setDestructorRan(); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; auto createVecOfPointers = [&](auto const& toClone, std::default_random_engine& eng) -> std::vector> { std::vector> result; - std::uniform_int_distribution<> toCreateDist(4, 64); + std::uniform_int_distribution toCreateDist(4, 64); auto numToCreate = toCreateDist(eng); result.reserve(numToCreate); - for (int i = 0; i < numToCreate; ++i) + for (auto i = 0uz; i < numToCreate; ++i) result.emplace_back(SharedIntrusive(toClone)); return result; }; - constexpr int kLoopIters = 2 * 1024; - constexpr int kFlipPointersLoopIters = 256; - constexpr int kNumThreads = 16; + constexpr auto kLoopIters = 2uz * 1024; + constexpr auto kFlipPointersLoopIters = 256uz; + constexpr auto kNumThreads = 16uz; std::vector> toClone; Barrier loopStartSyncPoint{kNumThreads}; Barrier postCreateToCloneSyncPoint{kNumThreads}; @@ -657,7 +704,7 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) std::random_device rd; std::vector result; result.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) result.emplace_back(rd()); return result; }(); @@ -666,8 +713,8 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) // mixed strong and weak pointers, runs a loop that randomly // changes strong pointers to weak pointers, and destroys them // all at once. - auto cloneAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) + auto cloneAndDestroy = [&](std::size_t threadId) { + for (auto i = 0uz; i < kLoopIters; ++i) { // ------ Sync Point ------ loopStartSyncPoint.arriveAndWait(); @@ -702,7 +749,7 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) postCreateVecOfPointersSyncPoint.arriveAndWait(); std::uniform_int_distribution<> isStrongDist(0, 1); - for (int f = 0; f < kFlipPointersLoopIters; ++f) + for (auto f = 0uz; f < kFlipPointersLoopIters; ++f) { for (auto& p : v) { @@ -725,11 +772,11 @@ TEST(IntrusiveSharedTest, multithreaded_clear_mixed_union) }; std::vector threads; threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads.emplace_back(cloneAndDestroy, i); } - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads[i].join(); } @@ -761,21 +808,32 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) auto tracingCallback = [&](TrackedState cur, std::optional next) { using enum TrackedState; auto [destructorRan, partialDeleteRan] = getDestructorState(); - if (next == PartiallyDeleted) + if (!next) + return; + + switch (*next) { - EXPECT_FALSE(partialDeleteRan || destructorRan); - setPartialDeleteRan(); - } - if (next == Deleted) - { - EXPECT_FALSE(destructorRan); - setDestructorRan(); + case PartiallyDeleted: + EXPECT_FALSE(partialDeleteRan || destructorRan); + setPartialDeleteRan(); + break; + + case Deleted: + EXPECT_FALSE(destructorRan); + setDestructorRan(); + break; + + case Uninitialized: + case Alive: + case PartiallyDeletedStarted: + case DeletedStarted: + break; } }; - constexpr int kLoopIters = 2 * 1024; - constexpr int kLockWeakLoopIters = 256; - constexpr int kNumThreads = 16; + constexpr auto kLoopIters = 2uz * 1024; + constexpr auto kLockWeakLoopIters = 256uz; + constexpr auto kNumThreads = 16uz; std::vector> toLock; Barrier loopStartSyncPoint{kNumThreads}; Barrier postCreateToLockSyncPoint{kNumThreads}; @@ -784,8 +842,8 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) // lockAndDestroy creates weak pointers from the strong pointer // and runs a loop that locks the weak pointer. At the end of the loop // all the pointers are destroyed all at once. - auto lockAndDestroy = [&](int threadId) { - for (int i = 0; i < kLoopIters; ++i) + auto lockAndDestroy = [&](std::size_t threadId) { + for (auto i = 0uz; i < kLoopIters; ++i) { // ------ Sync Point ------ loopStartSyncPoint.arriveAndWait(); @@ -816,7 +874,7 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) // Multiple threads all create a weak pointer from the same // strong pointer WeakIntrusive const weak{toLock[threadId]}; - for (int wi = 0; wi < kLockWeakLoopIters; ++wi) + for (auto wi = 0uz; wi < kLockWeakLoopIters; ++wi) { EXPECT_FALSE(weak.expired()); auto strong = weak.lock(); @@ -831,11 +889,11 @@ TEST(IntrusiveSharedTest, multithreaded_locking_weak) }; std::vector threads; threads.reserve(kNumThreads); - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads.emplace_back(lockAndDestroy, i); } - for (int i = 0; i < kNumThreads; ++i) + for (auto i = 0uz; i < kNumThreads; ++i) { threads[i].join(); } diff --git a/src/tests/libxrpl/basics/MallocTrim.cpp b/src/tests/libxrpl/basics/MallocTrim.cpp index 6ac8957f0e..52151262b0 100644 --- a/src/tests/libxrpl/basics/MallocTrim.cpp +++ b/src/tests/libxrpl/basics/MallocTrim.cpp @@ -199,7 +199,7 @@ TEST(mallocTrim, repeated_calls) beast::Journal const journal{beast::Journal::getNullSink()}; // Call malloc_trim multiple times to ensure it's safe - for (int i = 0; i < 5; ++i) + for (auto i = 0uz; i < 5; ++i) { MallocTrimReport const report = mallocTrim("iteration_" + std::to_string(i), journal); diff --git a/src/tests/libxrpl/basics/Number.cpp b/src/tests/libxrpl/basics/Number.cpp index 36e1b4a700..32f93eb1f7 100644 --- a/src/tests/libxrpl/basics/Number.cpp +++ b/src/tests/libxrpl/basics/Number.cpp @@ -323,11 +323,11 @@ TEST(NumberTest, add) __LINE__, }, { - // Does not round. Mantissas are going to be > maxRep, so if + // Does not round. Mantissas are going to be > kMaxRep, so if // added together as uint64_t's, the result will overflow. // With addition using uint128_t, there's no problem. After // normalizing, the resulting mantissa ends up less than - // maxRep. + // kMaxRep. Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}}, Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}}, @@ -1078,14 +1078,6 @@ TEST(NumberTest, root) EXPECT_EQ(result, z) << ss.str(); } }; - /* - auto tests = [&](auto const& cSmall, auto const& cLarge) { - test(cSmall); - if (scale != MantissaRange::mantissa_scale::small) - test(cLarge); - }; - */ - auto const cSmall = std::to_array( {{Number{2}, 2, Number{1414213562373095049, -18}}, {Number{2'000'000}, 2, Number{1414213562373095049, -15}}, @@ -1511,7 +1503,7 @@ TEST(NumberTest, to_string) NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); - EXPECT_EQ(maxMantissa, (9'999'999'999'999'999)); + EXPECT_EQ(maxMantissa, 9'999'999'999'999'999); test( Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, "9999999999999999", @@ -1550,7 +1542,7 @@ TEST(NumberTest, to_string) NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); - EXPECT_EQ((maxMantissa), (9'999'999'999'999'999'999ULL)); + EXPECT_EQ(maxMantissa, 9'999'999'999'999'999'999ULL); test( Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990", diff --git a/src/tests/libxrpl/basics/base58.cpp b/src/tests/libxrpl/basics/base58.cpp index d452453f76..d6b1d2c3f9 100644 --- a/src/tests/libxrpl/basics/base58.cpp +++ b/src/tests/libxrpl/basics/base58.cpp @@ -151,7 +151,7 @@ randomBigInt(std::uint8_t minSize = 1, std::uint8_t maxSize = 5) auto const numCoeff = numCoeffDist(eng); std::vector coeffs; coeffs.reserve(numCoeff); - for (int i = 0; i < numCoeff; ++i) + for (auto i = 0uz; i < numCoeff; ++i) { coeffs.push_back(dist(eng)); } @@ -167,7 +167,7 @@ TEST(Base58Test, multiprecision) auto eng = randEngine(); std::uniform_int_distribution dist; std::uniform_int_distribution dist1(1); - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist(eng); if (d == 0u) @@ -185,7 +185,7 @@ TEST(Base58Test, multiprecision) EXPECT_EQ(refMod.convert_to(), mod); EXPECT_EQ(foundDiv, refDiv); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist(eng); auto bigInt = multiprecision_utils::randomBigInt(/*minSize*/ 2); @@ -204,7 +204,7 @@ TEST(Base58Test, multiprecision) auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); EXPECT_EQ(refAdd, foundAdd); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist1(eng); // Force overflow @@ -221,7 +221,7 @@ TEST(Base58Test, multiprecision) auto const foundAdd = multiprecision_utils::toBoostMP(bigInt); EXPECT_NE(refAdd, foundAdd); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist(eng); auto bigInt = multiprecision_utils::randomBigInt(/* minSize */ 2); @@ -239,7 +239,7 @@ TEST(Base58Test, multiprecision) auto const foundMul = multiprecision_utils::toBoostMP(bigInt); EXPECT_EQ(refMul, foundMul); } - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::uint64_t const d = dist1(eng); // Force overflow @@ -265,7 +265,7 @@ TEST(Base58Test, fast_matches_ref) std::array b256ResultBuf[2]; std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b58ResultBuf[i]}; if (i == 0) @@ -297,7 +297,7 @@ TEST(Base58Test, fast_matches_ref) } } - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; if (i == 0) @@ -339,7 +339,7 @@ TEST(Base58Test, fast_matches_ref) std::array b256ResultBuf[2]; std::array, 2> b256Result; - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b58ResultBuf[i].data(), b58ResultBuf[i].size()}; if (i == 0) @@ -370,7 +370,7 @@ TEST(Base58Test, fast_matches_ref) } } - for (int i = 0; i < 2; ++i) + for (auto i = 0uz; i < 2; ++i) { std::span const outBuf{b256ResultBuf[i].data(), b256ResultBuf[i].size()}; if (i == 0) @@ -425,7 +425,7 @@ TEST(Base58Test, fast_matches_ref) // test with random data constexpr std::size_t kIters = 100000; - for (int i = 0; i < kIters; ++i) + for (auto i = 0uz; i < kIters; ++i) { std::array b256DataBuf{}; auto const [tokType, b256Data] = randomB256TestData(b256DataBuf); diff --git a/src/tests/libxrpl/basics/base_uint.cpp b/src/tests/libxrpl/basics/base_uint.cpp index 174cc33aa0..10795f4563 100644 --- a/src/tests/libxrpl/basics/base_uint.cpp +++ b/src/tests/libxrpl/basics/base_uint.cpp @@ -59,65 +59,67 @@ struct BaseUintTest : public ::testing::Test static void testComparisons() { - { - static constexpr std::array, 6> kTestArgs{ - {{"0000000000000000", "0000000000000001"}, - {"0000000000000000", "ffffffffffffffff"}, - {"1234567812345678", "2345678923456789"}, - {"8000000000000000", "8000000000000001"}, - {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, - {"fffffffffffffffe", "ffffffffffffffff"}}}; + using HexPair = std::pair; - for (auto const& arg : kTestArgs) + { + static constexpr auto kTestArgs = std::to_array({ + {"0000000000000000", "0000000000000001"}, + {"0000000000000000", "ffffffffffffffff"}, + {"1234567812345678", "2345678923456789"}, + {"8000000000000000", "8000000000000001"}, + {"aaaaaaaaaaaaaaa9", "aaaaaaaaaaaaaaaa"}, + {"fffffffffffffffe", "ffffffffffffffff"}, + }); + + for (auto const& [smallerText, largerText] : kTestArgs) { - xrpl::BaseUInt<64> const u{arg.first}, v{arg.second}; + xrpl::BaseUInt<64> const smaller{smallerText}, larger{largerText}; // For code readability, we want to use general boolean // expectations instead of specific EXPECT_LT etc. - EXPECT_TRUE(u < v); - EXPECT_TRUE(u <= v); - EXPECT_TRUE(u != v); - EXPECT_FALSE(u == v); - EXPECT_FALSE(u > v); - EXPECT_FALSE(u >= v); - EXPECT_FALSE(v < u); - EXPECT_FALSE(v <= u); - EXPECT_TRUE(v != u); - EXPECT_FALSE(v == u); - EXPECT_TRUE(v > u); - EXPECT_TRUE(v >= u); - EXPECT_TRUE(u == u); - EXPECT_TRUE(v == v); + EXPECT_TRUE(smaller < larger); + EXPECT_TRUE(smaller <= larger); + EXPECT_TRUE(smaller != larger); + EXPECT_FALSE(smaller == larger); + EXPECT_FALSE(smaller > larger); + EXPECT_FALSE(smaller >= larger); + EXPECT_FALSE(larger < smaller); + EXPECT_FALSE(larger <= smaller); + EXPECT_TRUE(larger != smaller); + EXPECT_FALSE(larger == smaller); + EXPECT_TRUE(larger > smaller); + EXPECT_TRUE(larger >= smaller); + EXPECT_TRUE(smaller == smaller); + EXPECT_TRUE(larger == larger); } } { - static constexpr std::array, 6> kTestArgs{ - { - {"000000000000000000000000", "000000000000000000000001"}, - {"000000000000000000000000", "ffffffffffffffffffffffff"}, - {"0123456789ab0123456789ab", "123456789abc123456789abc"}, - {"555555555555555555555555", "55555555555a555555555555"}, - {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, - {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, - }}; + static constexpr auto kTestArgs = std::to_array({ + {"000000000000000000000000", "000000000000000000000001"}, + {"000000000000000000000000", "ffffffffffffffffffffffff"}, + {"0123456789ab0123456789ab", "123456789abc123456789abc"}, + {"555555555555555555555555", "55555555555a555555555555"}, + {"aaaaaaaaaaaaaaa9aaaaaaaa", "aaaaaaaaaaaaaaaaaaaaaaaa"}, + {"fffffffffffffffffffffffe", "ffffffffffffffffffffffff"}, + }); - for (auto const& arg : kTestArgs) + for (auto const& [smallerText, largerText] : kTestArgs) { - xrpl::BaseUInt<96> const u{arg.first}, v{arg.second}; - EXPECT_TRUE(u < v); - EXPECT_TRUE(u <= v); - EXPECT_TRUE(u != v); - EXPECT_FALSE(u == v); - EXPECT_FALSE(u > v); - EXPECT_FALSE(u >= v); - EXPECT_FALSE(v < u); - EXPECT_FALSE(v <= u); - EXPECT_TRUE(v != u); - EXPECT_FALSE(v == u); - EXPECT_TRUE(v > u); - EXPECT_TRUE(v >= u); - EXPECT_TRUE(u == u); - EXPECT_TRUE(v == v); + xrpl::BaseUInt<96> const smaller{smallerText}, larger{largerText}; + EXPECT_TRUE(smaller < larger); + EXPECT_TRUE(smaller <= larger); + EXPECT_TRUE(smaller != larger); + EXPECT_FALSE(smaller == larger); + EXPECT_FALSE(smaller > larger); + EXPECT_FALSE(smaller >= larger); + EXPECT_FALSE(larger < smaller); + EXPECT_FALSE(larger <= smaller); + EXPECT_TRUE(larger != smaller); + EXPECT_FALSE(larger == smaller); + EXPECT_TRUE(larger > smaller); + EXPECT_TRUE(larger >= smaller); + EXPECT_TRUE(smaller == smaller); + EXPECT_TRUE(larger == larger); } } } @@ -401,14 +403,14 @@ TEST_F(BaseUintTest, base_uint) { } }; - constexpr StrBaseUInt kTestCases[] = { + constexpr auto kTestCases = std::to_array({ "000000000000000000000000", "000000000000000000000001", "fedcba9876543210ABCDEF91", "19FEDCBA0123456789abcdef", "800000000000000000000000", "fFfFfFfFfFfFfFfFfFfFfFfF", - }; + }); for (StrBaseUInt const& t : kTestCases) { diff --git a/src/tests/libxrpl/basics/join.cpp b/src/tests/libxrpl/basics/join.cpp index 66c832678b..427f0b42bc 100644 --- a/src/tests/libxrpl/basics/join.cpp +++ b/src/tests/libxrpl/basics/join.cpp @@ -19,11 +19,11 @@ struct JoinTest : public ::testing::Test TEST_F(JoinTest, join) { - auto test = [](auto collectionanddelimiter, std::string expected) { + auto test = [](auto collectionAndDelimiter, std::string expected) { std::stringstream ss; // Put something else in the buffer before and after to ensure that // the << operator returns the stream correctly. - ss << "(" << collectionanddelimiter << ")"; + ss << "(" << collectionAndDelimiter << ")"; auto const str = ss.str(); EXPECT_EQ(str.substr(1, str.length() - 2), expected); EXPECT_EQ(str.front(), '('); diff --git a/src/tests/libxrpl/consensus/CensorshipDetector.cpp b/src/tests/libxrpl/consensus/CensorshipDetector.cpp index aa6b2d086b..2c6b6ec731 100644 --- a/src/tests/libxrpl/consensus/CensorshipDetector.cpp +++ b/src/tests/libxrpl/consensus/CensorshipDetector.cpp @@ -69,7 +69,7 @@ TEST(CensorshipDetectorTest, censorship_detector) runRound(cdet, ++round, {23, 24, 25, 26}, {25, 27}, {23, 26}, {24}); runRound(cdet, ++round, {23, 26, 28}, {26, 28}, {23}, {}); - for (int i = 0; i != 10; ++i) + for (auto i = 0uz; i != 10; ++i) runRound(cdet, ++round, {23}, {}, {23}, {}); runRound(cdet, ++round, {23, 29}, {29}, {23}, {}); diff --git a/src/tests/libxrpl/csf/TrustGraph.h b/src/tests/libxrpl/csf/TrustGraph.h index d010b954e0..8a804fcd5b 100644 --- a/src/tests/libxrpl/csf/TrustGraph.h +++ b/src/tests/libxrpl/csf/TrustGraph.h @@ -118,9 +118,9 @@ public: std::vector res; // Loop over all pairs of uniqueUNLs - for (int i = 0; i < uniqueUNLs.size(); ++i) + for (auto i = 0uz; i < uniqueUNLs.size(); ++i) { - for (int j = (i + 1); j < uniqueUNLs.size(); ++j) + for (auto j = i + 1; j < uniqueUNLs.size(); ++j) { auto const& unlA = uniqueUNLs[i]; auto const& unlB = uniqueUNLs[j]; diff --git a/src/tests/libxrpl/csf/random.h b/src/tests/libxrpl/csf/random.h index 007bdecb1b..56838bb280 100644 --- a/src/tests/libxrpl/csf/random.h +++ b/src/tests/libxrpl/csf/random.h @@ -24,11 +24,12 @@ randomWeightedShuffle(std::vector v, std::vector w, G& g) { using std::swap; - for (int i = 0; i < v.size() - 1; ++i) + for (auto i = 0uz; i + 1 < v.size(); ++i) { - // pick a random item weighted by w - std::discrete_distribution<> dd(w.begin() + i, w.end()); // NOLINT(misc-const-correctness) - auto idx = dd(g); + // Pick a random item from the unplaced tail, weighted by w. + // NOLINTNEXTLINE(misc-const-correctness) + std::discrete_distribution dd(w.begin() + i, w.end()); + auto const idx = i + dd(g); std::swap(v[i], v[idx]); std::swap(w[i], w[idx]); } diff --git a/src/tests/libxrpl/nodestore/Database.cpp b/src/tests/libxrpl/nodestore/Database.cpp index 23087a2f84..82012ed347 100644 --- a/src/tests/libxrpl/nodestore/Database.cpp +++ b/src/tests/libxrpl/nodestore/Database.cpp @@ -27,11 +27,11 @@ namespace { std::vector allBackends() { - std::vector types{"memory", "nudb"}; #if XRPL_ROCKSDB_AVAILABLE - types.emplace_back("rocksdb"); + return {"memory", "nudb", "rocksdb"}; +#else + return {"memory", "nudb"}; #endif - return types; } std::vector diff --git a/src/tests/libxrpl/resource/Logic.cpp b/src/tests/libxrpl/resource/Logic.cpp index 1f935ebf4b..a3362b4540 100644 --- a/src/tests/libxrpl/resource/Logic.cpp +++ b/src/tests/libxrpl/resource/Logic.cpp @@ -17,9 +17,12 @@ #include #include +#include #include #include +#include #include +#include namespace xrpl::Resource { @@ -54,9 +57,10 @@ protected: //-------------------------------------------------------------------------- - static void - populateGossip(Gossip& gossip) + static Gossip + makeGossip() { + Gossip gossip; std::uint8_t const v(10 + randInt(9)); std::uint8_t const n(10 + randInt(9)); gossip.items.reserve(n); @@ -71,8 +75,9 @@ protected: static_cast(v + i), }}; item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}}; - gossip.items.push_back(item); + gossip.items.push_back(std::move(item)); } + return gossip; } }; @@ -87,8 +92,8 @@ TEST_F(ResourceManagerTest, limited_warn_drop) Consumer c{logic.newInboundEndpoint(addr)}; // Create load until we get a warning - int n = 10000; - bool warned = false; + auto n = 10000; + auto warned = false; while (--n >= 0) { @@ -97,7 +102,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) warned = true; break; } - ++logic.clock(); + logic.advance(); } ASSERT_TRUE(warned) << "Loop count exceeded without warning"; @@ -113,7 +118,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) EXPECT_TRUE(c.disconnect(j_)); break; } - ++logic.clock(); + logic.advance(); } ASSERT_TRUE(dropped) << "Loop count exceeded without dropping"; @@ -135,7 +140,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) auto n = kSecondsUntilExpiration + 1s; while (--n > 0s) { - ++logic.clock(); + logic.advance(); logic.periodicActivity(); Consumer const c{logic.newInboundEndpoint(addr)}; if (c.disposition() != Disposition::Drop) @@ -167,7 +172,7 @@ TEST_F(ResourceManagerTest, unlimited_warn_drop) warned = true; break; } - ++logic.clock(); + logic.advance(); } EXPECT_FALSE(warned) << "Should loop forever with no warning"; @@ -175,6 +180,8 @@ TEST_F(ResourceManagerTest, unlimited_warn_drop) TEST_F(ResourceManagerTest, charges) { + static constexpr auto kDecayTicks = 128uz; + TestLogic logic{j_}; { @@ -183,7 +190,7 @@ TEST_F(ResourceManagerTest, charges) Charge const fee{1000}; JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second"; c.charge(fee); - for (int i = 0; i < 128; ++i) + for (auto tick = 0uz; tick < kDecayTicks; ++tick) { JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count() << ", Balance = " << c.balance(); @@ -196,7 +203,7 @@ TEST_F(ResourceManagerTest, charges) Consumer c{logic.newInboundEndpoint(address)}; Charge const fee{1000}; JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second"; - for (int i = 0; i < 128; ++i) + for (auto tick = 0uz; tick < kDecayTicks; ++tick) { c.charge(fee); JLOG(j_.info()) << "Time= " << logic.clock().now().time_since_epoch().count() @@ -210,13 +217,10 @@ TEST_F(ResourceManagerTest, imports) { TestLogic logic{j_}; - Gossip g[5]; - - for (auto& i : g) - populateGossip(i); - - for (int i = 0; i < 5; ++i) - logic.importConsumers(std::to_string(i), g[i]); + static constexpr auto kGossipSources = 5uz; + std::ranges::for_each(std::views::iota(0uz, kGossipSources), [&](auto const i) { + logic.importConsumers(std::to_string(i), makeGossip()); + }); } TEST_F(ResourceManagerTest, import) @@ -233,7 +237,7 @@ TEST_F(ResourceManagerTest, import) 1, }}; item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}}; - g.items.push_back(item); + g.items.push_back(std::move(item)); logic.importConsumers("g", g); } diff --git a/src/tests/libxrpl/shamap/SHAMap.cpp b/src/tests/libxrpl/shamap/SHAMap.cpp index 238c34bf9c..e662e16be4 100644 --- a/src/tests/libxrpl/shamap/SHAMap.cpp +++ b/src/tests/libxrpl/shamap/SHAMap.cpp @@ -257,12 +257,9 @@ TEST_P(SHAMapTest, add_traverse_snapshot_build_tear_and_iterate) map.invariants(); } - int h = 7; + auto keyIndex = kKeys.size(); for (auto const& k : map) - { - EXPECT_EQ(k.key(), kKeys[h]); - --h; - } + EXPECT_EQ(k.key(), kKeys[--keyIndex]); } } @@ -288,7 +285,11 @@ TEST_F(SHAMapPathProof, verify_proof_path) uint256 rootHash; std::vector goodPath; - for (unsigned char c = 1; c < 100; ++c) + static constexpr unsigned char kFirstKey = 1; + static constexpr unsigned char kKeyCount = 100; + static constexpr unsigned char kLastKey = kKeyCount - 1; + + for (unsigned char c = kFirstKey; c < kKeyCount; ++c) { uint256 k(c); map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()})); @@ -304,7 +305,7 @@ TEST_F(SHAMapPathProof, verify_proof_path) auto& proofPath = *path; EXPECT_TRUE(map.verifyProofPath(root, k, proofPath)); - if (c == 1) + if (c == kFirstKey) { // extra node proofPath.insert(proofPath.begin(), proofPath.front()); @@ -313,7 +314,7 @@ TEST_F(SHAMapPathProof, verify_proof_path) uint256 const wrongKey(c + 1); EXPECT_FALSE(map.getProofPath(wrongKey)); } - if (c == 99) + if (c == kLastKey) { key = k; rootHash = root; diff --git a/src/tests/libxrpl/shamap/SHAMapSync.cpp b/src/tests/libxrpl/shamap/SHAMapSync.cpp index 400509d217..e4bcbd8970 100644 --- a/src/tests/libxrpl/shamap/SHAMapSync.cpp +++ b/src/tests/libxrpl/shamap/SHAMapSync.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -33,15 +34,17 @@ protected: boost::intrusive_ptr makeRandomAS() { + static constexpr auto kWordsPerState = 3uz; + Serializer s; - for (int d = 0; d < 3; ++d) + for (auto word = 0uz; word < kWordsPerState; ++word) s.add32(randInt(eng_)); return makeShamapitem(s.getSHA512Half(), s.slice()); } bool - confuseMap(SHAMap& map, int count) + confuseMap(SHAMap& map, std::size_t count) { // add a bunch of random states to a map, then remove them // map should be the same @@ -49,7 +52,7 @@ protected: std::list items; - for (int i = 0; i < count; ++i) + for (auto i = 0uz; i < count; ++i) { auto item = makeRandomAS(); items.push_back(item->key()); @@ -86,26 +89,30 @@ TEST_F(SHAMapSyncTest, sync) SHAMap source{SHAMapType::FREE, f}; SHAMap destination{SHAMapType::FREE, f2}; - int const items = 10000; - for (int i = 0; i < items; ++i) + static constexpr auto kItemCount = 10000uz; + static constexpr auto kInvariantInterval = 100uz; + static constexpr auto kNodesToConfuse = 500uz; + static constexpr auto kMaxNodesPerRequest = 2048; + + for (auto i = 0uz; i < kItemCount; ++i) { source.addItem(SHAMapNodeType::TnAccountState, makeRandomAS()); - if (i % 100 == 0) + if (i % kInvariantInterval == 0) source.invariants(); } source.invariants(); - ASSERT_TRUE(confuseMap(source, 500)); + ASSERT_TRUE(confuseMap(source, kNodesToConfuse)); source.invariants(); source.setImmutable(); - int count = 0; + std::size_t count = 0; source.visitLeaves([&count]([[maybe_unused]] auto const& item) { ++count; }); - EXPECT_EQ(count, items); + EXPECT_EQ(count, kItemCount); std::vector missingNodes; - source.walkMap(missingNodes, 2048); + source.walkMap(missingNodes, kMaxNodesPerRequest); EXPECT_TRUE(missingNodes.empty()); destination.setSynching(); @@ -128,7 +135,7 @@ TEST_F(SHAMapSyncTest, sync) f.clock().advance(std::chrono::seconds(1)); // get the list of nodes we know we need - auto nodesMissing = destination.getMissingNodes(2048, nullptr); + auto nodesMissing = destination.getMissingNodes(kMaxNodesPerRequest, nullptr); if (nodesMissing.empty()) break; 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 59/86] 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 60/86] 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 61/86] 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 62/86] 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 63/86] 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 64/86] 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 65/86] 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 97f35add2e53a04a30fb5b344d89da50b65073af Mon Sep 17 00:00:00 2001 From: Braedon Klock Date: Mon, 3 Aug 2026 17:08:42 -0400 Subject: [PATCH 66/86] fix: Add null check for account object reads (#7717) Co-authored-by: Mayukha Vadari --- src/xrpld/rpc/handlers/account/AccountObjects.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index 4a34ff02fc..ee2595bf94 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -191,6 +192,13 @@ getAccountObjects( for (; entryIter != dirEntries.end(); ++entryIter) { auto const sleNode = ledger.read(keylet::child(*entryIter)); + if (!sleNode) + { + // LCOV_EXCL_START + UNREACHABLE("xrpl::doAccountObjects : null SLE"); + continue; + // LCOV_EXCL_STOP + } bool canAppend = true; From 765babb20dacfadf70c534b4650c539b65f8f3b7 Mon Sep 17 00:00:00 2001 From: Timur Yalymov <36795566+tyalymov@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:11:33 +0100 Subject: [PATCH 67/86] fix: Add VaultInvariant check that lossUnrealized is non-negative (#7863) Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com> --- src/libxrpl/tx/invariants/VaultInvariant.cpp | 6 ++++ src/test/app/Invariants_test.cpp | 35 ++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp index a9ba0ec874..eca50eb809 100644 --- a/src/libxrpl/tx/invariants/VaultInvariant.cpp +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp @@ -483,6 +483,12 @@ ValidVault::finalize( result = false; } + if (view.rules().enabled(fixCleanup3_4_0) && afterVault.lossUnrealized < kZero) + { + JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative"; + result = false; + } + if (afterVault.assetsTotal < kZero) { JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive"; diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index eaf1f2704c..ac6d8e068f 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -3374,6 +3374,41 @@ class Invariants_test : public beast::unit_test::Suite precloseXrp, TxAccount::A2); + // A negative loss unrealized must trip the invariant. ttLOAN_MANAGE is + // allowed to change loss unrealized, so it isolates this check from the + // "must not change loss unrealized" invariant. Gated behind + // fixCleanup3_4_0 (see below). + 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()); + return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) { + sample.lossUnrealized = -1; + })); + }, + XRPAmount{}, + STTx{ttLOAN_MANAGE, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}, + precloseXrp, + TxAccount::A2); + + // Without fixCleanup3_4_0 the same state must NOT trip the invariant, + // preserving pre-amendment behavior (no fork risk). + doInvariantCheck( + makeEnv(defaultAmendments() - fixCleanup3_4_0), + {}, + [&](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) { + sample.lossUnrealized = -1; + })); + }, + XRPAmount{}, + STTx{ttLOAN_MANAGE, [](STObject& tx) {}}, + {tesSUCCESS, tesSUCCESS}, + precloseXrp, + TxAccount::A2); + doInvariantCheck( {"set assets outstanding must not exceed assets maximum"}, [&](Account const& a1, Account const& a2, ApplyContext& ac) { From b8451ffa325dcd44db68c630e649c6a2ddeebe96 Mon Sep 17 00:00:00 2001 From: Luc des Trois Maisons Date: Mon, 3 Aug 2026 17:17:23 -0400 Subject: [PATCH 68/86] fix: Add missing value_type to JSON iterators (#7907) --- include/xrpl/json/json_value.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 47ad3ac1e0..260917face 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -623,6 +623,7 @@ class ValueConstIterator : public ValueIteratorBase public: using size_t = unsigned int; using difference_type = int; + using value_type = Value const; using reference = Value const&; using pointer = Value const*; using SelfType = ValueConstIterator; @@ -687,6 +688,7 @@ class ValueIterator : public ValueIteratorBase public: using size_t = unsigned int; using difference_type = int; + using value_type = Value; using reference = Value&; using pointer = Value*; using SelfType = ValueIterator; From 06488c1318d96f56d0536251bee08ac85fa7fdd3 Mon Sep 17 00:00:00 2001 From: Alex Kremer Date: Tue, 4 Aug 2026 14:46:55 +0100 Subject: [PATCH 69/86] chore: Rename CamelCase namespaces to snake_case (#7933) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .clang-tidy | 2 + include/xrpl/basics/Resolver.h | 2 +- include/xrpl/beast/insight/StatsDCollector.h | 2 +- include/xrpl/beast/net/IPAddress.h | 10 +- include/xrpl/beast/net/IPAddressConversion.h | 20 +- include/xrpl/beast/net/IPAddressV4.h | 4 +- include/xrpl/beast/net/IPAddressV6.h | 4 +- include/xrpl/beast/net/IPEndpoint.h | 12 +- include/xrpl/core/ServiceRegistry.h | 6 +- include/xrpl/ledger/helpers/LendingHelpers.h | 12 +- include/xrpl/net/AutoSocket.h | 8 +- include/xrpl/peerfinder/Config.h | 8 +- include/xrpl/peerfinder/PeerfinderManager.h | 20 +- include/xrpl/peerfinder/Slot.h | 8 +- include/xrpl/peerfinder/Types.h | 10 +- include/xrpl/peerfinder/detail/Bootcache.h | 20 +- include/xrpl/peerfinder/detail/Checker.h | 8 +- include/xrpl/peerfinder/detail/Counts.h | 16 +- include/xrpl/peerfinder/detail/Fixed.h | 8 +- include/xrpl/peerfinder/detail/Handouts.h | 28 +-- include/xrpl/peerfinder/detail/Livecache.h | 28 +-- include/xrpl/peerfinder/detail/Logic.h | 90 +++---- include/xrpl/peerfinder/detail/SlotImp.h | 28 +-- include/xrpl/peerfinder/detail/Source.h | 4 +- .../xrpl/peerfinder/detail/SourceStrings.h | 4 +- include/xrpl/peerfinder/detail/Store.h | 8 +- include/xrpl/peerfinder/detail/Tuning.h | 4 +- include/xrpl/peerfinder/make_Manager.h | 4 +- include/xrpl/protocol/ApiVersion.h | 28 +-- include/xrpl/protocol/BuildInfo.h | 4 +- include/xrpl/protocol/ErrorCodes.h | 4 +- include/xrpl/protocol/MultiApiJson.h | 2 +- .../xrpl/protocol/NFTSyntheticSerializer.h | 4 +- include/xrpl/protocol/Protocol.h | 4 +- include/xrpl/protocol/PublicKey.h | 2 +- include/xrpl/protocol/XChainAttestations.h | 8 +- include/xrpl/resource/Charge.h | 6 +- include/xrpl/resource/Consumer.h | 4 +- include/xrpl/resource/Disposition.h | 4 +- include/xrpl/resource/Fees.h | 4 +- include/xrpl/resource/Gossip.h | 6 +- include/xrpl/resource/README.md | 6 +- include/xrpl/resource/ResourceManager.h | 12 +- include/xrpl/resource/detail/Entry.h | 4 +- include/xrpl/resource/detail/Import.h | 4 +- include/xrpl/resource/detail/Key.h | 8 +- include/xrpl/resource/detail/Kind.h | 4 +- include/xrpl/resource/detail/Logic.h | 16 +- include/xrpl/resource/detail/Tuning.h | 4 +- include/xrpl/server/InfoSub.h | 2 +- include/xrpl/server/Session.h | 2 +- include/xrpl/server/detail/BaseHTTPPeer.h | 2 +- include/xrpl/server/detail/BaseWSPeer.h | 2 +- src/libxrpl/basics/ResolverAsio.cpp | 4 +- src/libxrpl/basics/StringUtilities.cpp | 2 +- src/libxrpl/beast/insight/StatsDCollector.cpp | 8 +- src/libxrpl/beast/net/IPAddressConversion.cpp | 4 +- src/libxrpl/beast/net/IPAddressV4.cpp | 4 +- src/libxrpl/beast/net/IPAddressV6.cpp | 4 +- src/libxrpl/beast/net/IPEndpoint.cpp | 4 +- src/libxrpl/ledger/helpers/LendingHelpers.cpp | 24 +- src/libxrpl/peerfinder/Bootcache.cpp | 26 +- src/libxrpl/peerfinder/Config.cpp | 14 +- src/libxrpl/peerfinder/Endpoint.cpp | 8 +- src/libxrpl/peerfinder/PeerfinderManager.cpp | 16 +- src/libxrpl/peerfinder/SlotImp.cpp | 28 +-- src/libxrpl/peerfinder/SourceStrings.cpp | 8 +- src/libxrpl/protocol/BuildInfo.cpp | 4 +- src/libxrpl/protocol/ErrorCodes.cpp | 6 +- .../protocol/NFTSyntheticSerializer.cpp | 4 +- src/libxrpl/protocol/RPCErr.cpp | 2 +- src/libxrpl/protocol/STParsedJSON.cpp | 34 +-- src/libxrpl/protocol/XChainAttestations.cpp | 4 +- src/libxrpl/resource/Charge.cpp | 4 +- src/libxrpl/resource/Consumer.cpp | 12 +- src/libxrpl/resource/Fees.cpp | 6 +- src/libxrpl/resource/ResourceManager.cpp | 14 +- src/libxrpl/server/InfoSub.cpp | 2 +- src/libxrpl/server/JSONRPCUtil.cpp | 4 +- src/libxrpl/server/Port.cpp | 2 +- .../tx/transactors/bridge/XChainBridge.cpp | 28 +-- .../tx/transactors/lending/LoanBrokerSet.cpp | 2 +- .../tx/transactors/lending/LoanPay.cpp | 2 +- .../tx/transactors/lending/LoanSet.cpp | 2 +- src/test/app/Batch_test.cpp | 2 +- src/test/app/Delegate_test.cpp | 10 +- src/test/app/FixNFTokenPageLinks_test.cpp | 40 ++-- src/test/app/Invariants_test.cpp | 4 +- src/test/app/LedgerReplay_test.cpp | 6 +- src/test/app/LendingHelpers_test.cpp | 42 ++-- src/test/app/LoanBroker_test.cpp | 60 ++--- src/test/app/Loan_test.cpp | 54 ++--- src/test/app/PathMPT_test.cpp | 32 +-- src/test/app/Path_test.cpp | 40 ++-- src/test/app/PermissionedDEX_test.cpp | 16 +- src/test/app/SHAMapStore_test.cpp | 14 +- src/test/app/Sponsor_test.cpp | 14 +- src/test/app/TxQ_test.cpp | 10 +- src/test/app/Vault_test.cpp | 8 +- src/test/basics/PerfLog_test.cpp | 8 +- src/test/beast/IPEndpointCommon.h | 4 +- src/test/beast/IPEndpoint_test.cpp | 4 +- src/test/jtx/AMM.h | 2 +- src/test/jtx/Env.h | 2 +- src/test/jtx/TestHelpers.h | 4 +- src/test/jtx/impl/AMM.cpp | 2 +- src/test/jtx/impl/Env.cpp | 4 +- src/test/jtx/impl/TestHelpers.cpp | 14 +- src/test/jtx/impl/attester.cpp | 4 +- src/test/jtx/impl/ledgerStateFixes.cpp | 4 +- src/test/jtx/ledgerStateFix.h | 4 +- src/test/jtx/rpc.h | 2 +- src/test/overlay/TMGetObjectByHash_test.cpp | 12 +- src/test/overlay/compression_test.cpp | 2 +- src/test/overlay/reduce_relay_test.cpp | 6 +- src/test/overlay/tx_reduce_relay_test.cpp | 8 +- src/test/protocol/BuildInfo_test.cpp | 32 +-- src/test/protocol/InnerObjectFormats_test.cpp | 10 +- src/test/protocol/MultiApiJson_test.cpp | 28 +-- src/test/rpc/AccountLines_test.cpp | 36 +-- src/test/rpc/AccountTx_test.cpp | 22 +- src/test/rpc/Book_test.cpp | 8 +- src/test/rpc/Handler_test.cpp | 4 +- src/test/rpc/JSONRPC_test.cpp | 48 ++-- src/test/rpc/KeyGeneration_test.cpp | 4 +- src/test/rpc/LedgerEntry_test.cpp | 22 +- src/test/rpc/LedgerRPC_test.cpp | 2 +- src/test/rpc/LedgerRequest_test.cpp | 26 +- src/test/rpc/NoRippleCheck_test.cpp | 6 +- src/test/rpc/RPCCall_test.cpp | 8 +- src/test/rpc/RPCHelpers_test.cpp | 32 +-- src/test/rpc/Status_test.cpp | 6 +- src/test/rpc/TransactionEntry_test.cpp | 2 +- src/test/rpc/Transaction_test.cpp | 72 +++--- src/test/rpc/Version_test.cpp | 62 ++--- .../libxrpl/helpers/TestServiceRegistry.h | 2 +- src/tests/libxrpl/peerfinder/Livecache.cpp | 26 +- src/tests/libxrpl/peerfinder/PeerFinder.cpp | 58 ++--- src/tests/libxrpl/protocol/ApiVersion.cpp | 24 +- src/tests/libxrpl/resource/Logic.cpp | 20 +- src/xrpld/app/consensus/RCLConsensus.cpp | 2 +- src/xrpld/app/ledger/LedgerReplayer.h | 4 +- src/xrpld/app/ledger/LedgerToJson.h | 4 +- src/xrpld/app/ledger/detail/InboundLedger.cpp | 20 +- .../app/ledger/detail/InboundTransactions.cpp | 10 +- .../app/ledger/detail/LedgerDeltaAcquire.cpp | 10 +- src/xrpld/app/ledger/detail/LedgerMaster.cpp | 12 +- .../app/ledger/detail/LedgerReplayTask.cpp | 8 +- .../app/ledger/detail/LedgerReplayer.cpp | 4 +- src/xrpld/app/ledger/detail/LedgerToJson.cpp | 14 +- .../app/ledger/detail/SkipListAcquire.cpp | 10 +- src/xrpld/app/main/Application.cpp | 20 +- src/xrpld/app/main/CollectorManager.cpp | 4 +- src/xrpld/app/main/GRPCServer.cpp | 32 +-- src/xrpld/app/main/GRPCServer.h | 16 +- src/xrpld/app/main/Main.cpp | 6 +- src/xrpld/app/misc/DeliverMax.h | 4 +- src/xrpld/app/misc/NetworkOPs.cpp | 20 +- src/xrpld/app/misc/detail/DeliverMax.cpp | 4 +- src/xrpld/app/misc/detail/Transaction.cpp | 2 +- src/xrpld/app/misc/detail/WorkBase.h | 2 +- src/xrpld/app/rdb/PeerFinder.h | 2 +- src/xrpld/app/rdb/detail/PeerFinder.cpp | 8 +- src/xrpld/core/Config.h | 2 +- src/xrpld/overlay/Overlay.h | 4 +- src/xrpld/overlay/Peer.h | 8 +- src/xrpld/overlay/detail/ConnectAttempt.cpp | 6 +- src/xrpld/overlay/detail/ConnectAttempt.h | 10 +- src/xrpld/overlay/detail/Handshake.cpp | 22 +- src/xrpld/overlay/detail/Handshake.h | 12 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 60 ++--- src/xrpld/overlay/detail/OverlayImpl.h | 22 +- src/xrpld/overlay/detail/PeerImp.cpp | 226 +++++++++--------- src/xrpld/overlay/detail/PeerImp.h | 55 +++-- src/xrpld/overlay/detail/Tuning.h | 6 +- src/xrpld/overlay/make_Overlay.h | 2 +- src/xrpld/peerfinder/PeerfinderManager.h | 4 +- .../peerfinder/detail/PeerfinderConfig.cpp | 4 +- src/xrpld/peerfinder/detail/StoreSqdb.h | 6 +- src/xrpld/perflog/detail/PerfLogImp.h | 2 +- src/xrpld/rpc/BookChanges.h | 4 +- src/xrpld/rpc/CTID.h | 4 +- src/xrpld/rpc/Context.h | 8 +- src/xrpld/rpc/DeliveredAmount.h | 10 +- src/xrpld/rpc/GRPCHandlers.h | 10 +- src/xrpld/rpc/MPTokenIssuanceID.h | 4 +- src/xrpld/rpc/Output.h | 4 +- src/xrpld/rpc/RPCCall.h | 4 +- src/xrpld/rpc/RPCHandler.h | 6 +- src/xrpld/rpc/Role.h | 10 +- src/xrpld/rpc/ServerHandler.h | 10 +- src/xrpld/rpc/Status.h | 17 +- src/xrpld/rpc/detail/DeliveredAmount.cpp | 12 +- src/xrpld/rpc/detail/Handler.cpp | 24 +- src/xrpld/rpc/detail/Handler.h | 8 +- src/xrpld/rpc/detail/LegacyPathFind.cpp | 8 +- src/xrpld/rpc/detail/LegacyPathFind.h | 4 +- src/xrpld/rpc/detail/MPTokenIssuanceID.cpp | 4 +- src/xrpld/rpc/detail/PathRequest.cpp | 6 +- src/xrpld/rpc/detail/PathRequest.h | 4 +- src/xrpld/rpc/detail/PathRequestManager.cpp | 4 +- src/xrpld/rpc/detail/PathRequestManager.h | 4 +- src/xrpld/rpc/detail/RPCCall.cpp | 20 +- src/xrpld/rpc/detail/RPCHandler.cpp | 14 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 50 ++-- src/xrpld/rpc/detail/RPCHelpers.h | 8 +- src/xrpld/rpc/detail/RPCLedgerHelpers.cpp | 36 +-- src/xrpld/rpc/detail/RPCLedgerHelpers.h | 6 +- src/xrpld/rpc/detail/RPCSub.cpp | 12 +- src/xrpld/rpc/detail/Role.cpp | 14 +- src/xrpld/rpc/detail/ServerHandler.cpp | 76 +++--- src/xrpld/rpc/detail/Status.cpp | 8 +- src/xrpld/rpc/detail/TransactionSign.cpp | 156 ++++++------ src/xrpld/rpc/detail/TransactionSign.h | 8 +- src/xrpld/rpc/detail/Tuning.h | 4 +- src/xrpld/rpc/handlers/ChannelVerify.cpp | 4 +- src/xrpld/rpc/handlers/Handlers.h | 144 +++++------ src/xrpld/rpc/handlers/VaultInfo.cpp | 12 +- .../rpc/handlers/account/AccountChannels.cpp | 20 +- .../handlers/account/AccountCurrencies.cpp | 12 +- .../rpc/handlers/account/AccountInfo.cpp | 18 +- .../rpc/handlers/account/AccountLines.cpp | 22 +- .../rpc/handlers/account/AccountNFTs.cpp | 20 +- .../rpc/handlers/account/AccountObjects.cpp | 32 +-- .../rpc/handlers/account/AccountOffers.cpp | 28 +-- src/xrpld/rpc/handlers/account/AccountTx.cpp | 62 ++--- .../rpc/handlers/account/GatewayBalances.cpp | 14 +- .../rpc/handlers/account/NoRippleCheck.cpp | 20 +- src/xrpld/rpc/handlers/account/OwnerInfo.cpp | 4 +- src/xrpld/rpc/handlers/admin/BlackList.cpp | 2 +- src/xrpld/rpc/handlers/admin/UnlList.cpp | 2 +- .../rpc/handlers/admin/data/CanDelete.cpp | 10 +- .../rpc/handlers/admin/data/LedgerCleaner.cpp | 4 +- .../rpc/handlers/admin/data/LedgerRequest.cpp | 6 +- .../admin/keygen/ValidationCreate.cpp | 2 +- .../handlers/admin/keygen/WalletPropose.cpp | 10 +- src/xrpld/rpc/handlers/admin/log/LogLevel.cpp | 2 +- .../rpc/handlers/admin/log/LogRotate.cpp | 4 +- src/xrpld/rpc/handlers/admin/peer/Connect.cpp | 10 +- .../admin/peer/PeerReservationsAdd.cpp | 8 +- .../admin/peer/PeerReservationsDel.cpp | 6 +- .../admin/peer/PeerReservationsList.cpp | 2 +- src/xrpld/rpc/handlers/admin/peer/Peers.cpp | 2 +- .../admin/server_control/LedgerAccept.cpp | 2 +- .../handlers/admin/server_control/Stop.cpp | 8 +- .../admin/signing/ChannelAuthorize.cpp | 16 +- src/xrpld/rpc/handlers/admin/signing/Sign.cpp | 8 +- .../rpc/handlers/admin/signing/SignFor.cpp | 8 +- .../handlers/admin/status/ConsensusInfo.cpp | 2 +- .../rpc/handlers/admin/status/FetchInfo.cpp | 2 +- .../rpc/handlers/admin/status/GetCounts.cpp | 2 +- src/xrpld/rpc/handlers/admin/status/Print.cpp | 2 +- .../handlers/admin/status/ValidatorInfo.cpp | 4 +- .../admin/status/ValidatorListSites.cpp | 2 +- .../rpc/handlers/admin/status/Validators.cpp | 2 +- src/xrpld/rpc/handlers/ledger/Ledger.cpp | 10 +- src/xrpld/rpc/handlers/ledger/Ledger.h | 8 +- .../rpc/handlers/ledger/LedgerClosed.cpp | 2 +- .../rpc/handlers/ledger/LedgerCurrent.cpp | 2 +- src/xrpld/rpc/handlers/ledger/LedgerData.cpp | 20 +- src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp | 6 +- src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp | 173 +++++++------- .../rpc/handlers/ledger/LedgerEntryHelpers.h | 8 +- .../rpc/handlers/ledger/LedgerHeader.cpp | 4 +- src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp | 6 +- .../rpc/handlers/orderbook/BookChanges.cpp | 6 +- .../rpc/handlers/orderbook/BookOffers.cpp | 44 ++-- .../handlers/orderbook/DepositAuthorized.cpp | 46 ++-- .../handlers/orderbook/GetAggregatePrice.cpp | 32 +-- .../rpc/handlers/orderbook/NFTBuyOffers.cpp | 6 +- .../rpc/handlers/orderbook/NFTOffersHelpers.h | 10 +- .../rpc/handlers/orderbook/NFTSellOffers.cpp | 6 +- src/xrpld/rpc/handlers/orderbook/PathFind.cpp | 4 +- .../rpc/handlers/orderbook/RipplePathFind.cpp | 10 +- .../rpc/handlers/server_info/Feature.cpp | 2 +- src/xrpld/rpc/handlers/server_info/Fee.cpp | 4 +- .../rpc/handlers/server_info/Manifest.cpp | 6 +- .../server_info/ServerDefinitions.cpp | 4 +- .../rpc/handlers/server_info/ServerInfo.cpp | 2 +- .../rpc/handlers/server_info/ServerState.cpp | 2 +- src/xrpld/rpc/handlers/server_info/Version.h | 8 +- .../rpc/handlers/subscribe/Subscribe.cpp | 18 +- .../rpc/handlers/subscribe/Unsubscribe.cpp | 10 +- .../rpc/handlers/transaction/Simulate.cpp | 52 ++-- src/xrpld/rpc/handlers/transaction/Submit.cpp | 14 +- .../transaction/SubmitMultiSigned.cpp | 8 +- .../handlers/transaction/TransactionEntry.cpp | 6 +- src/xrpld/rpc/handlers/transaction/Tx.cpp | 28 +-- .../rpc/handlers/transaction/TxHistory.cpp | 6 +- .../handlers/transaction/TxReduceRelay.cpp | 2 +- src/xrpld/rpc/handlers/utility/Ping.cpp | 6 +- src/xrpld/rpc/handlers/utility/Random.cpp | 6 +- tests/conan/src/example.cpp | 2 +- 293 files changed, 2104 insertions(+), 2083 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 41df5470ff..02e90d9148 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -85,6 +85,8 @@ CheckOptions: readability-braces-around-statements.ShortStatementLines: 2 readability-identifier-naming.MacroDefinitionCase: UPPER_CASE + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.InlineNamespaceCase: lower_case readability-identifier-naming.ClassCase: CamelCase readability-identifier-naming.StructCase: CamelCase readability-identifier-naming.UnionCase: CamelCase diff --git a/include/xrpl/basics/Resolver.h b/include/xrpl/basics/Resolver.h index 239eb9630e..a159619517 100644 --- a/include/xrpl/basics/Resolver.h +++ b/include/xrpl/basics/Resolver.h @@ -11,7 +11,7 @@ namespace xrpl { class Resolver { public: - using HandlerType = std::function)>; + using HandlerType = std::function)>; virtual ~Resolver() = 0; diff --git a/include/xrpl/beast/insight/StatsDCollector.h b/include/xrpl/beast/insight/StatsDCollector.h index e14d3a27ff..0b44f345ba 100644 --- a/include/xrpl/beast/insight/StatsDCollector.h +++ b/include/xrpl/beast/insight/StatsDCollector.h @@ -26,7 +26,7 @@ public: * @param journal Destination for logging output. */ static std::shared_ptr - make(IP::Endpoint const& address, std::string const& prefix, Journal journal); + make(ip::Endpoint const& address, std::string const& prefix, Journal journal); }; } // namespace beast::insight diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index 4f4fb189a6..7422778ea2 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -15,7 +15,7 @@ //------------------------------------------------------------------------------ namespace beast { -namespace IP { +namespace ip { using Address = boost::asio::ip::address; @@ -73,13 +73,13 @@ isPublic(Address const& addr) return (addr.is_v4()) ? isPublic(addr.to_v4()) : isPublic(addr.to_v6()); } -} // namespace IP +} // namespace ip //------------------------------------------------------------------------------ template void -hash_append(Hasher& h, beast::IP::Address const& addr) noexcept +hash_append(Hasher& h, beast::ip::Address const& addr) noexcept { using beast::hash_append; if (addr.is_v4()) @@ -101,12 +101,12 @@ hash_append(Hasher& h, beast::IP::Address const& addr) noexcept namespace boost { template <> -struct hash<::beast::IP::Address> +struct hash<::beast::ip::Address> { explicit hash() = default; std::size_t - operator()(::beast::IP::Address const& addr) const + operator()(::beast::ip::Address const& addr) const { return ::beast::Uhash<>{}(addr); } diff --git a/include/xrpl/beast/net/IPAddressConversion.h b/include/xrpl/beast/net/IPAddressConversion.h index 73777cf841..e2d485642f 100644 --- a/include/xrpl/beast/net/IPAddressConversion.h +++ b/include/xrpl/beast/net/IPAddressConversion.h @@ -4,7 +4,7 @@ #include -namespace beast::IP { +namespace beast::ip { /** * Convert to Endpoint. @@ -32,7 +32,7 @@ toAsioAddress(Endpoint const& endpoint); boost::asio::ip::tcp::endpoint toAsioEndpoint(Endpoint const& endpoint); -} // namespace beast::IP +} // namespace beast::ip namespace beast { @@ -41,25 +41,25 @@ struct IPAddressConversion { explicit IPAddressConversion() = default; - static IP::Endpoint + static ip::Endpoint fromAsio(boost::asio::ip::address const& address) { - return IP::fromAsio(address); + return ip::fromAsio(address); } - static IP::Endpoint + static ip::Endpoint fromAsio(boost::asio::ip::tcp::endpoint const& endpoint) { - return IP::fromAsio(endpoint); + return ip::fromAsio(endpoint); } static boost::asio::ip::address - toAsioAddress(IP::Endpoint const& address) + toAsioAddress(ip::Endpoint const& address) { - return IP::toAsioAddress(address); + return ip::toAsioAddress(address); } static boost::asio::ip::tcp::endpoint - toAsioEndpoint(IP::Endpoint const& address) + toAsioEndpoint(ip::Endpoint const& address) { - return IP::toAsioEndpoint(address); + return ip::toAsioEndpoint(address); } }; diff --git a/include/xrpl/beast/net/IPAddressV4.h b/include/xrpl/beast/net/IPAddressV4.h index 94943af3ea..280c2c791c 100644 --- a/include/xrpl/beast/net/IPAddressV4.h +++ b/include/xrpl/beast/net/IPAddressV4.h @@ -2,7 +2,7 @@ #include -namespace beast::IP { +namespace beast::ip { using AddressV4 = boost::asio::ip::address_v4; @@ -25,4 +25,4 @@ isPublic(AddressV4 const& addr); char getClass(AddressV4 const& address); -} // namespace beast::IP +} // namespace beast::ip diff --git a/include/xrpl/beast/net/IPAddressV6.h b/include/xrpl/beast/net/IPAddressV6.h index b51cb62532..0659e7e405 100644 --- a/include/xrpl/beast/net/IPAddressV6.h +++ b/include/xrpl/beast/net/IPAddressV6.h @@ -2,7 +2,7 @@ #include -namespace beast::IP { +namespace beast::ip { using AddressV6 = boost::asio::ip::address_v6; @@ -18,4 +18,4 @@ isPrivate(AddressV6 const& addr); bool isPublic(AddressV6 const& addr); -} // namespace beast::IP +} // namespace beast::ip diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index c4b269e9c3..d4d3b2ab12 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -13,7 +13,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { using Port = std::uint16_t; @@ -223,7 +223,7 @@ operator<<(OutputStream& os, Endpoint const& endpoint) std::istream& operator>>(std::istream& is, Endpoint& endpoint); -} // namespace beast::IP +} // namespace beast::ip //------------------------------------------------------------------------------ @@ -232,12 +232,12 @@ namespace std { * std::hash support. */ template <> -struct hash<::beast::IP::Endpoint> +struct hash<::beast::ip::Endpoint> { hash() = default; std::size_t - operator()(::beast::IP::Endpoint const& endpoint) const + operator()(::beast::ip::Endpoint const& endpoint) const { return ::beast::Uhash<>{}(endpoint); } @@ -249,12 +249,12 @@ namespace boost { * boost::hash support. */ template <> -struct hash<::beast::IP::Endpoint> +struct hash<::beast::ip::Endpoint> { hash() = default; std::size_t - operator()(::beast::IP::Endpoint const& endpoint) const + operator()(::beast::ip::Endpoint const& endpoint) const { return ::beast::Uhash<>{}(endpoint); } diff --git a/include/xrpl/core/ServiceRegistry.h b/include/xrpl/core/ServiceRegistry.h index 2747ecd9e8..000bdaa7fa 100644 --- a/include/xrpl/core/ServiceRegistry.h +++ b/include/xrpl/core/ServiceRegistry.h @@ -18,9 +18,9 @@ namespace xrpl { namespace node_store { class Database; } // namespace node_store -namespace Resource { +namespace resource { class Manager; -} // namespace Resource +} // namespace resource namespace perf { class PerfLog; } // namespace perf @@ -160,7 +160,7 @@ public: virtual PeerReservationTable& getPeerReservations() = 0; - virtual Resource::Manager& + virtual resource::Manager& getResourceManager() = 0; // Storage services diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index fef18e3e09..c69efff964 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -296,7 +296,7 @@ struct AccountingDeltas // Whole-life (pre-LendingProtocolV1_1) recognition model: interest is // recognized into AssetsTotal/DebtTotal up front, at origination. -namespace Accrual { +namespace accrual { // LoanSet origination: what's added to Vault.AssetsTotal and LoanBroker.DebtTotal AccountingDeltas @@ -318,11 +318,11 @@ loanVaultExposure(SLE::const_ref loanSle); AccountingDeltas loanPaymentDeltas(LoanPaymentParts const& parts); -} // namespace Accrual +} // namespace accrual // Cash-basis (LendingProtocolV1_1) recognition model: AssetsTotal/DebtTotal // are principal-only, interest is recognized only as it's actually paid. -namespace CashBasis { +namespace cash_basis { AccountingDeltas loanOriginationDeltas(Number const& principalRequested); @@ -333,11 +333,11 @@ loanVaultExposure(SLE::const_ref loanSle); AccountingDeltas loanPaymentDeltas(LoanPaymentParts const& parts); -} // namespace CashBasis +} // namespace cash_basis -// Public dispatchers: pick CashBasis:: if featureLendingProtocolV1_1 is +// Public dispatchers: pick cash_basis:: if featureLendingProtocolV1_1 is // enabled AND the Vault's LEVersion (VaultHelpers::getVaultVersion) is -// VaultVersion::CashBasis, else Accrual::. These are the only entry points +// VaultVersion::CashBasis, else accrual::. These are the only entry points // transactors call. AccountingDeltas loanOriginationDeltas( diff --git a/include/xrpl/net/AutoSocket.h b/include/xrpl/net/AutoSocket.h index b98885959d..d090247388 100644 --- a/include/xrpl/net/AutoSocket.h +++ b/include/xrpl/net/AutoSocket.h @@ -67,16 +67,16 @@ public: return socket_->next_layer(); } - beast::IP::Endpoint + beast::ip::Endpoint localEndpoint() { - return beast::IP::fromAsio(lowestLayer().local_endpoint()); + return beast::ip::fromAsio(lowestLayer().local_endpoint()); } - beast::IP::Endpoint + beast::ip::Endpoint remoteEndpoint() { - return beast::IP::fromAsio(lowestLayer().remote_endpoint()); + return beast::ip::fromAsio(lowestLayer().remote_endpoint()); } lowest_layer_type& diff --git a/include/xrpl/peerfinder/Config.h b/include/xrpl/peerfinder/Config.h index 9ff0d342c3..3326ae8a97 100644 --- a/include/xrpl/peerfinder/Config.h +++ b/include/xrpl/peerfinder/Config.h @@ -9,7 +9,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { struct PeerLimitConfig { @@ -28,7 +28,7 @@ struct Config * This includes both inbound and outbound, but does not include * fixed peers. */ - std::size_t maxPeers{Tuning::kDefaultMaxPeers}; + std::size_t maxPeers{tuning::kDefaultMaxPeers}; /** * The number of automatic outbound connections to maintain. @@ -100,7 +100,7 @@ struct Config onWrite(beast::PropertyStream::Map& map) const; /** - * Make PeerFinder::Config from peer limit and server mode parameters. + * Make peer_finder::Config from peer limit and server mode parameters. */ static Config makeConfig( @@ -160,4 +160,4 @@ to_string(Result result) noexcept return "unknown"; } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/PeerfinderManager.h b/include/xrpl/peerfinder/PeerfinderManager.h index ed683520c1..bb03d85537 100644 --- a/include/xrpl/peerfinder/PeerfinderManager.h +++ b/include/xrpl/peerfinder/PeerfinderManager.h @@ -15,7 +15,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Maintains a set of IP addresses used for getting into the network. @@ -68,17 +68,17 @@ public: * file, along with the set of corresponding IP addresses. */ virtual void - addFixedPeer(std::string_view name, std::vector const& addresses) = 0; + addFixedPeer(std::string_view name, std::vector const& addresses) = 0; /** - * Add a set of strings as fallback IP::Endpoint sources. + * Add a set of strings as fallback ip::Endpoint sources. * @param name A label used for diagnostics. */ virtual void addFallbackStrings(std::string const& name, std::vector const& strings) = 0; /** - * Add a URL as a fallback location to obtain IP::Endpoint sources. + * Add a URL as a fallback location to obtain ip::Endpoint sources. * @param name A label used for diagnostics. */ /* VFALCO NOTE Unimplemented @@ -95,8 +95,8 @@ public: */ virtual std::pair, Result> newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) = 0; + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint const& remoteEndpoint) = 0; /** * Create a new outbound slot with the specified remote endpoint. @@ -104,7 +104,7 @@ public: * Usually this is because of a duplicate connection. */ virtual std::pair, Result> - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) = 0; + newOutboundSlot(beast::ip::Endpoint const& remoteEndpoint) = 0; /** * Called when mtENDPOINTS is received. @@ -145,7 +145,7 @@ public: * @return `true` if the connection should be kept */ virtual bool - onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) = 0; + onConnected(std::shared_ptr const& slot, beast::ip::Endpoint const& localEndpoint) = 0; /** * Request an active slot type. @@ -162,7 +162,7 @@ public: /** * Return a set of addresses we should connect to. */ - virtual std::vector + virtual std::vector autoconnect() = 0; virtual std::vector, std::vector>> @@ -176,4 +176,4 @@ public: oncePerSecond() = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/Slot.h b/include/xrpl/peerfinder/Slot.h index 9db39ac94c..58e094afc4 100644 --- a/include/xrpl/peerfinder/Slot.h +++ b/include/xrpl/peerfinder/Slot.h @@ -7,7 +7,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Properties and state associated with a peer to peer overlay connection. @@ -52,13 +52,13 @@ public: /** * The remote endpoint of socket. */ - [[nodiscard]] virtual beast::IP::Endpoint const& + [[nodiscard]] virtual beast::ip::Endpoint const& remoteEndpoint() const = 0; /** * The local endpoint of the socket, when known. */ - [[nodiscard]] virtual std::optional const& + [[nodiscard]] virtual std::optional const& localEndpoint() const = 0; [[nodiscard]] virtual std::optional @@ -72,4 +72,4 @@ public: publicKey() const = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/Types.h b/include/xrpl/peerfinder/Types.h index 9e82d9d65c..1327f2564f 100644 --- a/include/xrpl/peerfinder/Types.h +++ b/include/xrpl/peerfinder/Types.h @@ -8,14 +8,14 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { using clock_type = beast::AbstractClock; /** * Represents a set of addresses. */ -using IPAddresses = std::vector; +using IPAddresses = std::vector; //------------------------------------------------------------------------------ @@ -26,10 +26,10 @@ struct Endpoint { Endpoint() = default; - Endpoint(beast::IP::Endpoint ep, std::uint32_t hops); + Endpoint(beast::ip::Endpoint ep, std::uint32_t hops); std::uint32_t hops = 0; - beast::IP::Endpoint address; + beast::ip::Endpoint address; }; inline bool @@ -43,4 +43,4 @@ operator<(Endpoint const& lhs, Endpoint const& rhs) */ using Endpoints = std::vector; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Bootcache.h b/include/xrpl/peerfinder/detail/Bootcache.h index 2141a374fa..453d9c22d2 100644 --- a/include/xrpl/peerfinder/detail/Bootcache.h +++ b/include/xrpl/peerfinder/detail/Bootcache.h @@ -14,7 +14,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Stores IP addresses useful for gaining initial connections. @@ -65,7 +65,7 @@ private: }; using left_t = boost::bimaps:: - unordered_set_of, std::equal_to<>>; + unordered_set_of, std::equal_to<>>; using right_t = boost::bimaps::multiset_of>; using map_type = boost::bimap; using value_type = map_type::value_type; @@ -73,11 +73,11 @@ private: struct Transform { using first_argument_type = map_type::right_map::const_iterator::value_type const&; - using result_type = beast::IP::Endpoint const&; + using result_type = beast::ip::Endpoint const&; explicit Transform() = default; - beast::IP::Endpoint const& + beast::ip::Endpoint const& operator()(map_type::right_map::const_iterator::value_type const& v) const { return v.get_left(); @@ -121,7 +121,7 @@ public: size() const; /** - * IP::Endpoint iterators that traverse in decreasing valence. + * ip::Endpoint iterators that traverse in decreasing valence. */ /** @{ */ [[nodiscard]] const_iterator @@ -146,25 +146,25 @@ public: * Add a newly-learned address to the cache. */ bool - insert(beast::IP::Endpoint const& endpoint); + insert(beast::ip::Endpoint const& endpoint); /** * Add a staticallyconfigured address to the cache. */ bool - insertStatic(beast::IP::Endpoint const& endpoint); + insertStatic(beast::ip::Endpoint const& endpoint); /** * Called when an outbound connection handshake completes. */ void - onSuccess(beast::IP::Endpoint const& endpoint); + onSuccess(beast::ip::Endpoint const& endpoint); /** * Called when an outbound connection attempt fails to handshake. */ void - onFailure(beast::IP::Endpoint const& endpoint); + onFailure(beast::ip::Endpoint const& endpoint); /** * Stores the cache in the persistent database on a timer. @@ -189,4 +189,4 @@ private: flagForUpdate(); }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Checker.h b/include/xrpl/peerfinder/detail/Checker.h index 28ec83adb1..e1ac1d44e0 100644 --- a/include/xrpl/peerfinder/detail/Checker.h +++ b/include/xrpl/peerfinder/detail/Checker.h @@ -11,7 +11,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Tests remote listening sockets to make sure they are connectable. @@ -104,7 +104,7 @@ public: */ template void - asyncConnect(beast::IP::Endpoint const& endpoint, Handler&& handler); + asyncConnect(beast::ip::Endpoint const& endpoint, Handler&& handler); private: void @@ -179,7 +179,7 @@ Checker::wait() template template void -Checker::asyncConnect(beast::IP::Endpoint const& endpoint, Handler&& handler) +Checker::asyncConnect(beast::ip::Endpoint const& endpoint, Handler&& handler) { auto const op = std::make_shared>(*this, ioContext_, std::forward(handler)); @@ -202,4 +202,4 @@ Checker::remove(BasicAsyncOp& op) cond_.notify_all(); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Counts.h b/include/xrpl/peerfinder/detail/Counts.h index ce78eadce4..035103463e 100644 --- a/include/xrpl/peerfinder/detail/Counts.h +++ b/include/xrpl/peerfinder/detail/Counts.h @@ -10,7 +10,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Direction of a slot count adjustment. @@ -50,7 +50,7 @@ public: // Must be handshaked and in the right state XRPL_ASSERT( s.state() == Slot::State::Connected || s.state() == Slot::State::Accept, - "xrpl::PeerFinder::Counts::can_activate : valid input state"); + "xrpl::peer_finder::Counts::can_activate : valid input state"); if (s.fixed() || s.reserved()) return true; @@ -67,9 +67,9 @@ public: [[nodiscard]] std::size_t attemptsNeeded() const { - if (attempts_ >= Tuning::kMaxConnectAttempts) + if (attempts_ >= tuning::kMaxConnectAttempts) return 0; - return Tuning::kMaxConnectAttempts - attempts_; + return tuning::kMaxConnectAttempts - attempts_; } /** @@ -295,7 +295,7 @@ private: switch (s.state()) { case Slot::State::Accept: - XRPL_ASSERT(s.inbound(), "xrpl::PeerFinder::Counts::adjust : input is inbound"); + XRPL_ASSERT(s.inbound(), "xrpl::peer_finder::Counts::adjust : input is inbound"); acceptCount_ += n; break; @@ -303,7 +303,7 @@ private: case Slot::State::Connected: XRPL_ASSERT( !s.inbound(), - "xrpl::PeerFinder::Counts::adjust : input is not " + "xrpl::peer_finder::Counts::adjust : input is not " "inbound"); attempts_ += n; break; @@ -331,7 +331,7 @@ private: // LCOV_EXCL_START default: - UNREACHABLE("xrpl::PeerFinder::Counts::adjust : invalid input state"); + UNREACHABLE("xrpl::peer_finder::Counts::adjust : invalid input state"); break; // LCOV_EXCL_STOP }; @@ -391,4 +391,4 @@ private: int closingCount_{0}; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Fixed.h b/include/xrpl/peerfinder/detail/Fixed.h index 6754ec6dbd..5a52fd7c3e 100644 --- a/include/xrpl/peerfinder/detail/Fixed.h +++ b/include/xrpl/peerfinder/detail/Fixed.h @@ -7,7 +7,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Metadata for a Fixed slot. @@ -36,8 +36,8 @@ public: void failure(clock_type::time_point const& now) { - failures_ = std::min(failures_ + 1, Tuning::kConnectionBackoff.size() - 1); - when_ = now + std::chrono::minutes(Tuning::kConnectionBackoff[failures_]); + failures_ = std::min(failures_ + 1, tuning::kConnectionBackoff.size() - 1); + when_ = now + std::chrono::minutes(tuning::kConnectionBackoff[failures_]); } /** @@ -55,4 +55,4 @@ private: std::size_t failures_{0}; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Handouts.h b/include/xrpl/peerfinder/detail/Handouts.h index cb5fd7f850..c20d4b2139 100644 --- a/include/xrpl/peerfinder/detail/Handouts.h +++ b/include/xrpl/peerfinder/detail/Handouts.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { namespace detail { @@ -28,7 +28,7 @@ template std::size_t handoutOne(Target& t, HopContainer& h) { - XRPL_ASSERT(!t.full(), "xrpl::PeerFinder::detail::handoutOne : target is not full"); + XRPL_ASSERT(!t.full(), "xrpl::peer_finder::detail::handoutOne : target is not full"); for (auto it = h.begin(); it != h.end(); ++it) { auto const& e = *it; @@ -95,7 +95,7 @@ public: [[nodiscard]] bool full() const { - return list_.size() >= Tuning::kRedirectEndpointCount; + return list_.size() >= tuning::kRedirectEndpointCount; } [[nodiscard]] SlotImp::ptr const& @@ -124,7 +124,7 @@ private: template RedirectHandouts::RedirectHandouts(SlotImp::ptr slot) : slot_(std::move(slot)) { - list_.reserve(Tuning::kRedirectEndpointCount); + list_.reserve(tuning::kRedirectEndpointCount); } template @@ -138,7 +138,7 @@ RedirectHandouts::tryInsert(Endpoint const& ep) // addresses in a peer HTTP handshake instead of // the tmENDPOINTS message. // - if (ep.hops > Tuning::kMaxHops) + if (ep.hops > tuning::kMaxHops) return false; // Don't send them our address @@ -181,7 +181,7 @@ public: [[nodiscard]] bool full() const { - return list_.size() >= Tuning::kNumberOfEndpoints; + return list_.size() >= tuning::kNumberOfEndpoints; } void @@ -210,7 +210,7 @@ private: template SlotHandouts::SlotHandouts(SlotImp::ptr slot) : slot_(std::move(slot)) { - list_.reserve(Tuning::kNumberOfEndpoints); + list_.reserve(tuning::kNumberOfEndpoints); } template @@ -220,7 +220,7 @@ SlotHandouts::tryInsert(Endpoint const& ep) if (full()) return false; - if (ep.hops > Tuning::kMaxHops) + if (ep.hops > tuning::kMaxHops) return false; if (slot_->recent.filter(ep.address, ep.hops)) @@ -259,9 +259,9 @@ class ConnectHandouts public: // Keeps track of addresses we have made outgoing connections // to, for the purposes of not connecting to them too frequently. - using Squelches = beast::aged_set; + using Squelches = beast::aged_set; - using list_type = std::vector; + using list_type = std::vector; private: std::size_t needed_; @@ -274,7 +274,7 @@ public: template bool - tryInsert(beast::IP::Endpoint const& endpoint); + tryInsert(beast::ip::Endpoint const& endpoint); [[nodiscard]] bool empty() const @@ -316,13 +316,13 @@ ConnectHandouts::ConnectHandouts(std::size_t needed, Squelches& squelches) template bool -ConnectHandouts::tryInsert(beast::IP::Endpoint const& endpoint) +ConnectHandouts::tryInsert(beast::ip::Endpoint const& endpoint) { if (full()) return false; // Make sure the address isn't already in our list - if (std::ranges::any_of(list_, [&endpoint](beast::IP::Endpoint const& other) { + if (std::ranges::any_of(list_, [&endpoint](beast::ip::Endpoint const& other) { // Ignore port for security reasons return other.address() == endpoint.address(); })) @@ -341,4 +341,4 @@ ConnectHandouts::tryInsert(beast::IP::Endpoint const& endpoint) return true; } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Livecache.h b/include/xrpl/peerfinder/detail/Livecache.h index cac284d1cc..ec797065e5 100644 --- a/include/xrpl/peerfinder/detail/Livecache.h +++ b/include/xrpl/peerfinder/detail/Livecache.h @@ -29,7 +29,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { template class Livecache; @@ -188,10 +188,10 @@ class Livecache : protected detail::LivecacheBase { private: using cache_type = beast::aged_map< - beast::IP::Endpoint, + beast::ip::Endpoint, Element, std::chrono::steady_clock, - std::less, + std::less, Allocator>; beast::Journal journal_; @@ -220,8 +220,8 @@ public: // but not given out (since they would exceed maxHops). They // are used for automatic connection attempts. // - using Histogram = std::array; - using lists_type = std::array; + using Histogram = std::array; + using lists_type = std::array; template struct Transform @@ -400,7 +400,7 @@ Livecache::expire() { std::size_t n(0); typename cache_type::time_point const expired( - cache_.clock().now() - Tuning::kLiveCacheSecondsToLive); + cache_.clock().now() - tuning::kLiveCacheSecondsToLive); for (auto iter(cache_.chronological.begin()); iter != cache_.chronological.end() && iter.when() <= expired;) { @@ -427,8 +427,8 @@ Livecache::insert(Endpoint const& ep) // when redirecting. // XRPL_ASSERT( - ep.hops <= (Tuning::kMaxHops + 1), - "xrpl::PeerFinder::Livecache::insert : maximum input hops"); + ep.hops <= (tuning::kMaxHops + 1), + "xrpl::peer_finder::Livecache::insert : maximum input hops"); auto result = cache_.emplace(ep.address, ep); Element& e(result.first->second); if (result.second) @@ -468,7 +468,7 @@ void Livecache::onWrite(beast::PropertyStream::Map& map) { typename cache_type::time_point const expired( - cache_.clock().now() - Tuning::kLiveCacheSecondsToLive); + cache_.clock().now() - tuning::kLiveCacheSecondsToLive); map["size"] = size(); map["hist"] = hops.histogram(); beast::PropertyStream::Set set("entries", map); @@ -527,8 +527,8 @@ void Livecache::HopsT::insert(Element& e) { XRPL_ASSERT( - e.endpoint.hops <= Tuning::kMaxHops + 1, - "xrpl::PeerFinder::Livecache::HopsT::insert : maximum input hops"); + e.endpoint.hops <= tuning::kMaxHops + 1, + "xrpl::peer_finder::Livecache::HopsT::insert : maximum input hops"); // This has security implications without a shuffle lists_[e.endpoint.hops].push_front(e); ++hist_[e.endpoint.hops]; @@ -539,8 +539,8 @@ void Livecache::HopsT::reinsert(Element& e, std::uint32_t numHops) { XRPL_ASSERT( - numHops <= Tuning::kMaxHops + 1, - "xrpl::PeerFinder::Livecache::HopsT::reinsert : maximum hops input"); + numHops <= tuning::kMaxHops + 1, + "xrpl::peer_finder::Livecache::HopsT::reinsert : maximum hops input"); auto& list = lists_[e.endpoint.hops]; list.erase(list.iterator_to(e)); @@ -561,4 +561,4 @@ Livecache::HopsT::remove(Element& e) list.erase(list.iterator_to(e)); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Logic.h b/include/xrpl/peerfinder/detail/Logic.h index c623263884..4821054280 100644 --- a/include/xrpl/peerfinder/detail/Logic.h +++ b/include/xrpl/peerfinder/detail/Logic.h @@ -43,7 +43,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * The Logic for maintaining the list of Slot addresses. @@ -57,7 +57,7 @@ public: // Maps remote endpoints to slots. Since a slot has a // remote endpoint upon construction, this holds all counts_. // - using Slots = std::map>; + using Slots = std::map>; beast::Journal journal; clock_type& clock; @@ -81,7 +81,7 @@ private: Counts counts_; // A list of slots that should always be connected - std::map fixed_; + std::map fixed_; public: // Live livecache from mtENDPOINTS messages @@ -96,7 +96,7 @@ public: // The addresses (but not port) we are connected to. This includes // outgoing connection attempts. Note that this set can contain // duplicates (since the port is not set) - std::multiset connectedAddresses; + std::multiset connectedAddresses; // Set of public keys belonging to active peers std::set keys; @@ -170,13 +170,13 @@ public: } void - addFixedPeer(std::string_view name, beast::IP::Endpoint const& ep) + addFixedPeer(std::string_view name, beast::ip::Endpoint const& ep) { - addFixedPeer(name, std::vector{ep}); + addFixedPeer(name, std::vector{ep}); } void - addFixedPeer(std::string_view name, std::vector const& addresses) + addFixedPeer(std::string_view name, std::vector const& addresses) { std::scoped_lock const _(lock); @@ -213,8 +213,8 @@ public: // Called when the Checker completes a connectivity test void checkComplete( - beast::IP::Endpoint const& remoteAddress, - beast::IP::Endpoint const& checkedAddress, + beast::ip::Endpoint const& remoteAddress, + beast::ip::Endpoint const& checkedAddress, boost::system::error_code ec) { if (ec == boost::asio::error::operation_aborted) @@ -256,8 +256,8 @@ public: std::pair newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint const& remoteEndpoint) { JLOG(journal.debug()) << std::left << std::setw(18) << "Logic accept" << remoteEndpoint << " on local " << localEndpoint; @@ -293,7 +293,7 @@ public: // Remote address must not already exist XRPL_ASSERT( result.second, - "xrpl::PeerFinder::Logic::new_inbound_slot : remote endpoint " + "xrpl::peer_finder::Logic::new_inbound_slot : remote endpoint " "inserted"); // Add to the connected address list connectedAddresses.emplace(remoteEndpoint.address()); @@ -306,7 +306,7 @@ public: // Can't check for self-connect because we don't know the local endpoint std::pair - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) + newOutboundSlot(beast::ip::Endpoint const& remoteEndpoint) { JLOG(journal.debug()) << std::left << std::setw(18) << "Logic connect " << remoteEndpoint; @@ -329,7 +329,7 @@ public: // Remote address must not already exist XRPL_ASSERT( result.second, - "xrpl::PeerFinder::Logic::new_outbound_slot : remote endpoint " + "xrpl::peer_finder::Logic::new_outbound_slot : remote endpoint " "inserted"); // Add to the connected address list @@ -342,7 +342,7 @@ public: } bool - onConnected(SlotImp::ptr const& slot, beast::IP::Endpoint const& localEndpoint) + onConnected(SlotImp::ptr const& slot, beast::ip::Endpoint const& localEndpoint) { beast::WrappedSink sink{journal.sink(), slot->prefix()}; beast::Journal const journal{sink}; @@ -354,7 +354,7 @@ public: // The object must exist in our table XRPL_ASSERT( slots.contains(slot->remoteEndpoint()), - "xrpl::PeerFinder::Logic::onConnected : valid slot input"); + "xrpl::peer_finder::Logic::onConnected : valid slot input"); // Assign the local endpoint now that it's known slot->localEndpoint(localEndpoint); @@ -365,7 +365,7 @@ public: { XRPL_ASSERT( iter->second->localEndpoint() == slot->remoteEndpoint(), - "xrpl::PeerFinder::Logic::onConnected : local and remote " + "xrpl::peer_finder::Logic::onConnected : local and remote " "endpoints do match"); JLOG(journal.warn()) << "Logic dropping as self connect"; return false; @@ -393,11 +393,11 @@ public: // The object must exist in our table XRPL_ASSERT( slots.contains(slot->remoteEndpoint()), - "xrpl::PeerFinder::Logic::activate : valid slot input"); + "xrpl::peer_finder::Logic::activate : valid slot input"); // Must be accepted or connected XRPL_ASSERT( slot->state() == Slot::State::Accept || slot->state() == Slot::State::Connected, - "xrpl::PeerFinder::Logic::activate : valid slot state"); + "xrpl::peer_finder::Logic::activate : valid slot state"); // Check for duplicate connection by key if (keys.contains(key)) @@ -425,7 +425,7 @@ public: { [[maybe_unused]] bool const inserted = keys.insert(key).second; // Public key must not already exist - XRPL_ASSERT(inserted, "xrpl::PeerFinder::Logic::activate : public key inserted"); + XRPL_ASSERT(inserted, "xrpl::peer_finder::Logic::activate : public key inserted"); } // Change state and update counts @@ -443,7 +443,7 @@ public: if (iter == fixed_.end()) { logicError( - "PeerFinder::Logic::activate(): remote_endpoint " + "peer_finder::Logic::activate(): remote_endpoint " "missing from fixed_"); } @@ -476,10 +476,10 @@ public: // VFALCO TODO This should add the returned addresses to the // squelch list in one go once the list is built, // rather than having each module add to the squelch list. - std::vector + std::vector autoconnect() { - std::vector none; + std::vector none; std::scoped_lock const _(lock); @@ -635,7 +635,7 @@ public: // either. ipv6 has a slightly more compact string // representation of 0, so use that for self entries. ep.address = - beast::IP::Endpoint(beast::IP::AddressV6()).atPort(config_.listeningPort); + beast::ip::Endpoint(beast::ip::AddressV6()).atPort(config_.listeningPort); for (auto& t : targets) t.insert(ep); } @@ -656,7 +656,7 @@ public: result.emplace_back(slot, list); } - whenBroadcast = now + Tuning::kSecondsPerMessage; + whenBroadcast = now + tuning::kSecondsPerMessage; } return result; @@ -675,7 +675,7 @@ public: entry.second->expire(); // Expire the recent attempts table - beast::expire(squelches, Tuning::kRecentAttemptDuration); + beast::expire(squelches, tuning::kRecentAttemptDuration); bootcache.periodicActivity(); } @@ -692,7 +692,7 @@ public: Endpoint& ep(*iter); // Enforce hop limit - if (ep.hops > Tuning::kMaxHops) + if (ep.hops > tuning::kMaxHops) { JLOG(journal.debug()) << std::left << std::setw(18) << "Endpoints drop " << ep.address << " for excess hops " << ep.hops; @@ -754,10 +754,10 @@ public: beast::Journal const journal{sink}; // If we're sent too many endpoints, sample them at random: - if (list.size() > Tuning::kNumberOfEndpointsMax) + if (list.size() > tuning::kNumberOfEndpointsMax) { std::shuffle(list.begin(), list.end(), defaultPrng()); - list.resize(Tuning::kNumberOfEndpointsMax); + list.resize(tuning::kNumberOfEndpointsMax); } JLOG(journal.trace()) << "Endpoints contained " << list.size() @@ -768,12 +768,12 @@ public: // The object must exist in our table XRPL_ASSERT( slots.contains(slot->remoteEndpoint()), - "xrpl::PeerFinder::Logic::onEndpoints : valid slot input"); + "xrpl::peer_finder::Logic::onEndpoints : valid slot input"); // Must be handshaked! XRPL_ASSERT( slot->state() == Slot::State::Active, - "xrpl::PeerFinder::Logic::onEndpoints : valid slot state"); + "xrpl::peer_finder::Logic::onEndpoints : valid slot state"); clock_type::time_point const now(clock.now()); @@ -785,7 +785,7 @@ public: for (auto const& ep : list) { - XRPL_ASSERT(ep.hops, "xrpl::PeerFinder::Logic::onEndpoints : nonzero hops"); + XRPL_ASSERT(ep.hops, "xrpl::peer_finder::Logic::onEndpoints : nonzero hops"); slot->recent.insert(ep.address, ep.hops); @@ -837,7 +837,7 @@ public: bootcache.insert(ep.address); } - slot->whenAcceptEndpoints = now + Tuning::kSecondsPerMessage; + slot->whenAcceptEndpoints = now + tuning::kSecondsPerMessage; } //-------------------------------------------------------------------------- @@ -851,7 +851,7 @@ public: if (iter == slots.end()) { logicError( - "PeerFinder::Logic::remove(): remote_endpoint " + "peer_finder::Logic::remove(): remote_endpoint " "missing from slots_"); } @@ -866,7 +866,7 @@ public: if (iter == keys.end()) { logicError( - "PeerFinder::Logic::remove(): public_key missing " + "peer_finder::Logic::remove(): public_key missing " "from keys_"); } @@ -879,7 +879,7 @@ public: if (iter == connectedAddresses.end()) { logicError( - "PeerFinder::Logic::remove(): remote_endpoint " + "peer_finder::Logic::remove(): remote_endpoint " "address missing from connectedAddresses_"); } @@ -907,7 +907,7 @@ public: if (iter == fixed_.end()) { logicError( - "PeerFinder::Logic::on_closed(): remote_endpoint " + "peer_finder::Logic::on_closed(): remote_endpoint " "missing from fixed_"); } @@ -943,7 +943,7 @@ public: // LCOV_EXCL_START default: UNREACHABLE( - "xrpl::PeerFinder::Logic::on_closed : invalid slot " + "xrpl::peer_finder::Logic::on_closed : invalid slot " "state"); break; // LCOV_EXCL_STOP @@ -968,17 +968,17 @@ public: // Returns `true` if the address matches a fixed slot address // Must have the lock held bool - fixed(beast::IP::Endpoint const& endpoint) const + fixed(beast::ip::Endpoint const& endpoint) const { return std::ranges::any_of( fixed_, [&endpoint](auto const& entry) { return entry.first == endpoint; }); } // Returns `true` if the address matches a fixed slot address - // Note that this does not use the port information in the IP::Endpoint + // Note that this does not use the port information in the ip::Endpoint // Must have the lock held bool - fixed(beast::IP::Address const& address) const + fixed(beast::ip::Address const& address) const { return std::ranges::any_of( fixed_, [&address](auto const& entry) { return entry.first.address() == address; }); @@ -1097,9 +1097,9 @@ public: // //-------------------------------------------------------------------------- - // Returns true if the IP::Endpoint contains no invalid data. + // Returns true if the ip::Endpoint contains no invalid data. bool - isValidAddress(beast::IP::Endpoint const& address) + isValidAddress(beast::ip::Endpoint const& address) { if (isUnspecified(address)) return false; @@ -1220,7 +1220,7 @@ Logic::onRedirects( { std::scoped_lock const _(lock); std::size_t n = 0; - for (; first != last && n < Tuning::kMaxRedirects; ++first, ++n) + for (; first != last && n < tuning::kMaxRedirects; ++first, ++n) bootcache.insert(beast::IPAddressConversion::fromAsio(*first)); if (n > 0) { @@ -1229,4 +1229,4 @@ Logic::onRedirects( } } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/SlotImp.h b/include/xrpl/peerfinder/detail/SlotImp.h index 35c61b13cf..db86183f64 100644 --- a/include/xrpl/peerfinder/detail/SlotImp.h +++ b/include/xrpl/peerfinder/detail/SlotImp.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { class SlotImp : public Slot { @@ -21,13 +21,13 @@ public: // inbound SlotImp( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint remoteEndpoint, + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock); // outbound - SlotImp(beast::IP::Endpoint remoteEndpoint, bool fixed, clock_type& clock); + SlotImp(beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock); bool inbound() const override @@ -53,13 +53,13 @@ public: return state_; } - beast::IP::Endpoint const& + beast::ip::Endpoint const& remoteEndpoint() const override { return remoteEndpoint_; } - std::optional const& + std::optional const& localEndpoint() const override { return localEndpoint_; @@ -93,13 +93,13 @@ public: } void - localEndpoint(beast::IP::Endpoint const& endpoint) + localEndpoint(beast::ip::Endpoint const& endpoint) { localEndpoint_ = endpoint; } void - remoteEndpoint(beast::IP::Endpoint const& endpoint) + remoteEndpoint(beast::ip::Endpoint const& endpoint) { remoteEndpoint_ = endpoint; } @@ -140,20 +140,20 @@ public: * sending a slot the same address too frequently. */ void - insert(beast::IP::Endpoint const& ep, std::uint32_t hops); + insert(beast::ip::Endpoint const& ep, std::uint32_t hops); /** * Returns `true` if we should not send endpoint to the slot. */ bool - filter(beast::IP::Endpoint const& ep, std::uint32_t hops); + filter(beast::ip::Endpoint const& ep, std::uint32_t hops); private: void expire(); friend class SlotImp; - beast::aged_unordered_map cache_; + beast::aged_unordered_map cache_; } recent; void @@ -167,8 +167,8 @@ private: bool const fixed_; bool reserved_; State state_; - beast::IP::Endpoint remoteEndpoint_; - std::optional localEndpoint_; + beast::ip::Endpoint remoteEndpoint_; + std::optional localEndpoint_; std::optional publicKey_; static std::int32_t constexpr kUnknownPort = -1; @@ -196,4 +196,4 @@ public: clock_type::time_point whenAcceptEndpoints; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Source.h b/include/xrpl/peerfinder/detail/Source.h index 5cdb535bdd..09aa4e216a 100644 --- a/include/xrpl/peerfinder/detail/Source.h +++ b/include/xrpl/peerfinder/detail/Source.h @@ -7,7 +7,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * A static or dynamic source of peer addresses. @@ -46,4 +46,4 @@ public: fetch(Results& results, beast::Journal journal) = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/SourceStrings.h b/include/xrpl/peerfinder/detail/SourceStrings.h index 325a024764..e9783c775f 100644 --- a/include/xrpl/peerfinder/detail/SourceStrings.h +++ b/include/xrpl/peerfinder/detail/SourceStrings.h @@ -6,7 +6,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Provides addresses from a static set of strings. @@ -22,4 +22,4 @@ public: make(std::string const& name, Strings const& strings); }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Store.h b/include/xrpl/peerfinder/detail/Store.h index 9393ef6c2b..1f9352c6ec 100644 --- a/include/xrpl/peerfinder/detail/Store.h +++ b/include/xrpl/peerfinder/detail/Store.h @@ -6,7 +6,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Abstract persistence for PeerFinder data. @@ -17,7 +17,7 @@ public: virtual ~Store() = default; // load the bootstrap cache - using load_callback = std::function; + using load_callback = std::function; virtual std::size_t load(load_callback const& cb) = 0; @@ -26,11 +26,11 @@ public: { explicit Entry() = default; - beast::IP::Endpoint endpoint; + beast::ip::Endpoint endpoint; int valence{}; }; virtual void save(std::vector const& v) = 0; }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/peerfinder/detail/Tuning.h b/include/xrpl/peerfinder/detail/Tuning.h index ea4637dd9d..b4ccfae167 100644 --- a/include/xrpl/peerfinder/detail/Tuning.h +++ b/include/xrpl/peerfinder/detail/Tuning.h @@ -9,7 +9,7 @@ * Heuristically tuned constants. */ /** @{ */ -namespace xrpl::PeerFinder::Tuning { +namespace xrpl::peer_finder::tuning { //--------------------------------------------------------- // @@ -111,5 +111,5 @@ constexpr std::chrono::seconds kLiveCacheSecondsToLive(30); // Note that we ignore the port for purposes of comparison. constexpr std::chrono::seconds kRecentAttemptDuration(60); -} // namespace xrpl::PeerFinder::Tuning +} // namespace xrpl::peer_finder::tuning /** @} */ diff --git a/include/xrpl/peerfinder/make_Manager.h b/include/xrpl/peerfinder/make_Manager.h index 5da372e588..e514734d8b 100644 --- a/include/xrpl/peerfinder/make_Manager.h +++ b/include/xrpl/peerfinder/make_Manager.h @@ -10,7 +10,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * @brief Create a new Manager. @@ -33,4 +33,4 @@ makeManager( Store& store, beast::insight::Collector::ptr const& collector); -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/include/xrpl/protocol/ApiVersion.h b/include/xrpl/protocol/ApiVersion.h index c3292e6074..b52e705b38 100644 --- a/include/xrpl/protocol/ApiVersion.h +++ b/include/xrpl/protocol/ApiVersion.h @@ -33,7 +33,7 @@ namespace xrpl { * Command line Requests use apiCommandLineVersion. */ -namespace RPC { +namespace rpc { template static constexpr std::integral_constant kApiVersion = {}; @@ -60,7 +60,7 @@ static_assert(kApiMaximumValidVersion >= kApiMaximumSupportedVersion); inline void setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) { - XRPL_ASSERT(apiVersion != kApiInvalidVersion, "xrpl::RPC::setVersion : input is valid"); + XRPL_ASSERT(apiVersion != kApiInvalidVersion, "xrpl::rpc::setVersion : input is valid"); auto& retObj = parent[jss::version] = json::ValueType::Object; @@ -99,12 +99,12 @@ setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled) inline unsigned int getAPIVersionNumber(json::Value const& jv, bool betaEnabled) { - static json::Value const kMinVersion(RPC::kApiMinimumSupportedVersion); + static json::Value const kMinVersion(rpc::kApiMinimumSupportedVersion); json::Value const maxVersion( - betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion); + betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion); if (!jv.isObject() || !jv.isMember(jss::api_version)) - return RPC::kApiVersionIfUnspecified; + return rpc::kApiVersionIfUnspecified; try { @@ -113,33 +113,33 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled) { case json::ValueType::Int: if (rawVersion.asInt() < 0) - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; [[fallthrough]]; case json::ValueType::UInt: { auto const apiVersion = rawVersion.asUInt(); if (apiVersion < kMinVersion || apiVersion > maxVersion) - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; return apiVersion; } default: - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; } } catch (...) { - return RPC::kApiInvalidVersion; + return rpc::kApiInvalidVersion; } } -} // namespace RPC +} // namespace rpc template void forApiVersions(Fn const& fn, Args&&... args) requires // (MaxVer >= MinVer) && // - (MinVer >= RPC::kApiMinimumSupportedVersion) && // - (RPC::kApiMaximumValidVersion >= MaxVer) && requires { + (MinVer >= rpc::kApiMinimumSupportedVersion) && // + (rpc::kApiMaximumValidVersion >= MaxVer) && requires { fn(std::integral_constant{}, std::forward(args)...); fn(std::integral_constant{}, std::forward(args)...); } @@ -158,11 +158,11 @@ template void forAllApiVersions(Fn const& fn, Args&&... args) requires requires { - forApiVersions( + forApiVersions( fn, std::forward(args)...); } { - forApiVersions( + forApiVersions( fn, std::forward(args)...); } diff --git a/include/xrpl/protocol/BuildInfo.h b/include/xrpl/protocol/BuildInfo.h index 18ba20f23c..c3f90d8f9f 100644 --- a/include/xrpl/protocol/BuildInfo.h +++ b/include/xrpl/protocol/BuildInfo.h @@ -8,7 +8,7 @@ * Versioning information for this build. */ // VFALCO The namespace is deprecated -namespace xrpl::BuildInfo { +namespace xrpl::build_info { /** * Server version. @@ -84,4 +84,4 @@ isXrpldVersion(std::uint64_t version); bool isNewerVersion(std::uint64_t version); -} // namespace xrpl::BuildInfo +} // namespace xrpl::build_info diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index 8ac7c8c58f..465b6d711f 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -167,7 +167,7 @@ enum WarningCodeI { // VFALCO NOTE these should probably not be in the RPC namespace. -namespace RPC { +namespace rpc { /** * Maps an rpc error code to its token, default message, and HTTP status. @@ -337,7 +337,7 @@ containsError(json::Value const& json); int errorCodeHttpStatus(ErrorCodeI code); -} // namespace RPC +} // namespace rpc /** * Returns a single string with the contents of an RPC error. diff --git a/include/xrpl/protocol/MultiApiJson.h b/include/xrpl/protocol/MultiApiJson.h index 9a4882ec55..a0029fa491 100644 --- a/include/xrpl/protocol/MultiApiJson.h +++ b/include/xrpl/protocol/MultiApiJson.h @@ -188,6 +188,6 @@ struct MultiApiJson // Wrapper for Json for all supported API versions. using MultiApiJson = - detail::MultiApiJson; + detail::MultiApiJson; } // namespace xrpl diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h index bef05b9a8f..df4fedb707 100644 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h @@ -6,7 +6,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Adds common synthetic fields to transaction-related JSON responses @@ -16,4 +16,4 @@ void insertNFTSyntheticInJson(json::Value&, std::shared_ptr const&, TxMeta const&); /** @} */ -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 9938a9b768..567f66d339 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -139,7 +139,7 @@ tenthBipsOfValue(T value, TenthBips bips) return value * bips.value() / kTenthBipsPerUnity.value(); } -namespace Lending { +namespace lending { /** * The maximum management fee rate allowed by a loan broker in 1/10 bips. * @@ -236,7 +236,7 @@ static constexpr int kLoanPaymentsPerFeeIncrement = 5; * without an amendment */ static constexpr int kLoanMaximumPaymentsPerTransaction = 100; -} // namespace Lending +} // namespace lending /** * The maximum length of a URI inside an NFT diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 98301af487..833078d741 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -260,7 +260,7 @@ calcAccountID(PublicKey const& pk); inline std::string getFingerprint( - beast::IP::Endpoint const& address, + beast::ip::Endpoint const& address, std::optional const& publicKey = std::nullopt, std::optional const& id = std::nullopt) { diff --git a/include/xrpl/protocol/XChainAttestations.h b/include/xrpl/protocol/XChainAttestations.h index 8f1c7a4ce3..ed8ffeb88e 100644 --- a/include/xrpl/protocol/XChainAttestations.h +++ b/include/xrpl/protocol/XChainAttestations.h @@ -20,7 +20,7 @@ namespace xrpl { -namespace Attestations { +namespace attestations { struct AttestationBase { @@ -227,7 +227,7 @@ struct CmpByCreateCount } }; -}; // namespace Attestations +}; // namespace attestations // Result when checking when two attestation match. enum class AttestationMatch { @@ -241,7 +241,7 @@ enum class AttestationMatch { struct XChainClaimAttestation { - using TSignedAttestation = Attestations::AttestationClaim; + using TSignedAttestation = attestations::AttestationClaim; static SField const& arrayFieldName; AccountID keyAccount; @@ -297,7 +297,7 @@ struct XChainClaimAttestation struct XChainCreateAccountAttestation { - using TSignedAttestation = Attestations::AttestationCreateAccount; + using TSignedAttestation = attestations::AttestationCreateAccount; static SField const& arrayFieldName; AccountID keyAccount; diff --git a/include/xrpl/resource/Charge.h b/include/xrpl/resource/Charge.h index 12ea548fd2..b5bb8dd52e 100644 --- a/include/xrpl/resource/Charge.h +++ b/include/xrpl/resource/Charge.h @@ -4,7 +4,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * A consumption charge. @@ -32,7 +32,7 @@ public: label() const; /** - * Return the cost of the charge in Resource::Manager units. + * Return the cost of the charge in resource::Manager units. */ [[nodiscard]] value_type cost() const; @@ -60,4 +60,4 @@ private: std::ostream& operator<<(std::ostream& os, Charge const& v); -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Consumer.h b/include/xrpl/resource/Consumer.h index 9abcbffc82..01539e3a39 100644 --- a/include/xrpl/resource/Consumer.h +++ b/include/xrpl/resource/Consumer.h @@ -8,7 +8,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { struct Entry; class Logic; @@ -96,4 +96,4 @@ private: std::ostream& operator<<(std::ostream& os, Consumer const& v); -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Disposition.h b/include/xrpl/resource/Disposition.h index cd5bceafa5..28dd4edf62 100644 --- a/include/xrpl/resource/Disposition.h +++ b/include/xrpl/resource/Disposition.h @@ -1,6 +1,6 @@ #pragma once -namespace xrpl::Resource { +namespace xrpl::resource { /** * The disposition of a consumer after applying a load charge. @@ -24,4 +24,4 @@ enum class Disposition { Drop }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Fees.h b/include/xrpl/resource/Fees.h index 5001b504d6..411169253d 100644 --- a/include/xrpl/resource/Fees.h +++ b/include/xrpl/resource/Fees.h @@ -2,7 +2,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Schedule of fees charged for imposing load on the server. @@ -31,4 +31,4 @@ extern Charge const kFeeWarning; // The cost of receiving a warning. extern Charge const kFeeDrop; // The cost of being dropped for excess load. /** @} */ -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/Gossip.h b/include/xrpl/resource/Gossip.h index 4ad5852de0..0d8ccb100c 100644 --- a/include/xrpl/resource/Gossip.h +++ b/include/xrpl/resource/Gossip.h @@ -4,7 +4,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Data format for exchanging consumption information across peers. @@ -21,10 +21,10 @@ struct Gossip explicit Item() = default; int balance{}; - beast::IP::Endpoint address; + beast::ip::Endpoint address; }; std::vector items; }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/README.md b/include/xrpl/resource/README.md index 545d4e9ca0..96b6c3d603 100644 --- a/include/xrpl/resource/README.md +++ b/include/xrpl/resource/README.md @@ -1,4 +1,4 @@ -# Resource::Manager +# resource::Manager The ResourceManager module has these responsibilities: @@ -36,7 +36,7 @@ to the general public. ## Consumer Types Consumers are placed into three classifications (as identified by the -Resource::Kind enumeration): +resource::Kind enumeration): - InBound, - OutBound, and @@ -72,6 +72,6 @@ drop connections to those IP addresses that occur commonly in the gossip. ## Access -In xrpld, the Application holds a unique instance of Resource::Manager, +In xrpld, the Application holds a unique instance of resource::Manager, which may be retrieved by calling the method `Application::getResourceManager()`. diff --git a/include/xrpl/resource/ResourceManager.h b/include/xrpl/resource/ResourceManager.h index 03aab60c75..267cfb16e3 100644 --- a/include/xrpl/resource/ResourceManager.h +++ b/include/xrpl/resource/ResourceManager.h @@ -14,7 +14,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Tracks load and resource consumption. @@ -32,10 +32,10 @@ public: * IP if proxied. */ virtual Consumer - newInboundEndpoint(beast::IP::Endpoint const& address) = 0; + newInboundEndpoint(beast::ip::Endpoint const& address) = 0; virtual Consumer newInboundEndpoint( - beast::IP::Endpoint const& address, + beast::ip::Endpoint const& address, bool const proxy, std::string_view forwardedFor) = 0; @@ -43,13 +43,13 @@ public: * Create a new endpoint keyed by outbound IP address and port. */ virtual Consumer - newOutboundEndpoint(beast::IP::Endpoint const& address) = 0; + newOutboundEndpoint(beast::ip::Endpoint const& address) = 0; /** * Create a new unlimited endpoint keyed by forwarded IP. */ virtual Consumer - newUnlimitedEndpoint(beast::IP::Endpoint const& address) = 0; + newUnlimitedEndpoint(beast::ip::Endpoint const& address) = 0; /** * Extract packaged consumer information for export. @@ -78,4 +78,4 @@ public: std::unique_ptr makeManager(beast::insight::Collector::ptr const& collector, beast::Journal journal); -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Entry.h b/include/xrpl/resource/detail/Entry.h index 1336bda6ab..ec5a328b8b 100644 --- a/include/xrpl/resource/detail/Entry.h +++ b/include/xrpl/resource/detail/Entry.h @@ -12,7 +12,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { using clock_type = beast::AbstractClock; @@ -91,4 +91,4 @@ operator<<(std::ostream& os, Entry const& v) return os; } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Import.h b/include/xrpl/resource/detail/Import.h index b19dbc4d1a..c5366146c1 100644 --- a/include/xrpl/resource/detail/Import.h +++ b/include/xrpl/resource/detail/Import.h @@ -5,7 +5,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * A set of imported consumer data from a gossip origin. @@ -32,4 +32,4 @@ struct Import std::vector items; }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Key.h b/include/xrpl/resource/detail/Key.h index a0f11422a7..180e868319 100644 --- a/include/xrpl/resource/detail/Key.h +++ b/include/xrpl/resource/detail/Key.h @@ -7,17 +7,17 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { // The consumer key struct Key { Kind kind; - beast::IP::Endpoint address; + beast::ip::Endpoint address; Key() = delete; - Key(Kind k, beast::IP::Endpoint addr) : kind(k), address(std::move(addr)) + Key(Kind k, beast::ip::Endpoint addr) : kind(k), address(std::move(addr)) { } @@ -47,4 +47,4 @@ struct Key }; }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Kind.h b/include/xrpl/resource/detail/Kind.h index ce2e0773cf..9af760d252 100644 --- a/include/xrpl/resource/detail/Kind.h +++ b/include/xrpl/resource/detail/Kind.h @@ -1,6 +1,6 @@ #pragma once -namespace xrpl::Resource { +namespace xrpl::resource { /** * Kind of consumer. @@ -12,4 +12,4 @@ namespace xrpl::Resource { */ enum class Kind { Inbound, Outbound, Unlimited }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Logic.h b/include/xrpl/resource/detail/Logic.h index 3f36ad84a3..aaaeb4fdd1 100644 --- a/include/xrpl/resource/detail/Logic.h +++ b/include/xrpl/resource/detail/Logic.h @@ -24,7 +24,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { class Logic { @@ -96,7 +96,7 @@ public: } Consumer - newInboundEndpoint(beast::IP::Endpoint const& address) + newInboundEndpoint(beast::ip::Endpoint const& address) { Entry* entry(nullptr); @@ -126,7 +126,7 @@ public: } Consumer - newOutboundEndpoint(beast::IP::Endpoint const& address) + newOutboundEndpoint(beast::ip::Endpoint const& address) { Entry* entry(nullptr); @@ -159,7 +159,7 @@ public: * enabled. */ Consumer - newUnlimitedEndpoint(beast::IP::Endpoint const& address) + newUnlimitedEndpoint(beast::ip::Endpoint const& address) { Entry* entry(nullptr); @@ -387,7 +387,7 @@ public: { std::scoped_lock const _(lock_); Entry& entry(iter->second); - XRPL_ASSERT(entry.refcount == 0, "xrpl::Resource::Logic::erase : entry not used"); + XRPL_ASSERT(entry.refcount == 0, "xrpl::resource::Logic::erase : entry not used"); inactive_.erase(inactive_.iteratorTo(entry)); table_.erase(iter); } @@ -421,7 +421,7 @@ public: default: // LCOV_EXCL_START UNREACHABLE( - "xrpl::Resource::Logic::release : invalid entry " + "xrpl::resource::Logic::release : invalid entry " "kind"); break; // LCOV_EXCL_STOP @@ -440,7 +440,7 @@ public: static_assert( kFeeLogAsWarn > kFeeLogAsInfo && kFeeLogAsInfo > kFeeLogAsDebug && kFeeLogAsDebug > 10); - static auto kGetStream = [](Resource::Charge::value_type cost, beast::Journal& journal) { + static auto kGetStream = [](resource::Charge::value_type cost, beast::Journal& journal) { if (cost >= kFeeLogAsWarn) return journal.warn(); if (cost >= kFeeLogAsInfo) @@ -564,4 +564,4 @@ public: } }; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/resource/detail/Tuning.h b/include/xrpl/resource/detail/Tuning.h index 62f7fa3f9d..d631aaddba 100644 --- a/include/xrpl/resource/detail/Tuning.h +++ b/include/xrpl/resource/detail/Tuning.h @@ -2,7 +2,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { /** * Tunable constants. @@ -26,4 +26,4 @@ static constexpr std::chrono::seconds kSecondsUntilExpiration{300}; // Number of seconds until imported gossip expires static constexpr std::chrono::seconds kGossipExpirationSeconds{30}; -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/include/xrpl/server/InfoSub.h b/include/xrpl/server/InfoSub.h index 2e9bd857c7..4bf88cd53b 100644 --- a/include/xrpl/server/InfoSub.h +++ b/include/xrpl/server/InfoSub.h @@ -62,7 +62,7 @@ public: using ref = std::shared_ptr const&; - using Consumer = Resource::Consumer; + using Consumer = resource::Consumer; public: /** diff --git a/include/xrpl/server/Session.h b/include/xrpl/server/Session.h index be8d9a497c..03ac767c25 100644 --- a/include/xrpl/server/Session.h +++ b/include/xrpl/server/Session.h @@ -52,7 +52,7 @@ public: /** * Returns the remote address of the connection. */ - virtual beast::IP::Endpoint + virtual beast::ip::Endpoint remoteAddress() = 0; /** diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h b/include/xrpl/server/detail/BaseHTTPPeer.h index c7553c1da3..6020d3cc65 100644 --- a/include/xrpl/server/detail/BaseHTTPPeer.h +++ b/include/xrpl/server/detail/BaseHTTPPeer.h @@ -157,7 +157,7 @@ protected: return port_; } - beast::IP::Endpoint + beast::ip::Endpoint remoteAddress() override { return beast::IPAddressConversion::fromAsio(remoteAddress_); diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h index 59a866ab8c..b1670865bd 100644 --- a/include/xrpl/server/detail/BaseWSPeer.h +++ b/include/xrpl/server/detail/BaseWSPeer.h @@ -196,7 +196,7 @@ BaseWSPeer::run() startTimer(); closeOnTimer_ = true; impl().ws_.set_option(boost::beast::websocket::stream_base::decorator([](auto& res) { - res.set(boost::beast::http::field::server, BuildInfo::getFullVersionString()); + res.set(boost::beast::http::field::server, build_info::getFullVersionString()); })); impl().ws_.async_accept( request_, bind_executor(strand_, [self = impl().shared_from_this()](error_code const& ec) { diff --git a/src/libxrpl/basics/ResolverAsio.cpp b/src/libxrpl/basics/ResolverAsio.cpp index 25e95b7fc5..53739fed8a 100644 --- a/src/libxrpl/basics/ResolverAsio.cpp +++ b/src/libxrpl/basics/ResolverAsio.cpp @@ -255,7 +255,7 @@ public: if (ec == boost::asio::error::operation_aborted) return; - std::vector addresses; + std::vector addresses; auto iter = results.begin(); // If we get an error message back, we don't return any @@ -283,7 +283,7 @@ public: // first attempt to parse as an endpoint (IP addr + port). // If that doesn't succeed, fall back to generic name + port parsing - if (auto const result = beast::IP::Endpoint::fromStringChecked(str)) + if (auto const result = beast::ip::Endpoint::fromStringChecked(str)) { return make_pair(result->address().to_string(), std::to_string(result->port())); } diff --git a/src/libxrpl/basics/StringUtilities.cpp b/src/libxrpl/basics/StringUtilities.cpp index f4edaf5aca..2b7deecb8e 100644 --- a/src/libxrpl/basics/StringUtilities.cpp +++ b/src/libxrpl/basics/StringUtilities.cpp @@ -74,7 +74,7 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) // We need to use Endpoint to parse the domain to // strip surrounding brackets from IPv6 addresses, // e.g. [::1] => ::1. - auto const result = beast::IP::Endpoint::fromStringChecked(domain); + auto const result = beast::ip::Endpoint::fromStringChecked(domain); pUrl.domain = result ? result->address().to_string() : domain; std::string const port = smMatch[5]; if (!port.empty()) diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp b/src/libxrpl/beast/insight/StatsDCollector.cpp index 3cff5d93b5..72fe6189a5 100644 --- a/src/libxrpl/beast/insight/StatsDCollector.cpp +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp @@ -207,7 +207,7 @@ private: static constexpr auto kMaxPacketSize = 1472; Journal journal_; - IP::Endpoint address_; + ip::Endpoint address_; std::string prefix_; boost::asio::io_context ioContext_; std::optional> work_; @@ -222,13 +222,13 @@ private: std::thread thread_; static boost::asio::ip::udp::endpoint - toEndpoint(IP::Endpoint const& ep) + toEndpoint(ip::Endpoint const& ep) { return boost::asio::ip::udp::endpoint(ep.address(), ep.port()); } public: - StatsDCollectorImp(IP::Endpoint address, std::string prefix, Journal journal) + StatsDCollectorImp(ip::Endpoint address, std::string prefix, Journal journal) : journal_(journal) , address_(std::move(address)) , prefix_(std::move(prefix)) @@ -707,7 +707,7 @@ StatsDMeterImpl::doProcess() //------------------------------------------------------------------------------ std::shared_ptr -StatsDCollector::make(IP::Endpoint const& address, std::string const& prefix, Journal journal) +StatsDCollector::make(ip::Endpoint const& address, std::string const& prefix, Journal journal) { return std::make_shared(address, prefix, journal); } diff --git a/src/libxrpl/beast/net/IPAddressConversion.cpp b/src/libxrpl/beast/net/IPAddressConversion.cpp index c0a37d234e..bf24ef75c1 100644 --- a/src/libxrpl/beast/net/IPAddressConversion.cpp +++ b/src/libxrpl/beast/net/IPAddressConversion.cpp @@ -5,7 +5,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { Endpoint fromAsio(boost::asio::ip::address const& address) @@ -31,4 +31,4 @@ toAsioEndpoint(Endpoint const& endpoint) return boost::asio::ip::tcp::endpoint{endpoint.address(), endpoint.port()}; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/beast/net/IPAddressV4.cpp b/src/libxrpl/beast/net/IPAddressV4.cpp index f9b0c96022..2a59fe1cc4 100644 --- a/src/libxrpl/beast/net/IPAddressV4.cpp +++ b/src/libxrpl/beast/net/IPAddressV4.cpp @@ -1,6 +1,6 @@ #include -namespace beast::IP { +namespace beast::ip { bool isPrivate(AddressV4 const& addr) @@ -62,4 +62,4 @@ getClass(AddressV4 const& addr) return kTable[(addr.to_uint() & 0xE0000000) >> 29]; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/beast/net/IPAddressV6.cpp b/src/libxrpl/beast/net/IPAddressV6.cpp index c75ccaf1cc..e5ef55065f 100644 --- a/src/libxrpl/beast/net/IPAddressV6.cpp +++ b/src/libxrpl/beast/net/IPAddressV6.cpp @@ -4,7 +4,7 @@ #include -namespace beast::IP { +namespace beast::ip { bool isPrivate(AddressV6 const& addr) @@ -58,4 +58,4 @@ isPublic(AddressV6 const& addr) return true; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/beast/net/IPEndpoint.cpp b/src/libxrpl/beast/net/IPEndpoint.cpp index 5877151187..02ed5e37c5 100644 --- a/src/libxrpl/beast/net/IPEndpoint.cpp +++ b/src/libxrpl/beast/net/IPEndpoint.cpp @@ -14,7 +14,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { Endpoint::Endpoint() : port_(0) { @@ -176,4 +176,4 @@ operator>>(std::istream& is, Endpoint& endpoint) return is; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp index dac2c67181..89b03a03a7 100644 --- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp +++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp @@ -131,7 +131,7 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale) roundToAsset(asset, value, scale, Number::RoundingMode::Upward); } -namespace Accrual { +namespace accrual { AccountingDeltas loanOriginationDeltas(Number const& principalRequested, Number const& interestDue) @@ -169,9 +169,9 @@ loanPaymentDeltas(LoanPaymentParts const& parts) .debtTotalDelta = (parts.principalPaid + parts.interestPaid) - parts.valueChange}; } -} // namespace Accrual +} // namespace accrual -namespace CashBasis { +namespace cash_basis { AccountingDeltas loanOriginationDeltas(Number const& principalRequested) @@ -196,7 +196,7 @@ loanPaymentDeltas(LoanPaymentParts const& parts) return {.assetsTotalDelta = parts.interestPaid, .debtTotalDelta = parts.principalPaid}; } -} // namespace CashBasis +} // namespace cash_basis namespace { @@ -219,8 +219,8 @@ loanOriginationDeltas( Number const& interestDue) { return cashBasisEnabled(vaultSle) - ? CashBasis::loanOriginationDeltas(principalRequested) - : Accrual::loanOriginationDeltas(principalRequested, interestDue); + ? cash_basis::loanOriginationDeltas(principalRequested) + : accrual::loanOriginationDeltas(principalRequested, interestDue); } bool @@ -235,21 +235,21 @@ loanOriginationExceedsVaultMaximum( return false; auto const vaultMaximum = vaultSle->at(sfAssetsMaximum); - return Accrual::loanOriginationExceedsVaultMaximum(vaultMaximum, vaultTotal, interestDue); + return accrual::loanOriginationExceedsVaultMaximum(vaultMaximum, vaultTotal, interestDue); } Number loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle) { - return cashBasisEnabled(vaultSle) ? CashBasis::loanVaultExposure(loanSle) - : Accrual::loanVaultExposure(loanSle); + return cashBasisEnabled(vaultSle) ? cash_basis::loanVaultExposure(loanSle) + : accrual::loanVaultExposure(loanSle); } AccountingDeltas loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const& parts) { - return cashBasisEnabled(vaultSle) ? CashBasis::loanPaymentDeltas(parts) - : Accrual::loanPaymentDeltas(parts); + return cashBasisEnabled(vaultSle) ? cash_basis::loanPaymentDeltas(parts) + : accrual::loanPaymentDeltas(parts); } namespace detail { @@ -1617,7 +1617,7 @@ makeRegularPayment( LoanPaymentType const paymentType, beast::Journal j) { - using namespace Lending; + using namespace lending; XRPL_ASSERT_PARTS( paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment, diff --git a/src/libxrpl/peerfinder/Bootcache.cpp b/src/libxrpl/peerfinder/Bootcache.cpp index a2a56b4d01..4f9b5fc816 100644 --- a/src/libxrpl/peerfinder/Bootcache.cpp +++ b/src/libxrpl/peerfinder/Bootcache.cpp @@ -16,7 +16,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { Bootcache::Bootcache(Store& store, clock_type& clock, beast::Journal journal) : store_(store), clock_(clock), journal_(journal), whenUpdate_(clock_.now()) @@ -78,7 +78,7 @@ void Bootcache::load() { clear(); - auto const n(store_.load([this](beast::IP::Endpoint const& endpoint, int valence) { + auto const n(store_.load([this](beast::ip::Endpoint const& endpoint, int valence) { auto const result(this->map_.insert(value_type(endpoint, valence))); if (!result.second) { @@ -96,7 +96,7 @@ Bootcache::load() } bool -Bootcache::insert(beast::IP::Endpoint const& endpoint) +Bootcache::insert(beast::ip::Endpoint const& endpoint) { auto const result(map_.insert(value_type(endpoint, 0))); if (result.second) @@ -109,7 +109,7 @@ Bootcache::insert(beast::IP::Endpoint const& endpoint) } bool -Bootcache::insertStatic(beast::IP::Endpoint const& endpoint) +Bootcache::insertStatic(beast::ip::Endpoint const& endpoint) { auto result(map_.insert(value_type(endpoint, kStaticValence))); @@ -130,7 +130,7 @@ Bootcache::insertStatic(beast::IP::Endpoint const& endpoint) } void -Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) +Bootcache::onSuccess(beast::ip::Endpoint const& endpoint) { auto result(map_.insert(value_type(endpoint, 1))); if (result.second) @@ -144,7 +144,7 @@ Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) ++entry.valence(); map_.erase(result.first); result = map_.insert(value_type(endpoint, entry)); - XRPL_ASSERT(result.second, "xrpl::PeerFinder::Bootcache::onSuccess : endpoint inserted"); + XRPL_ASSERT(result.second, "xrpl::peer_finder::Bootcache::onSuccess : endpoint inserted"); } Entry const& entry(result.first->right); JLOG(journal_.info()) << std::left << std::setw(18) << "Bootcache connect " << endpoint @@ -154,7 +154,7 @@ Bootcache::onSuccess(beast::IP::Endpoint const& endpoint) } void -Bootcache::onFailure(beast::IP::Endpoint const& endpoint) +Bootcache::onFailure(beast::ip::Endpoint const& endpoint) { auto result(map_.insert(value_type(endpoint, -1))); if (result.second) @@ -168,7 +168,7 @@ Bootcache::onFailure(beast::IP::Endpoint const& endpoint) --entry.valence(); map_.erase(result.first); result = map_.insert(value_type(endpoint, entry)); - XRPL_ASSERT(result.second, "xrpl::PeerFinder::Bootcache::onFailure : endpoint inserted"); + XRPL_ASSERT(result.second, "xrpl::peer_finder::Bootcache::onFailure : endpoint inserted"); } Entry const& entry(result.first->right); auto const n(std::abs(entry.valence())); @@ -201,11 +201,11 @@ Bootcache::onWrite(beast::PropertyStream::Map& map) void Bootcache::prune() { - if (size() <= Tuning::kBootcacheSize) + if (size() <= tuning::kBootcacheSize) return; // Calculate the amount to remove - auto count((size() * Tuning::kBootcachePrunePercent) / 100); + auto count((size() * tuning::kBootcachePrunePercent) / 100); decltype(count) pruned(0); // Work backwards because bimap doesn't handle @@ -215,7 +215,7 @@ Bootcache::prune() { --count; --iter; - beast::IP::Endpoint const& endpoint(iter->get_left()); + beast::ip::Endpoint const& endpoint(iter->get_left()); Entry const& entry(iter->get_right()); JLOG(journal_.trace()) << std::left << std::setw(18) << "Bootcache pruned" << endpoint << " at valence " << entry.valence(); @@ -244,7 +244,7 @@ Bootcache::update() store_.save(list); // Reset the flag and cooldown timer needsUpdate_ = false; - whenUpdate_ = clock_.now() + Tuning::kBootcacheCooldownTime; + whenUpdate_ = clock_.now() + tuning::kBootcacheCooldownTime; } // Checks the clock and calls update if we are off the cooldown. @@ -263,4 +263,4 @@ Bootcache::flagForUpdate() checkUpdate(); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/Config.cpp b/src/libxrpl/peerfinder/Config.cpp index 3e2f74f42b..60ac0ca547 100644 --- a/src/libxrpl/peerfinder/Config.cpp +++ b/src/libxrpl/peerfinder/Config.cpp @@ -7,13 +7,13 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { std::size_t Config::calcOutPeers() const { return std::max( - ((maxPeers * Tuning::kOutPercent) + 50) / 100, std::size_t(Tuning::kMinOutCount)); + ((maxPeers * tuning::kOutPercent) + 50) / 100, std::size_t(tuning::kMinOutCount)); } void @@ -26,8 +26,8 @@ Config::applyTuning() // IP addresses. ipLimit = 2; - if (inPeers > Tuning::kDefaultMaxPeers) - ipLimit += std::min(5, static_cast(inPeers / Tuning::kDefaultMaxPeers)); + if (inPeers > tuning::kDefaultMaxPeers) + ipLimit += std::min(5, static_cast(inPeers / tuning::kDefaultMaxPeers)); } // We don't allow a single IP to consume all incoming slots, @@ -58,7 +58,7 @@ Config::makeConfig( int ipLimit, bool verifyEndpoints) { - PeerFinder::Config config; + peer_finder::Config config; if (!limits.maxPeers) { @@ -85,7 +85,7 @@ Config::makeConfig( if (limits.maxPeers && *limits.maxPeers != 0) config.maxPeers = *limits.maxPeers; - config.maxPeers = std::max(config.maxPeers, Tuning::kMinOutCount); + config.maxPeers = std::max(config.maxPeers, tuning::kMinOutCount); config.outPeers = config.calcOutPeers(); // Calculate the number of outbound peers we want. If we dont want @@ -132,4 +132,4 @@ Config::makeConfig( return config; } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/Endpoint.cpp b/src/libxrpl/peerfinder/Endpoint.cpp index 12f2725ea5..6f3e2289f9 100644 --- a/src/libxrpl/peerfinder/Endpoint.cpp +++ b/src/libxrpl/peerfinder/Endpoint.cpp @@ -5,11 +5,11 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { -Endpoint::Endpoint(beast::IP::Endpoint ep, std::uint32_t hops) - : hops(std::min(hops, Tuning::kMaxHops + 1)), address(std::move(ep)) +Endpoint::Endpoint(beast::ip::Endpoint ep, std::uint32_t hops) + : hops(std::min(hops, tuning::kMaxHops + 1)), address(std::move(ep)) { } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/PeerfinderManager.cpp b/src/libxrpl/peerfinder/PeerfinderManager.cpp index 2219627f09..0cce2389ec 100644 --- a/src/libxrpl/peerfinder/PeerfinderManager.cpp +++ b/src/libxrpl/peerfinder/PeerfinderManager.cpp @@ -29,7 +29,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { class ManagerImp : public Manager { @@ -98,7 +98,7 @@ public: } void - addFixedPeer(std::string_view name, std::vector const& addresses) override + addFixedPeer(std::string_view name, std::vector const& addresses) override { logic_.addFixedPeer(name, addresses); } @@ -119,14 +119,14 @@ public: std::pair, Result> newInboundSlot( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint const& remoteEndpoint) override + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint const& remoteEndpoint) override { return logic_.newInboundSlot(localEndpoint, remoteEndpoint); } std::pair, Result> - newOutboundSlot(beast::IP::Endpoint const& remoteEndpoint) override + newOutboundSlot(beast::ip::Endpoint const& remoteEndpoint) override { return logic_.newOutboundSlot(remoteEndpoint); } @@ -163,7 +163,7 @@ public: //-------------------------------------------------------------------------- bool - onConnected(std::shared_ptr const& slot, beast::IP::Endpoint const& localEndpoint) + onConnected(std::shared_ptr const& slot, beast::ip::Endpoint const& localEndpoint) override { SlotImp::ptr const impl(std::dynamic_pointer_cast(slot)); @@ -184,7 +184,7 @@ public: return logic_.redirect(impl); } - std::vector + std::vector autoconnect() override { return logic_.autoconnect(); @@ -265,4 +265,4 @@ makeManager( return std::make_unique(ioContext, clock, journal, store, collector); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/SlotImp.cpp b/src/libxrpl/peerfinder/SlotImp.cpp index 0a4f32fd62..5209bd51ab 100644 --- a/src/libxrpl/peerfinder/SlotImp.cpp +++ b/src/libxrpl/peerfinder/SlotImp.cpp @@ -9,11 +9,11 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { SlotImp::SlotImp( - beast::IP::Endpoint const& localEndpoint, - beast::IP::Endpoint remoteEndpoint, + beast::ip::Endpoint const& localEndpoint, + beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock) : recent(clock) @@ -30,7 +30,7 @@ SlotImp::SlotImp( { } -SlotImp::SlotImp(beast::IP::Endpoint remoteEndpoint, bool fixed, clock_type& clock) +SlotImp::SlotImp(beast::ip::Endpoint remoteEndpoint, bool fixed, clock_type& clock) : recent(clock) , inbound_(false) , fixed_(fixed) @@ -49,29 +49,29 @@ SlotImp::state(State state) { // Must go through activate() to set active state XRPL_ASSERT( - state != State::Active, "xrpl::PeerFinder::SlotImp::state : input state is not active"); + state != State::Active, "xrpl::peer_finder::SlotImp::state : input state is not active"); // The state must be different XRPL_ASSERT( state_ != state, - "xrpl::PeerFinder::SlotImp::state : input state is different from " + "xrpl::peer_finder::SlotImp::state : input state is different from " "current"); // You can't transition into the initial states XRPL_ASSERT( state != State::Accept && state != State::Connect, - "xrpl::PeerFinder::SlotImp::state : input state is not an initial"); + "xrpl::peer_finder::SlotImp::state : input state is not an initial"); // Can only become connected from outbound connect state XRPL_ASSERT( state != State::Connected || (!inbound_ && state_ == State::Connect), - "xrpl::PeerFinder::SlotImp::state : input state is not connected an " + "xrpl::peer_finder::SlotImp::state : input state is not connected an " "invalid state"); // Can't gracefully close on an outbound connection attempt XRPL_ASSERT( state != State::Closing || state_ != State::Connect, - "xrpl::PeerFinder::SlotImp::state : input state is not closing an " + "xrpl::peer_finder::SlotImp::state : input state is not closing an " "invalid state"); state_ = state; @@ -83,7 +83,7 @@ SlotImp::activate(clock_type::time_point const& now) // Can only become active from the accept or connected state XRPL_ASSERT( state_ == State::Accept || state_ == State::Connected, - "xrpl::PeerFinder::SlotImp::activate : valid state"); + "xrpl::peer_finder::SlotImp::activate : valid state"); state_ = State::Active; whenAcceptEndpoints = now; @@ -100,7 +100,7 @@ SlotImp::RecentT::RecentT(clock_type& clock) : cache_(clock) } void -SlotImp::RecentT::insert(beast::IP::Endpoint const& ep, std::uint32_t hops) +SlotImp::RecentT::insert(beast::ip::Endpoint const& ep, std::uint32_t hops) { auto const result(cache_.emplace(ep, hops)); if (!result.second) @@ -115,7 +115,7 @@ SlotImp::RecentT::insert(beast::IP::Endpoint const& ep, std::uint32_t hops) } bool -SlotImp::RecentT::filter(beast::IP::Endpoint const& ep, std::uint32_t hops) +SlotImp::RecentT::filter(beast::ip::Endpoint const& ep, std::uint32_t hops) { auto const iter(cache_.find(ep)); if (iter == cache_.end()) @@ -129,7 +129,7 @@ SlotImp::RecentT::filter(beast::IP::Endpoint const& ep, std::uint32_t hops) void SlotImp::RecentT::expire() { - beast::expire(cache_, Tuning::kLiveCacheSecondsToLive); + beast::expire(cache_, tuning::kLiveCacheSecondsToLive); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/peerfinder/SourceStrings.cpp b/src/libxrpl/peerfinder/SourceStrings.cpp index f47e0cd51d..ca6ff07cba 100644 --- a/src/libxrpl/peerfinder/SourceStrings.cpp +++ b/src/libxrpl/peerfinder/SourceStrings.cpp @@ -8,7 +8,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { class SourceStringsImp : public SourceStrings { @@ -33,9 +33,9 @@ public: results.addresses.reserve(strings_.size()); for (auto const& str : strings_) { - beast::IP::Endpoint ep(beast::IP::Endpoint::fromString(str)); + beast::ip::Endpoint ep(beast::ip::Endpoint::fromString(str)); if (isUnspecified(ep)) - ep = beast::IP::Endpoint::fromString(str); + ep = beast::ip::Endpoint::fromString(str); if (!isUnspecified(ep)) results.addresses.push_back(ep); } @@ -54,4 +54,4 @@ SourceStrings::make(std::string const& name, Strings const& strings) return std::make_shared(name, strings); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 6ac352f3e1..8a18b3f228 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -13,7 +13,7 @@ #include #include -namespace xrpl::BuildInfo { +namespace xrpl::build_info { namespace { @@ -173,4 +173,4 @@ isNewerVersion(std::uint64_t version) return false; } -} // namespace xrpl::BuildInfo +} // namespace xrpl::build_info diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index 87761ce13e..e81f975844 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -9,7 +9,7 @@ #include namespace xrpl { -namespace RPC { +namespace rpc { namespace detail { @@ -215,12 +215,12 @@ errorCodeHttpStatus(ErrorCodeI code) return getErrorInfo(code).httpStatus; } -} // namespace RPC +} // namespace rpc std::string rpcErrorString(json::Value const& jv) { - XRPL_ASSERT(RPC::containsError(jv), "xrpl::RPC::rpcErrorString : input contains an error"); + XRPL_ASSERT(rpc::containsError(jv), "xrpl::rpc::rpcErrorString : input contains an error"); return jv[jss::error].asString() + jv[jss::error_message].asString(); } diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp index 4f0a2d5071..fd44ae1f33 100644 --- a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp +++ b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp @@ -9,7 +9,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { void insertNFTSyntheticInJson( @@ -21,4 +21,4 @@ insertNFTSyntheticInJson( insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/libxrpl/protocol/RPCErr.cpp b/src/libxrpl/protocol/RPCErr.cpp index ec9a3dee9d..c172a1898d 100644 --- a/src/libxrpl/protocol/RPCErr.cpp +++ b/src/libxrpl/protocol/RPCErr.cpp @@ -13,7 +13,7 @@ json::Value rpcError(ErrorCodeI iError) { json::Value jvResult(json::ValueType::Object); - RPC::injectError(iError, jvResult); + rpc::injectError(iError, jvResult); return jvResult; } diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 33ca5424d1..6ab272b3d1 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -48,7 +48,7 @@ namespace xrpl { -namespace STParsedJSONDetail { +namespace st_parsed_json_detail { template constexpr U toUnsigned(S value) @@ -93,7 +93,7 @@ makeName(std::string const& object, std::string const& field) static inline json::Value notAnObject(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' is not a JSON object."); } @@ -106,33 +106,33 @@ notAnObject(std::string const& object) static inline json::Value notAnArray(std::string const& object) { - return RPC::makeError(RpcInvalidParams, "Field '" + object + "' is not a JSON array."); + return rpc::makeError(RpcInvalidParams, "Field '" + object + "' is not a JSON array."); } static inline json::Value unknownField(std::string const& object, std::string const& field) { - return RPC::makeError(RpcInvalidParams, "Field '" + makeName(object, field) + "' is unknown."); + return rpc::makeError(RpcInvalidParams, "Field '" + makeName(object, field) + "' is unknown."); } static inline json::Value outOfRange(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' is out of range."); } static inline json::Value badType(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' has bad type."); } static inline json::Value invalidData(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' has invalid data."); } @@ -145,14 +145,14 @@ invalidData(std::string const& object) static inline json::Value arrayExpected(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' must be a JSON array."); } static inline json::Value arrayTooBig(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' exceeds allowed JSON array size of " + std::to_string(kMaxParsedJsonArraySize) + " elements per field."); @@ -161,20 +161,20 @@ arrayTooBig(std::string const& object, std::string const& field) static inline json::Value stringExpected(std::string const& object, std::string const& field) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + makeName(object, field) + "' must be a string."); } static inline json::Value tooDeep(std::string const& object) { - return RPC::makeError(RpcInvalidParams, "Field '" + object + "' exceeds nesting depth limit."); + return rpc::makeError(RpcInvalidParams, "Field '" + object + "' exceeds nesting depth limit."); } static inline json::Value singletonExpected(std::string const& object, unsigned int index) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Field '" + object + "[" + std::to_string(index) + "]' must be an object with a single key/object value."); @@ -183,7 +183,7 @@ singletonExpected(std::string const& object, unsigned int index) static inline json::Value templateMismatch(SField const& sField) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Object '" + sField.getName() + "' contents did not meet requirements for that type."); } @@ -191,7 +191,7 @@ templateMismatch(SField const& sField) static inline json::Value nonObjectInArray(std::string const& item, json::UInt index) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Item '" + item + "' at index " + std::to_string(index) + " is not an object. Arrays may only contain objects."); @@ -791,7 +791,7 @@ parseLeaf( if (pathEl.isMember(jss::currency) && pathEl.isMember(jss::mpt_issuance_id)) { - error = RPC::makeError(RpcInvalidParams, "Invalid Asset."); + error = rpc::makeError(RpcInvalidParams, "Invalid Asset."); return ret; } @@ -1195,13 +1195,13 @@ parseArray( } } -} // namespace STParsedJSONDetail +} // namespace st_parsed_json_detail //------------------------------------------------------------------------------ STParsedJSONObject::STParsedJSONObject(std::string const& name, json::Value const& json) { - using namespace STParsedJSONDetail; + using namespace st_parsed_json_detail; object = parseObject(name, json, sfGeneric, 0, error); } diff --git a/src/libxrpl/protocol/XChainAttestations.cpp b/src/libxrpl/protocol/XChainAttestations.cpp index 792fe5da9d..7c887e785b 100644 --- a/src/libxrpl/protocol/XChainAttestations.cpp +++ b/src/libxrpl/protocol/XChainAttestations.cpp @@ -24,7 +24,7 @@ #include namespace xrpl { -namespace Attestations { +namespace attestations { AttestationBase::AttestationBase( AccountID attestationSignerAccount, @@ -385,7 +385,7 @@ operator==(AttestationCreateAccount const& lhs, AttestationCreateAccount const& std::tie(rhs.createCount, rhs.toCreate, rhs.rewardAmount); } -} // namespace Attestations +} // namespace attestations SField const& XChainClaimAttestation::arrayFieldName{sfXChainClaimAttestations}; SField const& XChainCreateAccountAttestation::arrayFieldName{sfXChainCreateAccountAttestations}; diff --git a/src/libxrpl/resource/Charge.cpp b/src/libxrpl/resource/Charge.cpp index e174c13522..f80588b143 100644 --- a/src/libxrpl/resource/Charge.cpp +++ b/src/libxrpl/resource/Charge.cpp @@ -6,7 +6,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { Charge::Charge(value_type cost, std::string label) : cost_(cost), label_(std::move(label)) { @@ -57,4 +57,4 @@ Charge::operator*(value_type m) const return Charge(cost_ * m, label_); } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/resource/Consumer.cpp b/src/libxrpl/resource/Consumer.cpp index 58d5775a31..934aaddbf5 100644 --- a/src/libxrpl/resource/Consumer.cpp +++ b/src/libxrpl/resource/Consumer.cpp @@ -12,7 +12,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { Consumer::Consumer(Logic& logic, Entry& entry) : logic_(&logic), entry_(&entry) { @@ -99,14 +99,14 @@ Consumer::charge(Charge const& what, std::string const& context) bool Consumer::warn() { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::warn : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::warn : non-null entry"); return logic_->warn(*entry_); } bool Consumer::disconnect(beast::Journal const& j) { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::disconnect : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::disconnect : non-null entry"); bool const d = logic_->disconnect(*entry_); if (d) { @@ -118,14 +118,14 @@ Consumer::disconnect(beast::Journal const& j) int Consumer::balance() { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::balance : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::balance : non-null entry"); return logic_->balance(*entry_); } Entry& Consumer::entry() { - XRPL_ASSERT(entry_, "xrpl::Resource::Consumer::entry : non-null entry"); + XRPL_ASSERT(entry_, "xrpl::resource::Consumer::entry : non-null entry"); return *entry_; } @@ -142,4 +142,4 @@ operator<<(std::ostream& os, Consumer const& v) return os; } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/resource/Fees.cpp b/src/libxrpl/resource/Fees.cpp index bb825fa3c7..037f60051e 100644 --- a/src/libxrpl/resource/Fees.cpp +++ b/src/libxrpl/resource/Fees.cpp @@ -2,7 +2,7 @@ #include -namespace xrpl::Resource { +namespace xrpl::resource { Charge const kFeeMalformedRequest(200, "malformed request"); Charge const kFeeRequestNoReply(10, "unsatisfiable request"); @@ -23,6 +23,6 @@ Charge const kFeeHeavyBurdenPeer(2000, "heavy peer request"); Charge const kFeeWarning(4000, "received warning"); Charge const kFeeDrop(6000, "dropped"); -// See also Resource::Logic::charge for log level cutoff values +// See also resource::Logic::charge for log level cutoff values -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/resource/ResourceManager.cpp b/src/libxrpl/resource/ResourceManager.cpp index e3b4d9cc5c..cdfa95facd 100644 --- a/src/libxrpl/resource/ResourceManager.cpp +++ b/src/libxrpl/resource/ResourceManager.cpp @@ -23,7 +23,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { class ManagerImp : public Manager { @@ -58,14 +58,14 @@ public: } Consumer - newInboundEndpoint(beast::IP::Endpoint const& address) override + newInboundEndpoint(beast::ip::Endpoint const& address) override { return logic_.newInboundEndpoint(address); } Consumer newInboundEndpoint( - beast::IP::Endpoint const& address, + beast::ip::Endpoint const& address, bool const proxy, std::string_view forwardedFor) override { @@ -85,13 +85,13 @@ public: } Consumer - newOutboundEndpoint(beast::IP::Endpoint const& address) override + newOutboundEndpoint(beast::ip::Endpoint const& address) override { return logic_.newOutboundEndpoint(address); } Consumer - newUnlimitedEndpoint(beast::IP::Endpoint const& address) override + newUnlimitedEndpoint(beast::ip::Endpoint const& address) override { return logic_.newUnlimitedEndpoint(address); } @@ -136,7 +136,7 @@ private: void run() { - beast::setCurrentThreadName("Resource::Mngr"); + beast::setCurrentThreadName("resource::Mngr"); for (;;) { logic_.periodicActivity(); @@ -164,4 +164,4 @@ makeManager(beast::insight::Collector::ptr const& collector, beast::Journal jour return std::make_unique(collector, journal); } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/libxrpl/server/InfoSub.cpp b/src/libxrpl/server/InfoSub.cpp index 353c295856..39883873fb 100644 --- a/src/libxrpl/server/InfoSub.cpp +++ b/src/libxrpl/server/InfoSub.cpp @@ -104,7 +104,7 @@ InfoSub::~InfoSub() } } -Resource::Consumer& +resource::Consumer& InfoSub::getConsumer() { return consumer_; diff --git a/src/libxrpl/server/JSONRPCUtil.cpp b/src/libxrpl/server/JSONRPCUtil.cpp index f38ff280ac..d59582fc1a 100644 --- a/src/libxrpl/server/JSONRPCUtil.cpp +++ b/src/libxrpl/server/JSONRPCUtil.cpp @@ -42,7 +42,7 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b // CHECKME this returns a different version than the replies below. Is // this by design or an accident or should it be using - // BuildInfo::getFullVersionString () as well? + // build_info::getFullVersionString () as well? output("Server: " + systemName() + "-json-rpc/v1"); output("\r\n"); @@ -123,7 +123,7 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b "Content-Type: application/json; charset=UTF-8\r\n"); output("Server: " + systemName() + "-json-rpc/"); - output(BuildInfo::getFullVersionString()); + output(build_info::getFullVersionString()); output( "\r\n" "\r\n"); diff --git a/src/libxrpl/server/Port.cpp b/src/libxrpl/server/Port.cpp index c1a79019af..694d4448d5 100644 --- a/src/libxrpl/server/Port.cpp +++ b/src/libxrpl/server/Port.cpp @@ -107,7 +107,7 @@ populate( { // First, check to see if 0.0.0.0 or ipv6 equivalent was configured, // which means all IP addresses. - auto const addr = beast::IP::Endpoint::fromStringChecked(ip); + auto const addr = beast::ip::Endpoint::fromStringChecked(ip); if (addr) { if (isUnspecified(*addr)) diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp index e03cb56fd5..cbfb93c386 100644 --- a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp @@ -864,7 +864,7 @@ applyClaimAttestations( return std::unexpected(tecXCHAIN_NO_CLAIM_ID); // Add claims that are part of the signer's list to the "claims" vector - std::vector atts; + std::vector atts; atts.reserve(std::distance(attBegin, attEnd)); for (auto att = attBegin; att != attEnd; ++att) { @@ -1042,7 +1042,7 @@ applyCreateAccountAttestations( return std::unexpected(tecINSUFFICIENT_RESERVE); } - std::vector atts; + std::vector atts; atts.reserve(std::distance(attBegin, attEnd)); for (auto att = attBegin; att != attEnd; ++att) { @@ -1160,8 +1160,8 @@ std::optional toClaim(STTx const& tx) { static_assert( - std::is_same_v || - std::is_same_v); + std::is_same_v || + std::is_same_v); try { @@ -1301,10 +1301,10 @@ attestationDoApply(ApplyContext& ctx) auto const& [srcChain, signersList, quorum, thisDoor, bridgeK] = scopeResult.value(); static_assert( - std::is_same_v || - std::is_same_v); + std::is_same_v || + std::is_same_v); - if constexpr (std::is_same_v) + if constexpr (std::is_same_v) { return applyClaimAttestations( ctx.view(), @@ -1317,7 +1317,7 @@ attestationDoApply(ApplyContext& ctx) quorum, ctx.journal); } - else if constexpr (std::is_same_v) + else if constexpr (std::is_same_v) { return applyCreateAccountAttestations( ctx.view(), @@ -2067,19 +2067,19 @@ XChainCreateClaimID::doApply() NotTEC XChainAddClaimAttestation::preflight(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return attestationPreflight(ctx); } TER XChainAddClaimAttestation::preclaim(PreclaimContext const& ctx) { - return attestationPreclaim(ctx); + return attestationPreclaim(ctx); } TER XChainAddClaimAttestation::doApply() { - return attestationDoApply(ctx_); + return attestationDoApply(ctx_); } //------------------------------------------------------------------------------ @@ -2087,19 +2087,19 @@ XChainAddClaimAttestation::doApply() NotTEC XChainAddAccountCreateAttestation::preflight(PreflightContext const& ctx) { - return attestationPreflight(ctx); + return attestationPreflight(ctx); } TER XChainAddAccountCreateAttestation::preclaim(PreclaimContext const& ctx) { - return attestationPreclaim(ctx); + return attestationPreclaim(ctx); } TER XChainAddAccountCreateAttestation::doApply() { - return attestationDoApply(ctx_); + return attestationDoApply(ctx_); } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp index e9c153404c..b12cfb692f 100644 --- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp @@ -34,7 +34,7 @@ LoanBrokerSet::checkExtraFeatures(PreflightContext const& ctx) NotTEC LoanBrokerSet::preflight(PreflightContext const& ctx) { - using namespace Lending; + using namespace lending; auto const& tx = ctx.tx; if (auto const data = tx[~sfData]; diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index 0053ed496e..74e8efeda2 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -73,7 +73,7 @@ LoanPay::preflight(PreflightContext const& ctx) XRPAmount LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx) { - using namespace Lending; + using namespace lending; auto const normalCost = Transactor::calculateBaseFee(view, tx); diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp index bafadd7c1d..95a9581dd3 100644 --- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp @@ -53,7 +53,7 @@ LoanSet::getFlagsMask(PreflightContext const& ctx) NotTEC LoanSet::preflight(PreflightContext const& ctx) { - using namespace Lending; + using namespace lending; auto const& tx = ctx.tx; diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index ffaf26b5a7..5230a1f7dd 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -3179,7 +3179,7 @@ class Batch_test : public beast::unit_test::Suite auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); { - using namespace loanBroker; + using namespace loan_broker; env(set(lender, vaultKeylet.key), kManagementFeeRate(TenthBips16(100)), kDebtMaximum(debtMaximumValue), diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 257ed33619..788514e284 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2823,15 +2823,15 @@ class Delegate_test : public beast::unit_test::Suite auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); env(createTx); - env(loanBroker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::coverDeposit(alice, keylet.key, XRP(1)), + env(loan_broker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan_broker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan_broker::coverDeposit(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::coverWithdraw(alice, keylet.key, XRP(1)), + env(loan_broker::coverWithdraw(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); - env(loanBroker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); + env(loan_broker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); env(loan::set(alice, keylet.key, Number(100)), delegate::As(bob), Ter(temINVALID)); env(loan::manage(alice, keylet.key, 0), delegate::As(bob), Ter(temINVALID)); diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index 7b13fc060b..9be01b2abe 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -139,7 +139,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite env.fund(XRP(1000), alice); auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(temDISABLED)); + env(ledger_state_fix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(temDISABLED)); } Env env{*this, testableAmendments()}; @@ -151,38 +151,38 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite { // Fail preflight1. Can't combine AccountTxnID and ticket. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx[sfAccountTxnID.jsonName] = "00000000000000000000000000000000" "00000000000000000000000000000000"; env(tx, ticket::Use(ticketSeq), Ter(temINVALID)); } // Fee too low. - env(ledgerStateFix::nftPageLinks(alice, alice), Ter(telINSUF_FEE_P)); + env(ledger_state_fix::nftPageLinks(alice, alice), Ter(telINSUF_FEE_P)); // Invalid flags. auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(alice, alice), + env(ledger_state_fix::nftPageLinks(alice, alice), Fee(linkFixFee), Txflags(tfPassive), Ter(temINVALID_FLAG)); { - // ledgerStateFix::nftPageLinks requires an Owner field. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + // ledger_state_fix::nftPageLinks requires an Owner field. + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx.removeMember(sfOwner.jsonName); env(tx, Fee(linkFixFee), Ter(temINVALID)); } { // NFTokenPageLink fixes require sfOwner and reject fields that // belong to other LedgerStateFix types. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx[sfBookDirectory.jsonName] = to_string(uint256{1}); env(tx, Fee(linkFixFee), Ter(temINVALID)); } { // Invalid LedgerFixType codes. - json::Value tx = ledgerStateFix::nftPageLinks(alice, alice); + json::Value tx = ledger_state_fix::nftPageLinks(alice, alice); tx[sfLedgerFixType.jsonName] = 0; env(tx, Fee(linkFixFee), Ter(tefINVALID_LEDGER_FIX_TYPE)); @@ -193,7 +193,9 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite // Preclaim Account const carol("carol"); env.memoize(carol); - env(ledgerStateFix::nftPageLinks(alice, carol), Fee(linkFixFee), Ter(tecOBJECT_NOT_FOUND)); + env(ledger_state_fix::nftPageLinks(alice, carol), + Fee(linkFixFee), + Ter(tecOBJECT_NOT_FOUND)); } void @@ -214,13 +216,17 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite // Owner has no pages to fix. auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(tecFAILED_PROCESSING)); + env(ledger_state_fix::nftPageLinks(alice, alice), + Fee(linkFixFee), + Ter(tecFAILED_PROCESSING)); // Alice has only one page. env(token::mint(alice), Txflags(tfTransferable)); env.close(); - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(tecFAILED_PROCESSING)); + env(ledger_state_fix::nftPageLinks(alice, alice), + Fee(linkFixFee), + Ter(tecFAILED_PROCESSING)); // Alice has at least three pages. for (std::uint32_t i = 0; i < 64; ++i) @@ -229,7 +235,9 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite env.close(); } - env(ledgerStateFix::nftPageLinks(alice, alice), Fee(linkFixFee), Ter(tecFAILED_PROCESSING)); + env(ledger_state_fix::nftPageLinks(alice, alice), + Fee(linkFixFee), + Ter(tecFAILED_PROCESSING)); } void @@ -439,7 +447,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite //********************************************************************** // Verify that the LedgerStateFix transaction is not enabled. auto const linkFixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::nftPageLinks(daria, alice), Fee(linkFixFee), Ter(temDISABLED)); + env(ledger_state_fix::nftPageLinks(daria, alice), Fee(linkFixFee), Ter(temDISABLED)); // Wait 15 ledgers so the LedgerStateFix transaction is no longer // retried. @@ -475,7 +483,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite env(noop(daria)); // daria fixes the links in alice's NFToken directory. - env(ledgerStateFix::nftPageLinks(daria, alice), Fee(linkFixFee)); + env(ledger_state_fix::nftPageLinks(daria, alice), Fee(linkFixFee)); env.close(); // alice's last page should now be present and include no links. @@ -516,7 +524,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite } // daria fixes the links in bob's NFToken directory. - env(ledgerStateFix::nftPageLinks(daria, bob), Fee(linkFixFee)); + env(ledger_state_fix::nftPageLinks(daria, bob), Fee(linkFixFee)); env.close(); // bob's last page should now be present and include a previous @@ -574,7 +582,7 @@ class FixNFTokenPageLinks_test : public beast::unit_test::Suite } // carol fixes the links in their own NFToken directory. - env(ledgerStateFix::nftPageLinks(carol, carol), Fee(linkFixFee)); + env(ledger_state_fix::nftPageLinks(carol, carol), Fee(linkFixFee)); env.close(); { diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index ac6d8e068f..9bc9524f80 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -2412,7 +2412,7 @@ class Invariants_test : public beast::unit_test::Suite vaultID = vKeylet.key; // Create Loan Broker - using namespace loanBroker; + using namespace loan_broker; auto const loanBrokerKeylet = keylet::loanBroker(a.id(), env.seq(a)); // Create a Loan Broker with all default values. @@ -2721,7 +2721,7 @@ class Invariants_test : public beast::unit_test::Suite brokerKeylet = this->createLoanBroker(alice, env, asset); if (!BEAST_EXPECT(env.le(brokerKeylet))) return false; - env(loanBroker::coverDeposit(alice, brokerKeylet.key, asset(10))); + env(loan_broker::coverDeposit(alice, brokerKeylet.key, asset(10))); env.close(); return BEAST_EXPECT(env.le(brokerKeylet)); }; diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 4cc83608d6..7b521402a4 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -280,13 +280,13 @@ public: send(std::shared_ptr const& m) override { } - [[nodiscard]] beast::IP::Endpoint + [[nodiscard]] beast::ip::Endpoint getRemoteAddress() const override { return {}; } void - charge(Resource::Charge const& fee, std::string const& context = {}) override + charge(resource::Charge const& fee, std::string const& context = {}) override { } [[nodiscard]] id_t @@ -1206,7 +1206,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite if (serverResult != expecting) return false; - beast::IP::Address const addr = boost::asio::ip::make_address("172.1.1.100"); + beast::ip::Address const addr = boost::asio::ip::make_address("172.1.1.100"); jtx::Env serverEnv(*this); serverEnv.app().config().ledgerReplay = server; auto httpResp = xrpl::makeResponse( diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/LendingHelpers_test.cpp index 1235920fab..5d67cdc3c5 100644 --- a/src/test/app/LendingHelpers_test.cpp +++ b/src/test/app/LendingHelpers_test.cpp @@ -1475,7 +1475,7 @@ class LendingHelpers_test : public beast::unit_test::Suite void testAccrualLoanOriginationDeltas() { - using namespace xrpl::Accrual; + using namespace xrpl::accrual; struct TestCase { @@ -1495,7 +1495,7 @@ class LendingHelpers_test : public beast::unit_test::Suite for (auto const& tc : testCases) { - testcase("Accrual::loanOriginationDeltas: " + tc.name); + testcase("accrual::loanOriginationDeltas: " + tc.name); auto const deltas = loanOriginationDeltas(tc.principalRequested, tc.interestDue); BEAST_EXPECTS( @@ -1513,9 +1513,9 @@ class LendingHelpers_test : public beast::unit_test::Suite void testCashBasisLoanOriginationDeltas() { - using namespace xrpl::CashBasis; + using namespace xrpl::cash_basis; - testcase("CashBasis::loanOriginationDeltas: interestDue is ignored"); + testcase("cash_basis::loanOriginationDeltas: interestDue is ignored"); Number const principalRequested{1'000}; Number const interestDue{75}; @@ -1533,7 +1533,7 @@ class LendingHelpers_test : public beast::unit_test::Suite void testAccrualLoanOriginationExceedsVaultMaximum() { - using namespace xrpl::Accrual; + using namespace xrpl::accrual; struct TestCase { @@ -1569,7 +1569,7 @@ class LendingHelpers_test : public beast::unit_test::Suite for (auto const& tc : testCases) { - testcase("Accrual::loanOriginationExceedsVaultMaximum: " + tc.name); + testcase("accrual::loanOriginationExceedsVaultMaximum: " + tc.name); BEAST_EXPECT( loanOriginationExceedsVaultMaximum( tc.vaultMaximum, tc.vaultTotal, tc.interestDue) == tc.expected); @@ -1613,19 +1613,19 @@ class LendingHelpers_test : public beast::unit_test::Suite void testAccrualLoanVaultExposure() { - testcase("Accrual::loanVaultExposure"); + testcase("accrual::loanVaultExposure"); auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); - BEAST_EXPECT(xrpl::Accrual::loanVaultExposure(sle) == Number{950}); + BEAST_EXPECT(xrpl::accrual::loanVaultExposure(sle) == Number{950}); } void testCashBasisLoanVaultExposure() { - testcase("CashBasis::loanVaultExposure"); + testcase("cash_basis::loanVaultExposure"); auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); - BEAST_EXPECT(xrpl::CashBasis::loanVaultExposure(sle) == Number{800}); + BEAST_EXPECT(xrpl::cash_basis::loanVaultExposure(sle) == Number{800}); } void @@ -1641,8 +1641,8 @@ class LendingHelpers_test : public beast::unit_test::Suite .feePaid = Number{3}}; { - testcase("Accrual::loanPaymentDeltas: nonzero valueChange"); - auto const deltas = xrpl::Accrual::loanPaymentDeltas(parts); + testcase("accrual::loanPaymentDeltas: nonzero valueChange"); + auto const deltas = xrpl::accrual::loanPaymentDeltas(parts); BEAST_EXPECT(deltas.assetsTotalDelta == parts.valueChange); BEAST_EXPECT( deltas.debtTotalDelta == @@ -1650,8 +1650,8 @@ class LendingHelpers_test : public beast::unit_test::Suite } { - testcase("CashBasis::loanPaymentDeltas: nonzero valueChange ignored"); - auto const deltas = xrpl::CashBasis::loanPaymentDeltas(parts); + testcase("cash_basis::loanPaymentDeltas: nonzero valueChange ignored"); + auto const deltas = xrpl::cash_basis::loanPaymentDeltas(parts); BEAST_EXPECT(deltas.assetsTotalDelta == parts.interestPaid); BEAST_EXPECT(deltas.debtTotalDelta == parts.principalPaid); } @@ -1675,7 +1675,7 @@ class LendingHelpers_test : public beast::unit_test::Suite Env const env{*this}; auto const deltas = loanOriginationDeltas(legacyVault, principalRequested, interestDue); auto const expected = - xrpl::Accrual::loanOriginationDeltas(principalRequested, interestDue); + xrpl::accrual::loanOriginationDeltas(principalRequested, interestDue); BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); } @@ -1687,7 +1687,7 @@ class LendingHelpers_test : public beast::unit_test::Suite Env const env{*this}; auto const deltas = loanOriginationDeltas(cashBasisVault, principalRequested, interestDue); - auto const expected = xrpl::CashBasis::loanOriginationDeltas(principalRequested); + auto const expected = xrpl::cash_basis::loanOriginationDeltas(principalRequested); BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); } @@ -1713,7 +1713,7 @@ class LendingHelpers_test : public beast::unit_test::Suite Env const env{*this}; BEAST_EXPECT( loanOriginationExceedsVaultMaximum(legacyVault, vaultTotal, interestDue) == - xrpl::Accrual::loanOriginationExceedsVaultMaximum( + xrpl::accrual::loanOriginationExceedsVaultMaximum( vaultMaximum, vaultTotal, interestDue)); } @@ -1741,7 +1741,7 @@ class LendingHelpers_test : public beast::unit_test::Suite Env const env{*this}; auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); BEAST_EXPECT( - loanVaultExposure(legacyVault, sle) == xrpl::Accrual::loanVaultExposure(sle)); + loanVaultExposure(legacyVault, sle) == xrpl::accrual::loanVaultExposure(sle)); } { @@ -1752,7 +1752,7 @@ class LendingHelpers_test : public beast::unit_test::Suite Env const env{*this}; auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50}); BEAST_EXPECT( - loanVaultExposure(cashBasisVault, sle) == xrpl::CashBasis::loanVaultExposure(sle)); + loanVaultExposure(cashBasisVault, sle) == xrpl::cash_basis::loanVaultExposure(sle)); } } @@ -1774,7 +1774,7 @@ class LendingHelpers_test : public beast::unit_test::Suite testcase("loanPaymentDeltas dispatcher: amendment enabled, legacy vault picks Accrual"); Env const env{*this}; auto const deltas = loanPaymentDeltas(legacyVault, parts); - auto const expected = xrpl::Accrual::loanPaymentDeltas(parts); + auto const expected = xrpl::accrual::loanPaymentDeltas(parts); BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); } @@ -1786,7 +1786,7 @@ class LendingHelpers_test : public beast::unit_test::Suite "picks CashBasis"); Env const env{*this}; auto const deltas = loanPaymentDeltas(cashBasisVault, parts); - auto const expected = xrpl::CashBasis::loanPaymentDeltas(parts); + auto const expected = xrpl::cash_basis::loanPaymentDeltas(parts); BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta); BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta); } diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index f6f85a0cca..ee398bfc3b 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -93,7 +93,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); BEAST_EXPECT(static_cast(env.le(keylet)) == goodVault); - using namespace loanBroker; + 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)); @@ -168,7 +168,7 @@ class LoanBroker_test : public beast::unit_test::Suite } using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; // Bogus assets to use in test cases static PrettyAsset const kBadMptAsset = [&]() { @@ -645,8 +645,8 @@ class LoanBroker_test : public beast::unit_test::Suite } } - using namespace loanBroker; - using namespace xrpl::Lending; + using namespace loan_broker; + using namespace xrpl::lending; TenthBips32 const tenthBipsZero{0}; @@ -862,7 +862,7 @@ class LoanBroker_test : public beast::unit_test::Suite LoanBrokerTest brokerTest) { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; Env env(*this); @@ -1093,7 +1093,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase("Invalid LoanBrokerCoverClawback"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; // preflight { @@ -1220,7 +1220,7 @@ class LoanBroker_test : public beast::unit_test::Suite auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); // Create LoanBroker pointing to the vault - env(loanBroker::set(alice, vaultKeylet.key)); + env(loan_broker::set(alice, vaultKeylet.key)); env.close(); // Build the CoverDeposit STTx directly @@ -1256,7 +1256,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase("Require Auth - Implicit Pseudo-account authorization"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -1364,7 +1364,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase("testLoanBrokerSetDebtMaximum"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; Env env(*this); @@ -1550,10 +1550,10 @@ class LoanBroker_test : public beast::unit_test::Suite auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); - env(loanBroker::set(broker, keylet.key)); + env(loan_broker::set(broker, keylet.key)); env.close(); - env(loanBroker::coverDeposit(broker, brokerKeylet.key, deposit), Ter(err)); + env(loan_broker::coverDeposit(broker, brokerKeylet.key, deposit), Ter(err)); env.close(); }; @@ -1621,7 +1621,7 @@ class LoanBroker_test : public beast::unit_test::Suite auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo); env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze)); - env(loanBroker::set(lender, vaultKeylet.key), Ter(tecFROZEN)); + env(loan_broker::set(lender, vaultKeylet.key), Ter(tecFROZEN)); } void @@ -1629,7 +1629,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase << "LoanBrokerDelete - locked broker pseudo-account MPT"; using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer("issuer"); Account const alice("alice"); @@ -1749,7 +1749,7 @@ class LoanBroker_test : public beast::unit_test::Suite { testcase << "LoanBrokerDelete - frozen broker pseudo-account IOU"; using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer("issuer"); Account const alice("alice"); @@ -1833,7 +1833,7 @@ class LoanBroker_test : public beast::unit_test::Suite testCoverDepositFreezes() { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -1984,7 +1984,7 @@ class LoanBroker_test : public beast::unit_test::Suite testcase("LoanBrokerCoverWithdraw IOU self-withdrawal while individually frozen"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -2046,7 +2046,7 @@ class LoanBroker_test : public beast::unit_test::Suite testCoverWithdrawFreezes() { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; @@ -2351,7 +2351,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}), - loanBroker::kDestination(dest), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == tecNO_LINE); env.close(); @@ -2361,14 +2361,14 @@ class LoanBroker_test : public beast::unit_test::Suite // Test LoanBroker withdraw auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); - env(loanBroker::set(broker, keylet.key)); + env(loan_broker::set(broker, keylet.key)); env.close(); - env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000))); + env(loan_broker::coverDeposit(broker, brokerKeylet.key, token(1'000))); env.close(); - env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), - loanBroker::kDestination(dest), + env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == tecNO_LINE); env.close(); @@ -2379,8 +2379,8 @@ class LoanBroker_test : public beast::unit_test::Suite env(fclear(issuer, asfRequireAuth)); env.close(); - env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), - loanBroker::kDestination(dest), + env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == tecNO_LINE); env.close(); @@ -2472,7 +2472,7 @@ class LoanBroker_test : public beast::unit_test::Suite env.close(); env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}), - loanBroker::kDestination(dest), + loan_broker::kDestination(dest), Ter(std::ignore)); // Shouldn't fail if at MaximumAmount since no new tokens are issued @@ -2489,14 +2489,14 @@ class LoanBroker_test : public beast::unit_test::Suite // Test LoanBroker withdraw auto const brokerKeylet = keylet::loanBroker(broker, env.seq(broker)); - env(loanBroker::set(broker, keylet.key)); + env(loan_broker::set(broker, keylet.key)); env.close(); - env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000))); + env(loan_broker::coverDeposit(broker, brokerKeylet.key, token(1'000))); env.close(); - env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)), - loanBroker::kDestination(dest), + env(loan_broker::coverWithdraw(broker, brokerKeylet.key, token(100)), + loan_broker::kDestination(dest), Ter(std::ignore)); BEAST_EXPECT(env.ter() == err); env.close(); @@ -2522,7 +2522,7 @@ class LoanBroker_test : public beast::unit_test::Suite testCoverPrecisionGuard() { using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Account const issuer{"issuer"}; Account const alice{"alice"}; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 8a6f1669df..977cdb443c 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -545,7 +545,7 @@ protected: auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender)); - using namespace loanBroker; + using namespace loan_broker; env(set(lender, vaultKeylet.key, params.flags), kData(params.data), kManagementFeeRate(params.managementFeeRate), @@ -1533,7 +1533,7 @@ protected: auto const borrowerStartingBalance = env.balance(borrower, broker.asset); // Try to delete the loan broker with an active loan - env(loanBroker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS)); + env(loan_broker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS)); // Ensure the above tx doesn't get ordered after the LoanDelete and // delete our broker! env.close(); @@ -1608,7 +1608,7 @@ protected: int interestExponent) { using namespace jtx; - using namespace Lending; + using namespace lending; auto const& asset = broker.asset.raw(); auto const currencyLabel = getCurrencyLabel(asset); @@ -2072,7 +2072,7 @@ protected: Number const& startingCoverAvailable, Number const& amountToBeCovered) { coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered); - env(loanBroker::coverDeposit( + env(loan_broker::coverDeposit( brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered})); coverAvailable(broker.brokerID, startingCoverAvailable); env.close(); @@ -3572,7 +3572,7 @@ protected: BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); auto const coverAvailable = brokerSle->at(sfCoverAvailable); - env(loanBroker::coverWithdraw( + env(loan_broker::coverWithdraw( lender, broker.brokerID, STAmount(broker.asset, coverAvailable))); env.close(); @@ -3580,7 +3580,7 @@ protected: BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); } // Verify we can delete the loan broker - env(loanBroker::del(lender, broker.brokerID)); + env(loan_broker::del(lender, broker.brokerID)); env.close(); } } @@ -4659,7 +4659,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; - using namespace Lending; + using namespace lending; Env env(*this, features); Account const issuer{"issuer"}; @@ -4818,7 +4818,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; - using namespace Lending; + using namespace lending; Env env(*this, features); Account const issuer{"issuer"}; @@ -4959,7 +4959,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; - using namespace Lending; + using namespace lending; Env env(*this, features); Account const issuer{"issuer"}; @@ -5060,7 +5060,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; - using namespace Lending; + using namespace lending; Env env(*this, features); Account const issuer{"issuer"}; @@ -5084,7 +5084,7 @@ protected: BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; { auto const coverDepositValue = broker.asset(broker.params.coverDeposit * 10).value(); - env(loanBroker::coverDeposit(lender, broker.brokerID, coverDepositValue)); + env(loan_broker::coverDeposit(lender, broker.brokerID, coverDepositValue)); env.close(); } @@ -5145,7 +5145,7 @@ protected: using namespace jtx; using namespace std::chrono_literals; - using namespace Lending; + using namespace lending; Env env{*this, features}; Account const issuer{"issuer"}; @@ -5450,7 +5450,7 @@ protected: testcase("Lending: CanTrade disabled has no impact"); using namespace jtx; using namespace loan; - using namespace loanBroker; + using namespace loan_broker; Env env(*this, all_); @@ -5671,7 +5671,7 @@ protected: using namespace jtx; using namespace loan; - using namespace loanBroker; + using namespace loan_broker; Env env(*this, features); @@ -6283,7 +6283,7 @@ protected: auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender)); - env(loanBroker::set(lender, vaultKeyLet.key), txFee); + env(loan_broker::set(lender, vaultKeyLet.key), txFee); env.close(); // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}}; @@ -6317,7 +6317,7 @@ protected: testcase("Minimum cover rounding allows undercoverage (XRP)"); using namespace jtx; - using namespace loanBroker; + using namespace loan_broker; Env env{*this, features}; @@ -6488,7 +6488,7 @@ protected: auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker)); - env(loanBroker::set(broker, vaultKeylet.key), txFee); + env(loan_broker::set(broker, vaultKeylet.key), txFee); env.close(); auto const serviceFee = 101; @@ -6767,7 +6767,7 @@ protected: // at least 1,000 cover. Default cover is 1,000, so we add more to be // safe. auto const additionalCover = iou(50'000).value(); - env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover})); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover})); env.close(); // Verify broker owner has a trustline auto const brokerTrustline = keylet::trustLine(broker, iou); @@ -6847,7 +6847,7 @@ protected: // at least 1,000 cover. Default cover is 1,000, so we add more to be // safe. auto const additionalCover = mpt(50'000).value(); - env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); env.close(); // Verify broker owner is authorized auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); @@ -6947,7 +6947,7 @@ protected: // at least 1,000 cover. Default cover is 1,000, so we add more to be // safe. auto const additionalCover = mpt(50'000).value(); - env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); env.close(); // Verify broker owner is authorized auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); @@ -7062,7 +7062,7 @@ protected: using namespace jtx; using namespace loan; - using namespace loanBroker; + using namespace loan_broker; Env env{*this, features}; @@ -7923,8 +7923,8 @@ protected: env.close(); auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); - env(loanBroker::set(lender, vaultKeylet.key), - loanBroker::kDebtMaximum(Number{100}), + env(loan_broker::set(lender, vaultKeylet.key), + loan_broker::kDebtMaximum(Number{100}), Fee(env.current()->fees().base * 2)); env.close(); @@ -8168,7 +8168,7 @@ protected: { using namespace jtx; using namespace loan; - using namespace loanBroker; + using namespace loan_broker; bool const withAmendment = features[fixCleanup3_2_0]; @@ -8726,9 +8726,9 @@ protected: BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - env(loanBroker::set(lender, broker.vaultID), - loanBroker::kLoanBrokerId(broker.brokerID), - loanBroker::kDebtMaximum(debtMaximum), + env(loan_broker::set(lender, broker.vaultID), + loan_broker::kLoanBrokerId(broker.brokerID), + loan_broker::kDebtMaximum(debtMaximum), Fee(env.current()->fees().base * 2)); env.close(); diff --git a/src/test/app/PathMPT_test.cpp b/src/test/app/PathMPT_test.cpp index 3ba67b58a6..ff4a024cb8 100644 --- a/src/test/app/PathMPT_test.cpp +++ b/src/test/app/PathMPT_test.cpp @@ -112,10 +112,10 @@ public: MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 100}); auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -125,39 +125,39 @@ public: .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; json::Value result; Gate g; - // Test RPC::Tuning::max_src_cur source currencies. + // Test rpc::tuning::max_src_cur source currencies. std::vector numSrc; - numSrc.reserve(RPC::Tuning::kMaxSrcCur); - for (std::uint8_t i = 0; i < RPC::Tuning::kMaxSrcCur; ++i) + numSrc.reserve(rpc::tuning::kMaxSrcCur); + for (std::uint8_t i = 0; i < rpc::tuning::kMaxSrcCur; ++i) numSrc.push_back(makeMptID(i, bob)); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_src_cur source currencies. - numSrc.push_back(makeMptID(RPC::Tuning::kMaxSrcCur, bob)); + // Test more than rpc::tuning::max_src_cur source currencies. + numSrc.push_back(makeMptID(rpc::tuning::kMaxSrcCur, bob)); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, numSrc); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(result.isMember(jss::error)); - // Test RPC::Tuning::max_auto_src_cur source currencies. + // Test rpc::tuning::max_auto_src_cur source currencies. numSrc.clear(); - for (auto i = 0; i < (RPC::Tuning::kMaxAutoSrcCur - 1); ++i) + for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i) { auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}}); numSrc.push_back(curm.issuanceID()); @@ -165,18 +165,18 @@ public: app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, {}); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_auto_src_cur source currencies. + // Test more than rpc::tuning::max_auto_src_cur source currencies. auto curm = MPTTester({.env = env, .issuer = alice, .holders = {bob}}); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = xrpl::test::detail::rpf(alice, bob, usd, {}); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 8f19a419a0..409a9e86f8 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -151,10 +151,10 @@ public: using namespace jtx; auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -164,7 +164,7 @@ public: .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; @@ -190,7 +190,7 @@ public: app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = std::move(params); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); @@ -262,10 +262,10 @@ public: env.close(); auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -275,49 +275,49 @@ public: .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; json::Value result; Gate g; - // Test RPC::Tuning::max_src_cur source currencies. + // Test rpc::tuning::max_src_cur source currencies. app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { - context.params = rpf(Account("alice"), Account("bob"), RPC::Tuning::kMaxSrcCur); + context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_src_cur source currencies. + // Test more than rpc::tuning::max_src_cur source currencies. app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { - context.params = rpf(Account("alice"), Account("bob"), RPC::Tuning::kMaxSrcCur + 1); + context.params = rpf(Account("alice"), Account("bob"), rpc::tuning::kMaxSrcCur + 1); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(result.isMember(jss::error)); - // Test RPC::Tuning::max_auto_src_cur source currencies. - for (auto i = 0; i < (RPC::Tuning::kMaxAutoSrcCur - 1); ++i) + // Test rpc::tuning::max_auto_src_cur source currencies. + for (auto i = 0; i < (rpc::tuning::kMaxAutoSrcCur - 1); ++i) env.trust(Account("alice")[std::to_string(i + 100)](100), "bob"); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = rpf(Account("alice"), Account("bob"), 0); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); BEAST_EXPECT(!result.isMember(jss::error)); - // Test more than RPC::Tuning::max_auto_src_cur source currencies. + // Test more than rpc::tuning::max_auto_src_cur source currencies. env.trust(Account("alice")["AUD"](100), "bob"); app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = rpf(Account("alice"), Account("bob"), 0); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); BEAST_EXPECT(g.waitFor(5s)); diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp index 67cb7602a0..68b2fa99a7 100644 --- a/src/test/app/PermissionedDEX_test.cpp +++ b/src/test/app/PermissionedDEX_test.cpp @@ -1816,7 +1816,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.fund(XRP(1000), carol); env.close(); - env(ledgerStateFix::bookExchangeRate(carol, uint256{1}), Ter(temDISABLED)); + env(ledger_state_fix::bookExchangeRate(carol, uint256{1}), Ter(temDISABLED)); } { @@ -1829,13 +1829,13 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); // BookExchangeRate fixes require sfBookDirectory. - auto missingBookDirectory = ledgerStateFix::bookExchangeRate(carol, uint256{1}); + auto missingBookDirectory = ledger_state_fix::bookExchangeRate(carol, uint256{1}); missingBookDirectory.removeMember(sfBookDirectory.jsonName); env(missingBookDirectory, Ter(temINVALID)); // BookExchangeRate fixes reject fields that belong to other // LedgerStateFix types. - auto extraOwner = ledgerStateFix::bookExchangeRate(carol, uint256{1}); + auto extraOwner = ledger_state_fix::bookExchangeRate(carol, uint256{1}); extraOwner[sfOwner.jsonName] = carol.human(); env(extraOwner, Ter(temINVALID)); } @@ -1847,7 +1847,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite { // Preclaim check: the target directory must exist. - env(ledgerStateFix::bookExchangeRate(setup.carol, uint256{1}), + env(ledger_state_fix::bookExchangeRate(setup.carol, uint256{1}), Fee(fixFee), Ter(tecOBJECT_NOT_FOUND)); } @@ -1861,7 +1861,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(ownerDirSle); BEAST_EXPECT(!ownerDirSle->isFieldPresent(sfExchangeRate)); - env(ledgerStateFix::bookExchangeRate(setup.carol, ownerDir.key), + env(ledger_state_fix::bookExchangeRate(setup.carol, ownerDir.key), Fee(fixFee), Ter(tecNO_PERMISSION)); } @@ -1885,7 +1885,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite BEAST_EXPECT(exchangeRate == quality); } - env(ledgerStateFix::bookExchangeRate(setup.carol, dirKey), + env(ledger_state_fix::bookExchangeRate(setup.carol, dirKey), Fee(fixFee), Ter(tecNO_PERMISSION)); } @@ -1932,7 +1932,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite env.close(); auto const fixFee = drops(env.current()->fees().increment); - env(ledgerStateFix::bookExchangeRate(carol_, openDirKey), Fee(fixFee)); + env(ledger_state_fix::bookExchangeRate(carol_, openDirKey), Fee(fixFee)); env.close(); // Confirm sfExchangeRate now matches the key quality. @@ -1947,7 +1947,7 @@ class PermissionedDEX_test : public beast::unit_test::Suite } // Submitting again should fail — nothing to fix. - env(ledgerStateFix::bookExchangeRate(carol_, openDirKey), + env(ledger_state_fix::bookExchangeRate(carol_, openDirKey), Fee(fixFee), Ter(tecNO_PERMISSION)); } diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 537ee4c177..6ee7442d23 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -60,7 +60,7 @@ class SHAMapStore_test : public beast::unit_test::Suite static bool goodLedger(jtx::Env& env, json::Value const& json, std::string ledgerID, bool checkDB = false) { - auto good = json.isMember(jss::result) && !RPC::containsError(json[jss::result]) && + auto good = json.isMember(jss::result) && !rpc::containsError(json[jss::result]) && json[jss::result][jss::ledger][jss::ledger_index] == ledgerID; if (!good || !checkDB) return good; @@ -99,7 +99,7 @@ class SHAMapStore_test : public beast::unit_test::Suite static bool bad(json::Value const& json, ErrorCodeI error = RpcLgrNotFound) { - return json.isMember(jss::result) && RPC::containsError(json[jss::result]) && + return json.isMember(jss::result) && rpc::containsError(json[jss::result]) && json[jss::result][jss::error_code] == error; } @@ -347,11 +347,11 @@ public: BEAST_EXPECT(lastRotated != 2); auto canDelete = env.rpc("can_delete"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0); canDelete = env.rpc("can_delete", "never"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0); auto const firstBatch = kDeleteInterval + ledgerSeq; @@ -370,7 +370,7 @@ public: // This does not kick off a cleanup canDelete = env.rpc("can_delete", std::to_string(ledgerSeq + (kDeleteInterval / 2))); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq + (kDeleteInterval / 2)); store.rendezvous(); @@ -423,7 +423,7 @@ public: // This does not kick off a cleanup canDelete = env.rpc("can_delete", "always"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT( canDelete[jss::result][jss::can_delete] == std::numeric_limits::max()); @@ -457,7 +457,7 @@ public: // This does not kick off a cleanup canDelete = env.rpc("can_delete", "now"); - BEAST_EXPECT(!RPC::containsError(canDelete[jss::result])); + BEAST_EXPECT(!rpc::containsError(canDelete[jss::result])); BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq - 1); for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq) diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index f20aac68f9..393a6e58f7 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1664,11 +1664,11 @@ public: env.close(); auto const brokerKeylet = keylet::loanBroker(alice.id(), env.seq(alice)); - env(loanBroker::set(alice, vaultKeylet.key), - loanBroker::kDebtMaximum(xrpAsset(1000).value()), - loanBroker::kManagementFeeRate(TenthBips16{0}), - loanBroker::kCoverRateMinimum(TenthBips32{0}), - loanBroker::kCoverRateLiquidation(TenthBips32{0})); + env(loan_broker::set(alice, vaultKeylet.key), + loan_broker::kDebtMaximum(xrpAsset(1000).value()), + loan_broker::kManagementFeeRate(TenthBips16{0}), + loan_broker::kCoverRateMinimum(TenthBips32{0}), + loan_broker::kCoverRateLiquidation(TenthBips32{0})); env.close(); auto const loanKeylet = keylet::loan(brokerKeylet.key, 1); @@ -2019,7 +2019,7 @@ public: env(sponsor::set_fee(sponsor, 0, fixFee), sponsor::SponseeAcc(alice)); env.close(); - env(ledgerStateFix::nftPageLinks(alice, alice), + env(ledger_state_fix::nftPageLinks(alice, alice), Fee(fixFee), sponsor::As(sponsor, spfSponsorFee), Ter(tecFAILED_PROCESSING)); @@ -2043,7 +2043,7 @@ public: OpenView overlay(&*env.closed()); auto jt = env.jt( - ledgerStateFix::nftPageLinks(alice, alice), + ledger_state_fix::nftPageLinks(alice, alice), Fee(fixFee), sponsor::As(sponsor, spfSponsorFee)); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 3175e742d9..1fe48add27 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -2571,7 +2571,7 @@ public: auto fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; BEAST_EXPECT( @@ -2600,7 +2600,7 @@ public: fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; BEAST_EXPECT( @@ -3225,7 +3225,7 @@ public: { auto const info = env.rpc("json", "account_info", to_string(prevLedgerWithQueue)); - BEAST_EXPECT(info.isMember(jss::result) && RPC::containsError(info[jss::result])); + BEAST_EXPECT(info.isMember(jss::result) && rpc::containsError(info[jss::result])); } env.close(); @@ -4630,7 +4630,7 @@ public: auto const fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; @@ -4688,7 +4688,7 @@ public: auto const fee = env.rpc("fee"); if (BEAST_EXPECT(fee.isMember(jss::result)) && - BEAST_EXPECT(!RPC::containsError(fee[jss::result]))) + BEAST_EXPECT(!rpc::containsError(fee[jss::result]))) { auto const& result = fee[jss::result]; diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index bd596d6149..791cf216c3 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -3936,7 +3936,7 @@ class Vault_test : public beast::unit_test::Suite // Deposit 100 IOU → 1000 shares. Borrow 40 → assetsAvailable=60. // Clawback 80 IOU → clamped to 60, then share math uses truncation. testCase(1, [&, this](Env& env, Data d) { - using namespace loanBroker; + using namespace loan_broker; using namespace loan; testcase("Scale clawback clamped with outstanding loan"); @@ -4410,7 +4410,7 @@ class Vault_test : public beast::unit_test::Suite testVaultClawbackBurnShares() { using namespace test::jtx; - using namespace loanBroker; + using namespace loan_broker; using namespace loan; Env env(*this, beast::Severity::Warning); @@ -4670,7 +4670,7 @@ class Vault_test : public beast::unit_test::Suite testVaultClawbackAssets() { using namespace test::jtx; - using namespace loanBroker; + using namespace loan_broker; using namespace loan; Env env(*this); env.enableFeature(fixCleanup3_1_3); @@ -6093,7 +6093,7 @@ class Vault_test : public beast::unit_test::Suite // Loan broker: no cover, no management fee, debt cap 10x principal. f.brokerID = keylet::loanBroker(f.lender.id(), env.seq(f.lender)).key; { - using namespace loanBroker; + using namespace loan_broker; env(set(f.lender, vaultKeylet.key), kDebtMaximum((*f.asset)(kStuckPrincipal * 10).value())); env.close(); diff --git a/src/test/basics/PerfLog_test.cpp b/src/test/basics/PerfLog_test.cpp index 41b5f81f5d..24ea971515 100644 --- a/src/test/basics/PerfLog_test.cpp +++ b/src/test/basics/PerfLog_test.cpp @@ -312,7 +312,7 @@ public: // Get the all the labels we can use for RPC interfaces without // causing an assert. - std::vector labels = test::jtx::makeVector(xrpl::RPC::getHandlerNames()); + std::vector labels = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); std::shuffle(labels.begin(), labels.end(), defaultPrng()); // Get two IDs to associate with each label. Errors tend to happen at @@ -483,7 +483,7 @@ public: json::Value parsedLastLine; json::Reader().parse(lastLine, parsedLastLine); - if (!BEAST_EXPECT(!RPC::containsError(parsedLastLine))) + if (!BEAST_EXPECT(!rpc::containsError(parsedLastLine))) { // Avoid cascade of failures return; @@ -804,7 +804,7 @@ public: json::Value parsedLastLine; json::Reader().parse(lastLine, parsedLastLine); - if (!BEAST_EXPECT(!RPC::containsError(parsedLastLine))) + if (!BEAST_EXPECT(!rpc::containsError(parsedLastLine))) { // Avoid cascade of failures return; @@ -944,7 +944,7 @@ public: json::Value parsedLastLine; json::Reader().parse(lastLine, parsedLastLine); - if (!BEAST_EXPECT(!RPC::containsError(parsedLastLine))) + if (!BEAST_EXPECT(!rpc::containsError(parsedLastLine))) { // Avoid cascade of failures return; diff --git a/src/test/beast/IPEndpointCommon.h b/src/test/beast/IPEndpointCommon.h index 45d036476c..6fb2bd9569 100644 --- a/src/test/beast/IPEndpointCommon.h +++ b/src/test/beast/IPEndpointCommon.h @@ -7,7 +7,7 @@ #include -namespace beast::IP { +namespace beast::ip { inline Endpoint randomEP(bool v4 = true) @@ -44,4 +44,4 @@ randomEP(bool v4 = true) randInt(1, UINT16_MAX)}; } -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/test/beast/IPEndpoint_test.cpp b/src/test/beast/IPEndpoint_test.cpp index bc04087891..b61878aa76 100644 --- a/src/test/beast/IPEndpoint_test.cpp +++ b/src/test/beast/IPEndpoint_test.cpp @@ -22,7 +22,7 @@ #include #include -namespace beast::IP { +namespace beast::ip { //------------------------------------------------------------------------------ @@ -475,4 +475,4 @@ public: BEAST_DEFINE_TESTSUITE(IPEndpoint, beast, beast); -} // namespace beast::IP +} // namespace beast::ip diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 68b6d9f745..435e32b7b4 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -199,7 +199,7 @@ public: std::optional const& asset2 = std::nullopt, std::optional const& ammAccount = std::nullopt, bool ignoreParams = false, - unsigned apiVersion = RPC::kApiInvalidVersion) const; + unsigned apiVersion = rpc::kApiInvalidVersion) const; [[nodiscard]] json::Value ammRpcInfo( diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index a175fd5006..5cb841578a 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -1081,7 +1081,7 @@ Env::rpc( Args&&... args) { return doRpc( - RPC::kApiCommandLineVersion, + rpc::kApiCommandLineVersion, std::vector{cmd, std::forward(args)...}, headers); } diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index e7a2808f07..d5cd8e66b8 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -876,7 +876,7 @@ checkMetrics( /* LoanBroker */ /******************************************************************************/ -namespace loanBroker { +namespace loan_broker { json::Value set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0); @@ -917,7 +917,7 @@ auto const kCoverRateLiquidation = auto const kDestination = JTxFieldWrapper(sfDestination); -} // namespace loanBroker +} // namespace loan_broker /* Loan */ /******************************************************************************/ diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 8effce288e..74232037ce 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -234,7 +234,7 @@ AMM::ammRpcInfo( jv[jss::amm_account] = *ammAccount; } auto jr = - (apiVersion == RPC::kApiInvalidVersion + (apiVersion == rpc::kApiInvalidVersion ? env_.rpc("json", "amm_info", to_string(jv)) : env_.rpc(apiVersion, "json", "amm_info", to_string(jv))); if (jr.isObject() && jr.isMember(jss::result) && jr[jss::result].isMember(jss::status)) diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index 4da2e2b521..35553bdeb2 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -498,9 +498,9 @@ Env::postconditions( !test.expect( parsed.rpcCode == jt.rpcCode->first && parsed.rpcMessage == jt.rpcCode->second, "apply " + locStr + ": Got RPC result "s + - (parsed.rpcCode ? RPC::getErrorInfo(*parsed.rpcCode).token.cStr() : "NO RESULT") + + (parsed.rpcCode ? rpc::getErrorInfo(*parsed.rpcCode).token.cStr() : "NO RESULT") + " (" + parsed.rpcMessage + "); Expected " + - RPC::getErrorInfo(jt.rpcCode->first).token.cStr() + " (" + jt.rpcCode->second + + rpc::getErrorInfo(jt.rpcCode->first).token.cStr() + " (" + jt.rpcCode->second + ")")) || bad; // If we have an rpcCode (just checked), then the rpcException check is diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 4d3869b4f9..d73eb8adf4 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -211,10 +211,10 @@ findPathsRequest( using namespace jtx; auto& app = env.app(); - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = env.journal, .app = app, .loadType = loadType, @@ -224,7 +224,7 @@ findPathsRequest( .role = Role::USER, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiVersionIfUnspecified}, + .apiVersion = rpc::kApiVersionIfUnspecified}, {}, {}}; @@ -252,7 +252,7 @@ findPathsRequest( app.getJobQueue().postCoro(JtClient, "RPC-Client", [&](auto const& coro) { context.params = std::move(params); context.coro = coro; - RPC::doCommand(context, result); + rpc::doCommand(context, result); g.signal(); }); @@ -743,7 +743,7 @@ issueHelperMPT(IssuerArgs const& args) /* LoanBroker */ /******************************************************************************/ -namespace loanBroker { +namespace loan_broker { json::Value set(AccountID const& account, uint256 const& vaultId, uint32_t flags) @@ -809,7 +809,7 @@ coverClawback(AccountID const& account, std::uint32_t flags) return jv; } -} // namespace loanBroker +} // namespace loan_broker /* Loan */ /******************************************************************************/ diff --git a/src/test/jtx/impl/attester.cpp b/src/test/jtx/impl/attester.cpp index ac946a1bf3..3799d957e9 100644 --- a/src/test/jtx/impl/attester.cpp +++ b/src/test/jtx/impl/attester.cpp @@ -24,7 +24,7 @@ signClaimAttestation( std::uint64_t claimID, std::optional const& dst) { - auto const toSign = Attestations::AttestationClaim::message( + auto const toSign = attestations::AttestationClaim::message( bridge, sendingAccount, sendingAmount, rewardAccount, wasLockingChainSend, claimID, dst); return sign(pk, sk, makeSlice(toSign)); } @@ -42,7 +42,7 @@ signCreateAccountAttestation( std::uint64_t createCount, AccountID const& dst) { - auto const toSign = Attestations::AttestationCreateAccount::message( + auto const toSign = attestations::AttestationCreateAccount::message( bridge, sendingAccount, sendingAmount, diff --git a/src/test/jtx/impl/ledgerStateFixes.cpp b/src/test/jtx/impl/ledgerStateFixes.cpp index 30c6659124..ae195021b8 100644 --- a/src/test/jtx/impl/ledgerStateFixes.cpp +++ b/src/test/jtx/impl/ledgerStateFixes.cpp @@ -9,7 +9,7 @@ #include -namespace xrpl::test::jtx::ledgerStateFix { +namespace xrpl::test::jtx::ledger_state_fix { // Fix NFTokenPage links on owner's account. acct pays fee. json::Value @@ -35,4 +35,4 @@ bookExchangeRate(jtx::Account const& acct, uint256 const& bookDir) return jv; } -} // namespace xrpl::test::jtx::ledgerStateFix +} // namespace xrpl::test::jtx::ledger_state_fix diff --git a/src/test/jtx/ledgerStateFix.h b/src/test/jtx/ledgerStateFix.h index 2fe5c8accc..4ae22f891e 100644 --- a/src/test/jtx/ledgerStateFix.h +++ b/src/test/jtx/ledgerStateFix.h @@ -8,7 +8,7 @@ /** * LedgerStateFix operations. */ -namespace xrpl::test::jtx::ledgerStateFix { +namespace xrpl::test::jtx::ledger_state_fix { /** * Repair the links in an NFToken directory. @@ -22,4 +22,4 @@ nftPageLinks(jtx::Account const& acct, jtx::Account const& owner); json::Value bookExchangeRate(jtx::Account const& acct, uint256 const& bookDir); -} // namespace xrpl::test::jtx::ledgerStateFix +} // namespace xrpl::test::jtx::ledger_state_fix diff --git a/src/test/jtx/rpc.h b/src/test/jtx/rpc.h index 9bd99c15f8..7fd550563c 100644 --- a/src/test/jtx/rpc.h +++ b/src/test/jtx/rpc.h @@ -48,7 +48,7 @@ public: jt.ter = telENV_RPC_FAILED; if (code_) { - auto const& errorInfo = RPC::getErrorInfo(*code_); + auto const& errorInfo = rpc::getErrorInfo(*code_); // When an RPC request returns an error code ('error_code'), it // always includes an error message ('error_message'), and sometimes // includes an error token ('error'). If it does, the error token is diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index f52220a90c..6c9105164a 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -44,7 +44,7 @@ using namespace jtx; * Test for TMGetObjectByHash reply size limiting. * * This verifies the fix that limits TMGetObjectByHash replies to - * Tuning::hardMaxReplyNodes to prevent excessive memory usage and + * tuning::hardMaxReplyNodes to prevent excessive memory usage and * potential DoS attacks from peers requesting large numbers of objects. */ class TMGetObjectByHash_test : public beast::unit_test::Suite @@ -61,11 +61,11 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite public: PeerTest( Application& app, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay) : PeerImp( @@ -133,8 +133,8 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite auto streamPtr = std::make_unique(socket_type(env.app().getIOContext()), *context_); - beast::IP::Endpoint const local(boost::asio::ip::make_address("172.1.1.1"), 51235); - beast::IP::Endpoint const remote(boost::asio::ip::make_address("172.1.1.2"), 51235); + beast::ip::Endpoint const local(boost::asio::ip::make_address("172.1.1.1"), 51235); + beast::ip::Endpoint const remote(boost::asio::ip::make_address("172.1.1.2"), 51235); PublicKey const key(std::get<0>(randomKeyPair(KeyType::Ed25519))); auto consumer = overlay.resourceManager().newInboundEndpoint(remote); @@ -227,7 +227,7 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite void run() override { - int const limit = static_cast(Tuning::kHardMaxReplyNodes); + int const limit = static_cast(tuning::kHardMaxReplyNodes); testReplyLimit(limit + 1, limit); testReplyLimit(limit, limit); testReplyLimit(limit - 1, limit - 1); diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp index 60cc69a14f..a583a3aeab 100644 --- a/src/test/overlay/compression_test.cpp +++ b/src/test/overlay/compression_test.cpp @@ -413,7 +413,7 @@ public: return env; }; auto handshake = [&](int outboundEnable, int inboundEnable) { - beast::IP::Address const addr = boost::asio::ip::make_address("172.1.1.100"); + beast::ip::Address const addr = boost::asio::ip::make_address("172.1.1.100"); auto env = getEnv(outboundEnable); auto request = xrpl::makeRequest( diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 2f42313037..77920007de 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -92,13 +92,13 @@ public: send(std::shared_ptr const& m) override { } - [[nodiscard]] beast::IP::Endpoint + [[nodiscard]] beast::ip::Endpoint getRemoteAddress() const override { return {}; } void - charge(Resource::Charge const& fee, std::string const& context = {}) override + charge(resource::Charge const& fee, std::string const& context = {}) override { } [[nodiscard]] bool @@ -1610,7 +1610,7 @@ vp_base_squelch_max_selected_peers=2 env_.app().config().compression = c.compression; }; auto handshake = [&](int outboundEnable, int inboundEnable) { - beast::IP::Address const addr = boost::asio::ip::make_address("172.1.1.100"); + beast::ip::Address const addr = boost::asio::ip::make_address("172.1.1.100"); setEnv(outboundEnable); auto request = xrpl::makeRequest( diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index 43f6ef2506..8626d3e19c 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -121,11 +121,11 @@ private: public: PeerTest( Application& app, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay) : PeerImp( @@ -192,9 +192,9 @@ private: auto streamPtr = std::make_unique( socket_type(std::forward(env.app().getIOContext())), *context_); - beast::IP::Endpoint const local( + beast::ip::Endpoint const local( boost::asio::ip::make_address("172.1.1." + std::to_string(lid_))); - beast::IP::Endpoint const remote( + beast::ip::Endpoint const remote( boost::asio::ip::make_address("172.1.1." + std::to_string(rid_))); PublicKey const key(std::get<0>(randomKeyPair(KeyType::Ed25519))); auto consumer = overlay.resourceManager().newInboundEndpoint(remote); diff --git a/src/test/protocol/BuildInfo_test.cpp b/src/test/protocol/BuildInfo_test.cpp index 1741f45938..a669e3e292 100644 --- a/src/test/protocol/BuildInfo_test.cpp +++ b/src/test/protocol/BuildInfo_test.cpp @@ -11,7 +11,7 @@ public: { testcase("EncodeSoftwareVersion"); - auto encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b7"); + auto encodedVersion = build_info::encodeSoftwareVersion("1.2.3-b7"); // the first two bytes identify the particular implementation, 0x183B BEAST_EXPECT((encodedVersion & 0xFFFF'0000'0000'0000LLU) == 0x183B'0000'0000'0000LLU); @@ -25,15 +25,15 @@ public: // 01 if a beta BEAST_EXPECT((encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b01); // 10 if an RC - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.4-rc7"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.4-rc7"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b10); // 11 if neither an RC nor a beta - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.5"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.5"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00C0'0000LLU) >> 22 == 0b11); } // the next six bits: rc/beta number (1-63) - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.6-b63"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.6-b63"); BEAST_EXPECT((encodedVersion & 0x0000'0000'003F'0000LLU) >> 16 == 63); // the last two bytes are zeros @@ -41,14 +41,14 @@ public: // Test some version strings with wrong formats: // no rc/beta number - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.3-b"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00FF'0000LLU) == 0); // rc/beta number out of range - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.3-b64"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.3-b64"); BEAST_EXPECT((encodedVersion & 0x0000'0000'00FF'0000LLU) == 0); // Check that the rc/beta number of a release is 0: - encodedVersion = BuildInfo::encodeSoftwareVersion("1.2.6"); + encodedVersion = build_info::encodeSoftwareVersion("1.2.6"); BEAST_EXPECT((encodedVersion & 0x0000'0000'003F'0000LLU) == 0); } @@ -57,9 +57,9 @@ public: { testcase("IsXrpldVersion"); auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; - BEAST_EXPECT(!BuildInfo::isXrpldVersion(vFF)); + BEAST_EXPECT(!build_info::isXrpldVersion(vFF)); auto vXrpld = 0x183B'0000'0000'0000LLU; - BEAST_EXPECT(BuildInfo::isXrpldVersion(vXrpld)); + BEAST_EXPECT(build_info::isXrpldVersion(vXrpld)); } void @@ -67,16 +67,16 @@ public: { testcase("IsNewerVersion"); auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; - BEAST_EXPECT(!BuildInfo::isNewerVersion(vFF)); + BEAST_EXPECT(!build_info::isNewerVersion(vFF)); - auto v159 = BuildInfo::encodeSoftwareVersion("1.5.9"); - BEAST_EXPECT(!BuildInfo::isNewerVersion(v159)); + auto v159 = build_info::encodeSoftwareVersion("1.5.9"); + BEAST_EXPECT(!build_info::isNewerVersion(v159)); - auto vCurrent = BuildInfo::getEncodedVersion(); - BEAST_EXPECT(!BuildInfo::isNewerVersion(vCurrent)); + auto vCurrent = build_info::getEncodedVersion(); + BEAST_EXPECT(!build_info::isNewerVersion(vCurrent)); - auto vMax = BuildInfo::encodeSoftwareVersion("255.255.255"); - BEAST_EXPECT(BuildInfo::isNewerVersion(vMax)); + auto vMax = build_info::encodeSoftwareVersion("255.255.255"); + BEAST_EXPECT(build_info::isNewerVersion(vMax)); } void diff --git a/src/test/protocol/InnerObjectFormats_test.cpp b/src/test/protocol/InnerObjectFormats_test.cpp index 5154153ecf..73a283da39 100644 --- a/src/test/protocol/InnerObjectFormats_test.cpp +++ b/src/test/protocol/InnerObjectFormats_test.cpp @@ -5,7 +5,7 @@ #include #include // json::Reader #include -#include // RPC::containsError +#include // rpc::containsError #include // STParsedJSONObject #include @@ -13,7 +13,7 @@ namespace xrpl { -namespace InnerObjectFormatsUnitTestDetail { +namespace inner_object_formats_unit_test_detail { struct TestJSONTxt { @@ -149,7 +149,7 @@ static TestJSONTxt const kTestArray[] = { }; -} // namespace InnerObjectFormatsUnitTestDetail +} // namespace inner_object_formats_unit_test_detail class InnerObjectFormatsParsedJSON_test : public beast::unit_test::Suite { @@ -157,7 +157,7 @@ public: void run() override { - using namespace InnerObjectFormatsUnitTestDetail; + using namespace inner_object_formats_unit_test_detail; // Instantiate a jtx::Env so debugLog writes are exercised. test::jtx::Env const env(*this); @@ -166,7 +166,7 @@ public: { json::Value req; json::Reader().parse(test.txt, req); - if (RPC::containsError(req)) + if (rpc::containsError(req)) { Throw( "Internal InnerObjectFormatsParsedJSON error. Bad JSON."); diff --git a/src/test/protocol/MultiApiJson_test.cpp b/src/test/protocol/MultiApiJson_test.cpp index c6f844a206..2f0d4cb3ec 100644 --- a/src/test/protocol/MultiApiJson_test.cpp +++ b/src/test/protocol/MultiApiJson_test.cpp @@ -62,35 +62,35 @@ struct MultiApiJson_test : beast::unit_test::Suite // Some static data for test inputs static int const kPrimes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97}; - static_assert(std::size(kPrimes) > RPC::kApiMaximumValidVersion); + static_assert(std::size(kPrimes) > rpc::kApiMaximumValidVersion); MultiApiJson<1, 3> s1{}; static_assert( - s1.kSize == RPC::kApiMaximumValidVersion + 1 - RPC::kApiMinimumSupportedVersion); + s1.kSize == rpc::kApiMaximumValidVersion + 1 - rpc::kApiMinimumSupportedVersion); int productAllVersions = 1; - for (unsigned i = RPC::kApiMinimumSupportedVersion; i <= RPC::kApiMaximumValidVersion; + for (unsigned i = rpc::kApiMinimumSupportedVersion; i <= rpc::kApiMaximumValidVersion; ++i) { - auto const index = i - RPC::kApiMinimumSupportedVersion; + auto const index = i - rpc::kApiMinimumSupportedVersion; BEAST_EXPECT(index == s1.index(i)); BEAST_EXPECT(s1.valid(i)); s1.val[index] = makeJson("value", kPrimes[i]); productAllVersions *= kPrimes[i]; } BEAST_EXPECT(!s1.valid(0)); - BEAST_EXPECT(!s1.valid(RPC::kApiMaximumValidVersion + 1)); + BEAST_EXPECT(!s1.valid(rpc::kApiMaximumValidVersion + 1)); BEAST_EXPECT(!s1.valid( - std::numeric_limits::max())); + std::numeric_limits::max())); int result = 1; - static_assert(RPC::kApiMinimumSupportedVersion + 1 <= RPC::kApiMaximumValidVersion); - forApiVersions( + static_assert(rpc::kApiMinimumSupportedVersion + 1 <= rpc::kApiMaximumValidVersion); + forApiVersions( std::as_const(s1).visit(), [this](json::Value const& json, unsigned int version, int* result) { BEAST_EXPECT( - version >= RPC::kApiMinimumSupportedVersion && - version <= RPC::kApiMinimumSupportedVersion + 1); + version >= rpc::kApiMinimumSupportedVersion && + version <= rpc::kApiMinimumSupportedVersion + 1); if (BEAST_EXPECT(json.isMember("value"))) { *result *= json["value"].asInt(); @@ -99,8 +99,8 @@ struct MultiApiJson_test : beast::unit_test::Suite &result); BEAST_EXPECT( result == - kPrimes[RPC::kApiMinimumSupportedVersion] * - kPrimes[RPC::kApiMinimumSupportedVersion + 1]); + kPrimes[rpc::kApiMinimumSupportedVersion] * + kPrimes[rpc::kApiMinimumSupportedVersion + 1]); // Check all the values with mutable data forAllApiVersions(s1.visit(), [&s1, this](json::Value& json, auto version) { @@ -116,8 +116,8 @@ struct MultiApiJson_test : beast::unit_test::Suite std::as_const(s1).visit(), [this](json::Value const& json, unsigned int version, int* result) { BEAST_EXPECT( - version >= RPC::kApiMinimumSupportedVersion && - version <= RPC::kApiMaximumValidVersion); + version >= rpc::kApiMinimumSupportedVersion && + version <= rpc::kApiMaximumValidVersion); if (BEAST_EXPECT(json.isMember("value"))) { *result *= json["value"].asInt(); diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 8d55c5e19d..cb20de9bf5 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -34,7 +34,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class AccountLines_test : public beast::unit_test::Suite { @@ -51,7 +51,7 @@ public: auto const lines = env.rpc("json", "account_lines", "{ }"); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::missingFieldError(jss::account)[jss::error_message]); + rpc::missingFieldError(jss::account)[jss::error_message]); } { // account_lines with a malformed account. @@ -60,7 +60,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); } { // test account non-string @@ -87,7 +87,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::makeError(RpcActNotFound)[jss::error_message]); + rpc::makeError(RpcActNotFound)[jss::error_message]); } env.fund(XRP(10000), alice); env.close(); @@ -250,7 +250,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); } { // A negative limit should fail. @@ -260,7 +260,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); } { // Limit the response to 1 trust line. @@ -297,7 +297,7 @@ public: auto const linesD = env.rpc("json", "account_lines", to_string(paramsD)); BEAST_EXPECT( linesD[jss::result][jss::error_message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); } { // A non-string marker should also fail. @@ -307,7 +307,7 @@ public: auto const lines = env.rpc("json", "account_lines", to_string(params)); BEAST_EXPECT( lines[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::marker, "string")); + rpc::expectedFieldMessage(jss::marker, "string")); } { // Check that the flags we expect from alice to gw2 are present. @@ -496,7 +496,7 @@ public: auto const linesEnd = env.rpc("json", "account_lines", to_string(linesEndParams)); BEAST_EXPECT( linesEnd[jss::result][jss::error_message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); } void @@ -728,7 +728,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::missingFieldError(jss::account)[jss::error_message]); + rpc::missingFieldError(jss::account)[jss::error_message]); 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); @@ -746,7 +746,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); 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); @@ -765,7 +765,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::makeError(RpcActNotFound)[jss::error_message]); + rpc::makeError(RpcActNotFound)[jss::error_message]); 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); @@ -998,7 +998,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::makeError(RpcActMalformed)[jss::error_message]); + rpc::makeError(RpcActMalformed)[jss::error_message]); 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); @@ -1017,7 +1017,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); 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); @@ -1090,7 +1090,7 @@ public: auto const linesD = env.rpc("json2", to_string(requestD)); BEAST_EXPECT( linesD[jss::error][jss::message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); BEAST_EXPECT(linesD.isMember(jss::jsonrpc) && linesD[jss::jsonrpc] == "2.0"); BEAST_EXPECT(linesD.isMember(jss::ripplerpc) && linesD[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesD.isMember(jss::id) && linesD[jss::id] == 5); @@ -1109,7 +1109,7 @@ public: auto const lines = env.rpc("json2", to_string(request)); BEAST_EXPECT( lines[jss::error][jss::message] == - RPC::expectedFieldMessage(jss::marker, "string")); + rpc::expectedFieldMessage(jss::marker, "string")); 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); @@ -1266,7 +1266,7 @@ public: auto const linesEnd = env.rpc("json2", to_string(linesEndRequest)); BEAST_EXPECT( linesEnd[jss::error][jss::message] == - RPC::makeError(RpcInvalidParams)[jss::error_message]); + rpc::makeError(RpcInvalidParams)[jss::error_message]); BEAST_EXPECT(linesEnd.isMember(jss::jsonrpc) && linesEnd[jss::jsonrpc] == "2.0"); BEAST_EXPECT(linesEnd.isMember(jss::ripplerpc) && linesEnd[jss::ripplerpc] == "2.0"); BEAST_EXPECT(linesEnd.isMember(jss::id) && linesEnd[jss::id] == 5); @@ -1286,4 +1286,4 @@ public: BEAST_DEFINE_TESTSUITE(AccountLines, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index f1fbc2871b..b144a6dc84 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -199,7 +199,7 @@ class AccountTx_test : public beast::unit_test::Suite auto isErr = [](json::Value const& j, ErrorCodeI code) { return j.isMember(jss::result) && j[jss::result].isMember(jss::error) && - j[jss::result][jss::error] == RPC::getErrorInfo(code).token; + j[jss::result][jss::error] == rpc::getErrorInfo(code).token; }; json::Value jParams; @@ -425,56 +425,56 @@ class AccountTx_test : public beast::unit_test::Suite p[jss::limit] = 1.2; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = "10" should fail (string instead of integer) p[jss::limit] = "10"; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = true should fail (boolean instead of integer) p[jss::limit] = true; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = false should fail (boolean instead of integer) p[jss::limit] = false; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = -1 should fail (negative number) p[jss::limit] = -1; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = [] should fail (array instead of integer) p[jss::limit] = json::Value(json::ValueType::Array); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = {} should fail (object instead of integer) p[jss::limit] = json::Value(json::ValueType::Object); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = "malformed" should fail (malformed string) p[jss::limit] = "malformed"; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = ["limit"] should fail (array with string) p[jss::limit] = json::Value(json::ValueType::Array); p[jss::limit].append("limit"); BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = {"limit": 10} should fail (object with // property) @@ -482,7 +482,7 @@ class AccountTx_test : public beast::unit_test::Suite p[jss::limit][jss::limit] = 10; BEAST_EXPECT( env.rpc("json", "account_tx", to_string(p))[jss::result][jss::error_message] == - RPC::expectedFieldMessage(jss::limit, "unsigned integer")); + rpc::expectedFieldMessage(jss::limit, "unsigned integer")); // Test case: limit = 10 should succeed (valid integer) p[jss::limit] = 10; diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 83f7b64b4b..646cf190ff 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -1548,7 +1548,7 @@ public: auto usd = gw["USD"]; - for (auto i = 0; i <= RPC::Tuning::kBookOffers.rmax; i++) + for (auto i = 0; i <= rpc::tuning::kBookOffers.rmax; i++) env(offer(gw, XRP(50 + (1 * i)), usd(1.0 + (0.1 * i)))); if (asAdmin) @@ -1565,15 +1565,15 @@ public: BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? 1u : 0u)); // NOTE - a marker field is not returned for this method - jvParams[jss::limit] = RPC::Tuning::kBookOffers.rmax + 1; + jvParams[jss::limit] = rpc::tuning::kBookOffers.rmax + 1; jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::offers].isArray()); - BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? RPC::Tuning::kBookOffers.rmax + 1 : 0u)); + BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? rpc::tuning::kBookOffers.rmax + 1 : 0u)); jvParams[jss::limit] = json::ValueType::Null; jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; BEAST_EXPECT(jrr[jss::offers].isArray()); - BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? RPC::Tuning::kBookOffers.rDefault : 0u)); + BEAST_EXPECT(jrr[jss::offers].size() == (asAdmin ? rpc::tuning::kBookOffers.rDefault : 0u)); } void diff --git a/src/test/rpc/Handler_test.cpp b/src/test/rpc/Handler_test.cpp index e900b92fc3..be78864cac 100644 --- a/src/test/rpc/Handler_test.cpp +++ b/src/test/rpc/Handler_test.cpp @@ -88,7 +88,7 @@ class Handler_test : public beast::unit_test::Suite std::random_device dev; std::ranlux48 prng(dev()); - std::vector names = test::jtx::makeVector(xrpl::RPC::getHandlerNames()); + std::vector names = test::jtx::makeVector(xrpl::rpc::getHandlerNames()); std::uniform_int_distribution distr{0, names.size() - 1}; @@ -96,7 +96,7 @@ class Handler_test : public beast::unit_test::Suite auto const [mean, stdev, n] = time( 1'000'000, [&](std::size_t i) { - auto const d = RPC::getHandler(1, false, names[i]); + auto const d = rpc::getHandler(1, false, names[i]); dummy = dummy + i + (int)d->role; }, [&]() -> std::size_t { return distr(prng); }); diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index e18974e7e7..efba4075c7 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -36,7 +36,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { struct TxnTestData { @@ -2248,7 +2248,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == baseFee); } @@ -2268,7 +2268,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == baseFee); } @@ -2285,7 +2285,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2306,7 +2306,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2325,7 +2325,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2344,7 +2344,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2400,7 +2400,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT(req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 10); } @@ -2422,7 +2422,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT(req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 10); } @@ -2450,7 +2450,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 8889); } @@ -2473,7 +2473,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2496,7 +2496,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); BEAST_EXPECT(!req[jss::tx_json].isMember(jss::Fee)); } @@ -2519,7 +2519,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( req[jss::tx_json].isMember(jss::Fee) && req[jss::tx_json][jss::Fee] == 8889); } @@ -2542,7 +2542,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); } { @@ -2563,7 +2563,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); } { @@ -2585,7 +2585,7 @@ public: env.app().getTxQ(), env.app()); - BEAST_EXPECT(RPC::containsError(result)); + BEAST_EXPECT(rpc::containsError(result)); } env.close(); @@ -2598,7 +2598,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "10"); BEAST_EXPECT( @@ -2624,7 +2624,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "7813"); @@ -2651,7 +2651,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "47"); BEAST_EXPECT( @@ -2682,7 +2682,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::Fee) && result[jss::tx_json][jss::Fee] == "6806"); @@ -2711,7 +2711,7 @@ public: auto rpcResult = env.rpc("json", "sign", to_string(toSign)); auto result = rpcResult[jss::result]; - BEAST_EXPECT(!RPC::containsError(result)); + BEAST_EXPECT(!rpc::containsError(result)); BEAST_EXPECT( result[jss::tx_json].isMember(jss::NetworkID) && result[jss::tx_json][jss::NetworkID] == 1025); @@ -2791,7 +2791,7 @@ public: { json::Value req; json::Reader().parse(txnTest.json, req); - if (RPC::containsError(req)) + if (rpc::containsError(req)) Throw("Internal JSONRPC_test error. Bad test JSON."); static Role const kTestedRoles[] = { @@ -2815,7 +2815,7 @@ public: } std::string errStr; - if (RPC::containsError(result)) + if (rpc::containsError(result)) errStr = result["error_message"].asString(); if (errStr == txnTest.expMsg[get<3>(testFunc)]) @@ -2848,4 +2848,4 @@ public: BEAST_DEFINE_TESTSUITE(JSONRPC, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/KeyGeneration_test.cpp b/src/test/rpc/KeyGeneration_test.cpp index aafe6f75a5..2b056fc6d2 100644 --- a/src/test/rpc/KeyGeneration_test.cpp +++ b/src/test/rpc/KeyGeneration_test.cpp @@ -15,7 +15,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { struct KeyStrings { @@ -800,4 +800,4 @@ public: BEAST_DEFINE_TESTSUITE(WalletPropose, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 7adb5a4518..b8301d5656 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -349,7 +349,7 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc( apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result]; auto const expectedErrMsg = - RPC::expectedFieldMessage(fieldName, getTypeName(typeID)); + rpc::expectedFieldMessage(fieldName, getTypeName(typeID)); checkErrorValue(jrr, expectedError, expectedErrMsg, location); }; @@ -383,13 +383,13 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc( apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result]; checkErrorValue( - jrr, "malformedRequest", RPC::missingFieldMessage(fieldName.cStr()), location); + jrr, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location); correctRequest[parentFieldName][fieldName] = json::ValueType::Null; json::Value const jrr2 = env.rpc( apiVersion, "json", "ledger_entry", to_string(correctRequest))[jss::result]; checkErrorValue( - jrr2, "malformedRequest", RPC::missingFieldMessage(fieldName.cStr()), location); + jrr2, "malformedRequest", rpc::missingFieldMessage(fieldName.cStr()), location); } auto tryField = [&](json::Value fieldValue) -> void { correctRequest[parentFieldName][fieldName] = fieldValue; @@ -399,7 +399,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr, expectedError, - RPC::expectedFieldMessage(fieldName, getTypeName(typeID)), + rpc::expectedFieldMessage(fieldName, getTypeName(typeID)), location); }; @@ -1083,8 +1083,8 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; auto const expectedErrMsg = fieldValue.isNull() - ? RPC::missingFieldMessage(jss::issuer.cStr()) - : RPC::expectedFieldMessage(jss::issuer, "AccountID"); + ? rpc::missingFieldMessage(jss::issuer.cStr()) + : rpc::expectedFieldMessage(jss::issuer, "AccountID"); checkErrorValue(jrr, "malformedAuthorizedCredentials", expectedErrMsg); }; @@ -1114,7 +1114,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr[jss::result], "malformedAuthorizedCredentials", - RPC::expectedFieldMessage(jss::authorized_credentials, "array")); + rpc::expectedFieldMessage(jss::authorized_credentials, "array")); } { @@ -1134,8 +1134,8 @@ class LedgerEntry_test : public beast::unit_test::Suite json::Value const jrr = env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; auto const expectedErrMsg = fieldValue.isNull() - ? RPC::missingFieldMessage(jss::credential_type.cStr()) - : RPC::expectedFieldMessage(jss::credential_type, "hex string"); + ? rpc::missingFieldMessage(jss::credential_type.cStr()) + : rpc::expectedFieldMessage(jss::credential_type, "hex string"); checkErrorValue(jrr, "malformedAuthorizedCredentials", expectedErrMsg); }; @@ -1836,7 +1836,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr, "malformedAddress", - RPC::expectedFieldMessage(jss::accounts, "array of Accounts")); + rpc::expectedFieldMessage(jss::accounts, "array of Accounts")); } { @@ -1851,7 +1851,7 @@ class LedgerEntry_test : public beast::unit_test::Suite checkErrorValue( jrr, "malformedAddress", - RPC::expectedFieldMessage(jss::accounts, "array of Accounts")); + rpc::expectedFieldMessage(jss::accounts, "array of Accounts")); } }; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 3a2c957691..af93108ff2 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -158,7 +158,7 @@ class LedgerRPC_test : public beast::unit_test::Suite { // Request a ledger with a very large (double) sequence. auto const ret = env.rpc("json", "ledger", "{ \"ledger_index\" : 2e15 }"); - BEAST_EXPECT(RPC::containsError(ret)); + BEAST_EXPECT(rpc::containsError(ret)); BEAST_EXPECT(ret[jss::error_message] == "Invalid parameters."); } diff --git a/src/test/rpc/LedgerRequest_test.cpp b/src/test/rpc/LedgerRequest_test.cpp index 93feee9497..98bde4e5a5 100644 --- a/src/test/rpc/LedgerRequest_test.cpp +++ b/src/test/rpc/LedgerRequest_test.cpp @@ -15,7 +15,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class LedgerRequest_test : public beast::unit_test::Suite { @@ -43,28 +43,28 @@ public: // arbitrary text is converted to 0. auto const result = env.rpc("ledger_request", "arbitrary_text"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too small"); } { auto const result = env.rpc("ledger_request", "-1"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too small"); } { auto const result = env.rpc("ledger_request", "0"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too small"); } { auto const result = env.rpc("ledger_request", "1"); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::ledger_index] == 1 && result[jss::result].isMember(jss::ledger)); BEAST_EXPECT( @@ -75,7 +75,7 @@ public: { auto const result = env.rpc("ledger_request", "2"); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::ledger_index] == 2 && result[jss::result].isMember(jss::ledger)); BEAST_EXPECT( @@ -86,7 +86,7 @@ public: { auto const result = env.rpc("ledger_request", "3"); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::ledger_index] == 3 && result[jss::result].isMember(jss::ledger)); BEAST_EXPECT( @@ -98,7 +98,7 @@ public: { auto const r = env.rpc("ledger_request", ledgerHash); BEAST_EXPECT( - !RPC::containsError(r[jss::result]) && r[jss::result][jss::ledger_index] == 3 && + !rpc::containsError(r[jss::result]) && r[jss::result][jss::ledger_index] == 3 && r[jss::result].isMember(jss::ledger)); BEAST_EXPECT( r[jss::result][jss::ledger].isMember(jss::ledger_hash) && @@ -112,7 +112,7 @@ public: auto const result = env.rpc("ledger_request", ledgerHash); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Invalid field 'ledger_hash', not hex string."); } @@ -123,21 +123,21 @@ public: auto const result = env.rpc("ledger_request", ledgerHash); BEAST_EXPECT( - !RPC::containsError(result[jss::result]) && + !rpc::containsError(result[jss::result]) && result[jss::result][jss::have_header] == false); } { auto const result = env.rpc("ledger_request", "4"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too large"); } { auto const result = env.rpc("ledger_request", "5"); BEAST_EXPECT( - RPC::containsError(result[jss::result]) && + rpc::containsError(result[jss::result]) && result[jss::result][jss::error_message] == "Ledger index too large"); } } @@ -357,4 +357,4 @@ public: BEAST_DEFINE_TESTSUITE(LedgerRequest, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 9c17d291a1..6e30f944c7 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -286,9 +286,9 @@ class NoRippleCheckLimits_test : public beast::unit_test::Suite // be better if we could add this functionality to Env somehow // or otherwise disable endpoint charging for certain test // cases. - using namespace xrpl::Resource; + using namespace xrpl::resource; using namespace std::chrono; - using namespace beast::IP; + using namespace beast::ip; auto c = env.app().getResourceManager().newInboundEndpoint( Endpoint::fromString(test::getEnvLocalhostAddr())); @@ -301,7 +301,7 @@ class NoRippleCheckLimits_test : public beast::unit_test::Suite } }; - for (auto i = 0; i < xrpl::RPC::Tuning::kNoRippleCheck.rmax + 5; ++i) + for (auto i = 0; i < xrpl::rpc::tuning::kNoRippleCheck.rmax + 5; ++i) { if (!admin) checkBalance(); diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index 4b5ab1f230..ef3213008c 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -5855,8 +5855,8 @@ public: { testcase << "RPCCall API version " << apiVersion; if (!BEAST_EXPECT( - apiVersion >= RPC::kApiMinimumSupportedVersion && - apiVersion <= RPC::kApiMaximumValidVersion)) + apiVersion >= rpc::kApiMinimumSupportedVersion && + apiVersion <= rpc::kApiMaximumValidVersion)) return; test::jtx::Env const env(*this, makeNetworkConfig(11111)); // Used only for its Journal. @@ -5870,8 +5870,8 @@ public: std::vector const args{rpcCallTest.args.begin(), rpcCallTest.args.end()}; char const* const expVersioned = - (apiVersion - RPC::kApiMinimumSupportedVersion) < rpcCallTest.exp.size() - ? rpcCallTest.exp[apiVersion - RPC::kApiMinimumSupportedVersion] + (apiVersion - rpc::kApiMinimumSupportedVersion) < rpcCallTest.exp.size() + ? rpcCallTest.exp[apiVersion - rpc::kApiMinimumSupportedVersion] : rpcCallTest.exp.back(); // Note that, over the long term, kNone of these tests should diff --git a/src/test/rpc/RPCHelpers_test.cpp b/src/test/rpc/RPCHelpers_test.cpp index 1458c0aa80..25368235a3 100644 --- a/src/test/rpc/RPCHelpers_test.cpp +++ b/src/test/rpc/RPCHelpers_test.cpp @@ -19,50 +19,50 @@ public: // Test no type. json::Value tx = json::ValueType::Object; - auto result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + auto result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == 0); // Test empty type. tx[jss::type] = ""; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); // Test type using canonical name in mixedcase. tx[jss::type] = "MPTokenIssuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE); // Test type using canonical name in lowercase. tx[jss::type] = "mptokenissuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE); // Test type using RPC name with exact match. tx[jss::type] = "mpt_issuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status::kOK); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status::kOK); BEAST_EXPECT(result.second == ltMPTOKEN_ISSUANCE); // Test type using RPC name with inexact match. tx[jss::type] = "MPT_Issuance"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); // Test invalid type. tx[jss::type] = 1234; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); // Test unknown type. tx[jss::type] = "unknown"; - result = RPC::chooseLedgerEntryType(tx); - BEAST_EXPECT(result.first == RPC::Status{RpcInvalidParams}); + result = rpc::chooseLedgerEntryType(tx); + BEAST_EXPECT(result.first == rpc::Status{RpcInvalidParams}); BEAST_EXPECT(result.second == 0); } diff --git a/src/test/rpc/Status_test.cpp b/src/test/rpc/Status_test.cpp index c4f8544980..aaf696e9af 100644 --- a/src/test/rpc/Status_test.cpp +++ b/src/test/rpc/Status_test.cpp @@ -12,7 +12,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class codeString_test : public beast::unit_test::Suite { @@ -202,6 +202,6 @@ public: } }; -BEAST_DEFINE_TESTSUITE(fillJson, rpc, RPC); +BEAST_DEFINE_TESTSUITE(fillJson, rpc, xrpl); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/test/rpc/TransactionEntry_test.cpp b/src/test/rpc/TransactionEntry_test.cpp index 38a95e84f8..b57c615b71 100644 --- a/src/test/rpc/TransactionEntry_test.cpp +++ b/src/test/rpc/TransactionEntry_test.cpp @@ -183,7 +183,7 @@ class TransactionEntry_test : public beast::unit_test::Suite { json::Value expected; json::Reader().parse(expectedJson, expected); - if (RPC::containsError(expected)) + if (rpc::containsError(expected)) Throw("Internal JSONRPC_test error. Bad test JSON."); for (auto memberIt = expected.begin(); memberIt != expected.end(); memberIt++) diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index 4dae475b63..2921c63c17 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -62,9 +62,9 @@ class Transaction_test : public beast::unit_test::Suite char const* command = jss::tx.cStr(); char const* binary = jss::binary.cStr(); - char const* notFound = RPC::getErrorInfo(RpcTxnNotFound).token; - char const* invalid = RPC::getErrorInfo(RpcInvalidLgrRange).token; - char const* excessive = RPC::getErrorInfo(RpcExcessiveLgrRange).token; + char const* notFound = rpc::getErrorInfo(RpcTxnNotFound).token; + char const* invalid = rpc::getErrorInfo(RpcInvalidLgrRange).token; + char const* excessive = rpc::getErrorInfo(RpcExcessiveLgrRange).token; Env env{*this, features}; auto const alice = Account("alice"); @@ -301,9 +301,9 @@ class Transaction_test : public beast::unit_test::Suite char const* command = jss::tx.cStr(); char const* binary = jss::binary.cStr(); - char const* notFound = RPC::getErrorInfo(RpcTxnNotFound).token; - char const* invalid = RPC::getErrorInfo(RpcInvalidLgrRange).token; - char const* excessive = RPC::getErrorInfo(RpcExcessiveLgrRange).token; + char const* notFound = rpc::getErrorInfo(RpcTxnNotFound).token; + char const* invalid = rpc::getErrorInfo(RpcInvalidLgrRange).token; + char const* excessive = rpc::getErrorInfo(RpcExcessiveLgrRange).token; Env env{*this, makeNetworkConfig(11111)}; uint32_t const netID = env.app().getNetworkIDService().getNetworkID(); @@ -333,7 +333,7 @@ class Transaction_test : public beast::unit_test::Suite auto const result = env.rpc( command, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *RPC::encodeCTID(startLegSeq + i, txnIdx, netID), + *rpc::encodeCTID(startLegSeq + i, txnIdx, netID), binary, to_string(startLegSeq), to_string(endLegSeq)); @@ -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->getSeqValue(), netID); for (int deltaEndSeq = 0; deltaEndSeq < 2; ++deltaEndSeq) { auto const result = env.rpc( @@ -374,7 +374,7 @@ class Transaction_test : public beast::unit_test::Suite auto const result = env.rpc( command, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *RPC::encodeCTID(startLegSeq + i, txnIdx, netID), + *rpc::encodeCTID(startLegSeq + i, txnIdx, netID), binary, to_string(endLegSeq + 1), to_string(endLegSeq + 100)); @@ -434,7 +434,7 @@ class Transaction_test : public beast::unit_test::Suite auto const result = env.rpc( command, // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - *RPC::encodeCTID(endLegSeq, txnIdx, netID), + *rpc::encodeCTID(endLegSeq, txnIdx, netID), to_string(startLegSeq), to_string(deletedLedger - 1)); @@ -527,75 +527,75 @@ class Transaction_test : public beast::unit_test::Suite // Test case 1: Valid input values auto const expected11 = std::optional("CFFFFFFFFFFFFFFF"); - BEAST_EXPECT(RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU) == expected11); + BEAST_EXPECT(rpc::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU) == expected11); auto const expected12 = std::optional("C000000000000000"); - BEAST_EXPECT(RPC::encodeCTID(0, 0, 0) == expected12); + BEAST_EXPECT(rpc::encodeCTID(0, 0, 0) == expected12); auto const expected13 = std::optional("C000000100020003"); - BEAST_EXPECT(RPC::encodeCTID(1U, 2U, 3U) == expected13); + BEAST_EXPECT(rpc::encodeCTID(1U, 2U, 3U) == expected13); auto const expected14 = std::optional("C0CA2AA7326FFFFF"); - BEAST_EXPECT(RPC::encodeCTID(13249191UL, 12911U, 65535U) == expected14); + BEAST_EXPECT(rpc::encodeCTID(13249191UL, 12911U, 65535U) == expected14); // Test case 2: ledger_seq greater than 0xFFFFFFF - BEAST_EXPECT(!RPC::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU)); + BEAST_EXPECT(!rpc::encodeCTID(0x1000'0000UL, 0xFFFFU, 0xFFFFU)); // Test case 3: txn_index greater than 0xFFFF - BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF)); + BEAST_EXPECT(!rpc::encodeCTID(0x0FFF'FFFF, 0x1'0000, 0xFFFF)); // Test case 4: network_id greater than 0xFFFF - BEAST_EXPECT(!RPC::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U)); + BEAST_EXPECT(!rpc::encodeCTID(0x0FFF'FFFFUL, 0xFFFFU, 0x1'0000U)); // Test case 5: Valid input values auto const expected51 = std::optional>(std::make_tuple(0, 0, 0)); - BEAST_EXPECT(RPC::decodeCTID("C000000000000000") == expected51); + BEAST_EXPECT(rpc::decodeCTID("C000000000000000") == expected51); auto const expected52 = std::optional>(std::make_tuple(1U, 2U, 3U)); - BEAST_EXPECT(RPC::decodeCTID("C000000100020003") == expected52); + BEAST_EXPECT(rpc::decodeCTID("C000000100020003") == expected52); auto const expected53 = std::optional>( std::make_tuple(13249191UL, 12911U, 49221U)); - BEAST_EXPECT(RPC::decodeCTID("C0CA2AA7326FC045") == expected53); + BEAST_EXPECT(rpc::decodeCTID("C0CA2AA7326FC045") == expected53); // Test case 6: ctid not a string or big int - BEAST_EXPECT(!RPC::decodeCTID(0xCFF)); + BEAST_EXPECT(!rpc::decodeCTID(0xCFF)); // Test case 7: ctid not a hexadecimal string - BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFFG")); + BEAST_EXPECT(!rpc::decodeCTID("C003FFFFFFFFFFFG")); // Test case 8: ctid not exactly 16 nibbles - BEAST_EXPECT(!RPC::decodeCTID("C003FFFFFFFFFFF")); + BEAST_EXPECT(!rpc::decodeCTID("C003FFFFFFFFFFF")); // Test case 9: ctid too large to be a valid CTID value - BEAST_EXPECT(!RPC::decodeCTID("CFFFFFFFFFFFFFFFF")); + BEAST_EXPECT(!rpc::decodeCTID("CFFFFFFFFFFFFFFFF")); // Test case 10: ctid doesn't start with a C nibble - BEAST_EXPECT(!RPC::decodeCTID("FFFFFFFFFFFFFFFF")); + BEAST_EXPECT(!rpc::decodeCTID("FFFFFFFFFFFFFFFF")); // Test case 11: Valid input values BEAST_EXPECT( - (RPC::decodeCTID(0xCFFF'FFFF'FFFF'FFFFULL) == + (rpc::decodeCTID(0xCFFF'FFFF'FFFF'FFFFULL) == std::optional>( std::make_tuple(0x0FFF'FFFFUL, 0xFFFFU, 0xFFFFU)))); BEAST_EXPECT( - (RPC::decodeCTID(0xC000'0000'0000'0000ULL) == + (rpc::decodeCTID(0xC000'0000'0000'0000ULL) == std::optional>(std::make_tuple(0, 0, 0)))); BEAST_EXPECT( - (RPC::decodeCTID(0xC000'0001'0002'0003ULL) == + (rpc::decodeCTID(0xC000'0001'0002'0003ULL) == std::optional>(std::make_tuple(1U, 2U, 3U)))); BEAST_EXPECT( - (RPC::decodeCTID(0xC0CA'2AA7'326F'C045ULL) == + (rpc::decodeCTID(0xC0CA'2AA7'326F'C045ULL) == std::optional>( std::make_tuple(1324'9191UL, 12911U, 49221U)))); // Test case 12: ctid not exactly 16 nibbles - BEAST_EXPECT(!RPC::decodeCTID(0xC003'FFFF'FFFF'FFF)); + BEAST_EXPECT(!rpc::decodeCTID(0xC003'FFFF'FFFF'FFF)); // Test case 13: ctid too large to be a valid CTID value // this test case is not possible in c++ because it would overflow the // type, left in for completeness - // BEAST_EXPECT(!RPC::decodeCTID(0xCFFFFFFFFFFFFFFFFULL)); + // BEAST_EXPECT(!rpc::decodeCTID(0xCFFFFFFFFFFFFFFFFULL)); // Test case 14: ctid doesn't start with a C nibble - BEAST_EXPECT(!RPC::decodeCTID(0xFFFF'FFFF'FFFF'FFFFULL)); + BEAST_EXPECT(!rpc::decodeCTID(0xFFFF'FFFF'FFFF'FFFFULL)); } void @@ -619,7 +619,7 @@ class Transaction_test : public beast::unit_test::Suite env(pay(alice, bob, XRP(10))); env.close(); - auto const ctid = RPC::encodeCTID(startLegSeq, 0, netID); + auto const ctid = rpc::encodeCTID(startLegSeq, 0, netID); if (netID > 0xFFFF) { // Concise transaction IDs do not support a network ID > 0xFFFF. @@ -650,7 +650,7 @@ class Transaction_test : public beast::unit_test::Suite env.close(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - std::string const ctid = *RPC::encodeCTID(startLegSeq, 0, netID); + std::string const ctid = *rpc::encodeCTID(startLegSeq, 0, netID); auto isUpper = [](char c) { return std::isupper(c) != 0; }; // Verify that there are at least two upper case letters in ctid and @@ -705,7 +705,7 @@ class Transaction_test : public beast::unit_test::Suite BEAST_EXPECT(jrr.isMember(jss::ctid) == (netID <= 0xFFFF)); if (jrr.isMember(jss::ctid)) { - auto const ctid = RPC::encodeCTID(ledgerSeq, 0, netID); + auto const ctid = rpc::encodeCTID(ledgerSeq, 0, netID); BEAST_EXPECT( jrr[jss::ctid] == *ctid); // NOLINT(bugprone-unchecked-optional-access) } @@ -725,7 +725,7 @@ class Transaction_test : public beast::unit_test::Suite env.close(); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) - auto const ctid = *RPC::encodeCTID(startLegSeq, 0, netID + 1); + auto const ctid = *rpc::encodeCTID(startLegSeq, 0, netID + 1); json::Value jsonTx; jsonTx[jss::binary] = false; jsonTx[jss::ctid] = ctid; diff --git a/src/test/rpc/Version_test.cpp b/src/test/rpc/Version_test.cpp index 71830c2219..b5c1abc160 100644 --- a/src/test/rpc/Version_test.cpp +++ b/src/test/rpc/Version_test.cpp @@ -32,7 +32,7 @@ class Version_test : public beast::unit_test::Suite auto jrr = env.rpc( "json", "version", - "{\"api_version\": " + std::to_string(RPC::kApiMaximumSupportedVersion) + + "{\"api_version\": " + std::to_string(rpc::kApiMaximumSupportedVersion) + "}")[jss::result]; BEAST_EXPECT(isCorrectReply(jrr)); @@ -62,7 +62,7 @@ class Version_test : public beast::unit_test::Suite auto re = env.rpc( "json", "version", - "{\"api_version\": " + std::to_string(RPC::kApiMinimumSupportedVersion - 1) + "}"); + "{\"api_version\": " + std::to_string(rpc::kApiMinimumSupportedVersion - 1) + "}"); BEAST_EXPECT(badVersion(re)); BEAST_EXPECT(env.app().config().betaRpcApi); @@ -71,7 +71,7 @@ class Version_test : public beast::unit_test::Suite "version", "{\"api_version\": " + std::to_string( - std::max(RPC::kApiMaximumSupportedVersion.value, RPC::kApiBetaVersion.value) + + std::max(rpc::kApiMaximumSupportedVersion.value, rpc::kApiBetaVersion.value) + 1) + "}"); BEAST_EXPECT(badVersion(re)); @@ -86,38 +86,38 @@ class Version_test : public beast::unit_test::Suite testcase("test getAPIVersionNumber function"); unsigned int const versionIfUnspecified = - RPC::kApiVersionIfUnspecified < RPC::kApiMinimumSupportedVersion - ? RPC::kApiInvalidVersion - : RPC::kApiVersionIfUnspecified; + rpc::kApiVersionIfUnspecified < rpc::kApiMinimumSupportedVersion + ? rpc::kApiInvalidVersion + : rpc::kApiVersionIfUnspecified; json::Value const jArray = json::Value(json::ValueType::Array); json::Value const jNull = json::Value(json::ValueType::Null); - BEAST_EXPECT(RPC::getAPIVersionNumber(jArray, false) == versionIfUnspecified); - BEAST_EXPECT(RPC::getAPIVersionNumber(jNull, false) == versionIfUnspecified); + BEAST_EXPECT(rpc::getAPIVersionNumber(jArray, false) == versionIfUnspecified); + BEAST_EXPECT(rpc::getAPIVersionNumber(jNull, false) == versionIfUnspecified); json::Value jObject = json::Value(json::ValueType::Object); - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == versionIfUnspecified); - jObject[jss::api_version] = RPC::kApiVersionIfUnspecified.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == versionIfUnspecified); + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == versionIfUnspecified); + jObject[jss::api_version] = rpc::kApiVersionIfUnspecified.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == versionIfUnspecified); - jObject[jss::api_version] = RPC::kApiMinimumSupportedVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiMinimumSupportedVersion); - jObject[jss::api_version] = RPC::kApiMaximumSupportedVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiMaximumSupportedVersion); + jObject[jss::api_version] = rpc::kApiMinimumSupportedVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiMinimumSupportedVersion); + jObject[jss::api_version] = rpc::kApiMaximumSupportedVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiMaximumSupportedVersion); - jObject[jss::api_version] = RPC::kApiMinimumSupportedVersion - 1; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); - jObject[jss::api_version] = RPC::kApiMaximumSupportedVersion + 1; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); - jObject[jss::api_version] = RPC::kApiBetaVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, true) == RPC::kApiBetaVersion); - jObject[jss::api_version] = RPC::kApiBetaVersion + 1; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, true) == RPC::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiMinimumSupportedVersion - 1; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiMaximumSupportedVersion + 1; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiBetaVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, true) == rpc::kApiBetaVersion); + jObject[jss::api_version] = rpc::kApiBetaVersion + 1; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, true) == rpc::kApiInvalidVersion); - jObject[jss::api_version] = RPC::kApiInvalidVersion.value; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); + jObject[jss::api_version] = rpc::kApiInvalidVersion.value; + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); jObject[jss::api_version] = "a"; - BEAST_EXPECT(RPC::getAPIVersionNumber(jObject, false) == RPC::kApiInvalidVersion); + BEAST_EXPECT(rpc::getAPIVersionNumber(jObject, false) == rpc::kApiInvalidVersion); } void @@ -141,7 +141,7 @@ class Version_test : public beast::unit_test::Suite "\"method\": \"version\", " "\"params\": { " "\"api_version\": " + - std::to_string(RPC::kApiMaximumSupportedVersion) + "}}"; + std::to_string(rpc::kApiMaximumSupportedVersion) + "}}"; auto re = env.rpc("json2", '[' + withoutApiVerion + ", " + withApiVerion + ']'); if (!BEAST_EXPECT(re.isArray())) @@ -176,7 +176,7 @@ class Version_test : public beast::unit_test::Suite "\"params\": { " "\"api_version\": " + std::to_string( - std::max(RPC::kApiMaximumSupportedVersion.value, RPC::kApiBetaVersion.value) + 1) + + std::max(rpc::kApiMaximumSupportedVersion.value, rpc::kApiBetaVersion.value) + 1) + "}}"; auto re = env.rpc("json2", '[' + withoutApiVerion + ", " + withWrongApiVerion + ']'); @@ -226,15 +226,15 @@ class Version_test : public beast::unit_test::Suite auto jrr = env.rpc( "json", "version", - "{\"api_version\": " + std::to_string(RPC::kApiBetaVersion) + "}")[jss::result]; + "{\"api_version\": " + std::to_string(rpc::kApiBetaVersion) + "}")[jss::result]; if (!BEAST_EXPECT(jrr.isMember(jss::version))) return; if (!BEAST_EXPECT(jrr[jss::version].isMember(jss::first)) && jrr[jss::version].isMember(jss::last)) return; - BEAST_EXPECT(jrr[jss::version][jss::first] == RPC::kApiMinimumSupportedVersion.value); - BEAST_EXPECT(jrr[jss::version][jss::last] == RPC::kApiBetaVersion.value); + BEAST_EXPECT(jrr[jss::version][jss::first] == rpc::kApiMinimumSupportedVersion.value); + BEAST_EXPECT(jrr[jss::version][jss::last] == rpc::kApiBetaVersion.value); } public: diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h index f7b09bccd1..e763c8bde4 100644 --- a/src/tests/libxrpl/helpers/TestServiceRegistry.h +++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h @@ -213,7 +213,7 @@ public: throw std::logic_error("TestServiceRegistry::peerReservations() not implemented"); } - Resource::Manager& + resource::Manager& getResourceManager() override { throw std::logic_error("TestServiceRegistry::getResourceManager() not implemented"); diff --git a/src/tests/libxrpl/peerfinder/Livecache.cpp b/src/tests/libxrpl/peerfinder/Livecache.cpp index 464ec0e5da..298b09e04e 100644 --- a/src/tests/libxrpl/peerfinder/Livecache.cpp +++ b/src/tests/libxrpl/peerfinder/Livecache.cpp @@ -29,7 +29,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { namespace { class LivecacheTest : public ::testing::Test @@ -41,22 +41,22 @@ protected: return beast::Journal{TestSink::instance()}; } - static beast::IP::Endpoint + static beast::ip::Endpoint endpoint(std::uint16_t index, bool v4 = true) { auto const port = static_cast(10000 + index); if (v4) { - auto bytes = beast::IP::AddressV4::bytes_type{ + auto bytes = beast::ip::AddressV4::bytes_type{ {54, static_cast((index / 256) % 256), static_cast(index % 256), 1}}; - return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV4{bytes}}, port}; + return beast::ip::Endpoint{beast::ip::Address{beast::ip::AddressV4{bytes}}, port}; } - auto bytes = beast::IP::AddressV6::bytes_type{ + auto bytes = beast::ip::AddressV6::bytes_type{ {0x20, 0x01, 0x0d, @@ -73,11 +73,11 @@ protected: static_cast((index / 256) % 256), static_cast(index % 256), 1}}; - return beast::IP::Endpoint{beast::IP::Address{beast::IP::AddressV6{bytes}}, port}; + return beast::ip::Endpoint{beast::ip::Address{beast::ip::AddressV6{bytes}}, port}; } void - addEndpoint(beast::IP::Endpoint const& ep, std::uint32_t hops = 0) + addEndpoint(beast::ip::Endpoint const& ep, std::uint32_t hops = 0) { cache_.insert(Endpoint{ep, hops}); } @@ -161,7 +161,7 @@ TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back) TEST_F(LivecacheTest, on_write_reports_entries_and_expiration) { cache_.insert(Endpoint{endpoint(1), 1}); - cache_.insert(Endpoint{endpoint(2), Tuning::kMaxHops + 1}); + cache_.insert(Endpoint{endpoint(2), tuning::kMaxHops + 1}); JsonPropertyStream stream; { @@ -190,7 +190,7 @@ TEST_F(LivecacheTest, expire_removes_entries_after_ttl) cache_.expire(); EXPECT_EQ(cache_.size(), 1u); - clock_.advance(Tuning::kLiveCacheSecondsToLive - 1s); + clock_.advance(tuning::kLiveCacheSecondsToLive - 1s); cache_.expire(); EXPECT_EQ(cache_.size(), 1u); @@ -206,7 +206,7 @@ TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl) cache_.insert(Endpoint{endpoint(1), 1}); cache_.insert(Endpoint{endpoint(2), 2}); - clock_.advance(Tuning::kLiveCacheSecondsToLive); + clock_.advance(tuning::kLiveCacheSecondsToLive); cache_.expire(); EXPECT_TRUE(cache_.empty()); } @@ -240,11 +240,11 @@ TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) { for (auto i = 0; i < 100; ++i) { - addEndpoint(endpoint(static_cast(i)), xrpl::randInt(Tuning::kMaxHops + 1)); + addEndpoint(endpoint(static_cast(i)), xrpl::randInt(tuning::kMaxHops + 1)); } using AtHop = std::vector; - using AllHops = std::array; + using AllHops = std::array; auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) { return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address); @@ -291,4 +291,4 @@ TEST_F(LivecacheTest, shuffle_preserves_bucket_contents) EXPECT_FALSE(allBucketsKeptOriginalOrder); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/tests/libxrpl/peerfinder/PeerFinder.cpp b/src/tests/libxrpl/peerfinder/PeerFinder.cpp index 31fa59d1ce..3a52bbb5aa 100644 --- a/src/tests/libxrpl/peerfinder/PeerFinder.cpp +++ b/src/tests/libxrpl/peerfinder/PeerFinder.cpp @@ -38,7 +38,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { namespace { using ::testing::_; @@ -51,10 +51,10 @@ journal() return beast::Journal{TestSink::instance()}; } -beast::IP::Endpoint +beast::ip::Endpoint endpoint(std::string const& value) { - return beast::IP::Endpoint::fromString(value); + return beast::ip::Endpoint::fromString(value); } class MockStore : public Store @@ -86,7 +86,7 @@ public: }; Store::Entry -storeEntry(beast::IP::Endpoint const& endpoint, int valence) +storeEntry(beast::ip::Endpoint const& endpoint, int valence) { Store::Entry entry; entry.endpoint = endpoint; @@ -106,15 +106,15 @@ class MockChecker public: MOCK_METHOD(void, stop, ()); MOCK_METHOD(void, wait, ()); - MOCK_METHOD(void, recordAsyncConnect, (beast::IP::Endpoint const& ep)); + MOCK_METHOD(void, recordAsyncConnect, (beast::ip::Endpoint const& ep)); boost::system::error_code nextError; bool completeAsync = true; - std::vector asyncConnects; + std::vector asyncConnects; template void - asyncConnect(beast::IP::Endpoint const& ep, Handler&& handler) + asyncConnect(beast::ip::Endpoint const& ep, Handler&& handler) { asyncConnects.push_back(ep); recordAsyncConnect(ep); @@ -204,7 +204,7 @@ protected: }; int -savedValence(std::vector const& entries, beast::IP::Endpoint const& endpoint) +savedValence(std::vector const& entries, beast::ip::Endpoint const& endpoint) { for (auto const& entry : entries) { @@ -458,7 +458,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) EXPECT_EQ(counts.outboundSlotsFree(), 1); EXPECT_EQ(counts.totalActive(), 0); EXPECT_FALSE(counts.isConnectedToNetwork()); - EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(counts.attemptsNeeded(), tuning::kMaxConnectAttempts); EXPECT_EQ(counts.stateString(), "0/1 out, 0/1 in, 0 connecting, 0 closing"); SlotImp inbound(endpoint("65.0.0.1:10001"), endpoint("65.0.0.2:10002"), false, clock); @@ -483,7 +483,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) counts.add(outbound); EXPECT_EQ(counts.attempts(), 1); EXPECT_EQ(counts.connectCount(), 1); - EXPECT_EQ(counts.attemptsNeeded(), Tuning::kMaxConnectAttempts - 1); + EXPECT_EQ(counts.attemptsNeeded(), tuning::kMaxConnectAttempts - 1); counts.remove(outbound); outbound.state(Slot::State::Connected); @@ -535,7 +535,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) Counts saturatedAttempts; saturatedAttempts.onConfig(config); std::vector> attempts; - for (int i = 0; i < Tuning::kMaxConnectAttempts; ++i) + for (int i = 0; i < tuning::kMaxConnectAttempts; ++i) { attempts.push_back( std::make_unique( @@ -544,7 +544,7 @@ TEST(PeerFinderCounts, tracks_slot_states_and_capacity) clock)); saturatedAttempts.add(*attempts.back()); } - EXPECT_EQ(saturatedAttempts.attempts(), Tuning::kMaxConnectAttempts); + EXPECT_EQ(saturatedAttempts.attempts(), tuning::kMaxConnectAttempts); EXPECT_EQ(saturatedAttempts.attemptsNeeded(), 0u); Config disconnected; @@ -563,7 +563,7 @@ TEST(PeerFinderHandouts, filters_redirect_slot_and_connect_targets) EXPECT_EQ(redirects.slot(), slot); EXPECT_TRUE(redirects.list().empty()); EXPECT_FALSE(redirects.full()); - EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1})); + EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), tuning::kMaxHops + 1})); EXPECT_FALSE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 0})); EXPECT_FALSE(redirects.tryInsert(Endpoint{remote.atPort(12000), 1})); EXPECT_TRUE(redirects.tryInsert(Endpoint{endpoint("65.0.0.3:10003"), 1})); @@ -574,7 +574,7 @@ TEST(PeerFinderHandouts, filters_redirect_slot_and_connect_targets) EXPECT_EQ(slotHandouts.slot(), slot); EXPECT_FALSE(slotHandouts.full()); EXPECT_FALSE( - slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.4:10004"), Tuning::kMaxHops + 1})); + slotHandouts.tryInsert(Endpoint{endpoint("65.0.0.4:10004"), tuning::kMaxHops + 1})); EXPECT_FALSE(slotHandouts.tryInsert(Endpoint{remote.atPort(12001), 1})); auto const recent = endpoint("65.0.0.5:10005"); @@ -620,7 +620,7 @@ TEST(PeerFinderHandouts, distributes_livecache_entries) EXPECT_FALSE(targets.front().list().empty()); EXPECT_FALSE(targets.back().list().empty()); - for (std::uint32_t i = 0; i < Tuning::kNumberOfEndpoints; ++i) + for (std::uint32_t i = 0; i < tuning::kNumberOfEndpoints; ++i) targets.front().insert(Endpoint{endpoint("65.1.0." + std::to_string(i + 1) + ":12000"), 1}); handout(targets.begin(), targets.begin() + 1, cache.hops.begin(), cache.hops.end()); @@ -633,7 +633,7 @@ TEST_F(PeerFinderTest, preprocess_filters_invalid_duplicate_and_extra_self_endpo auto const remote = endpoint("65.0.0.2:10002"); auto const slot = std::make_shared(local, remote, false, clock_); Endpoints endpoints{ - Endpoint{endpoint("65.0.0.3:10003"), Tuning::kMaxHops + 1}, + Endpoint{endpoint("65.0.0.3:10003"), tuning::kMaxHops + 1}, Endpoint{endpoint("0.0.0.0:2459"), 0}, Endpoint{endpoint("0.0.0.0:2460"), 0}, Endpoint{endpoint("10.0.0.1:10004"), 1}, @@ -677,7 +677,7 @@ TEST_F(PeerFinderTest, on_endpoints_checks_neighbor_before_caching_it) EXPECT_TRUE(slot->canAccept); EXPECT_TRUE(logic_.livecache.empty()); - clock_.advance(Tuning::kSecondsPerMessage); + clock_.advance(tuning::kSecondsPerMessage); logic_.onEndpoints(slot, advertised); EXPECT_EQ(logic_.livecache.size(), 1u); EXPECT_EQ(logic_.bootcache.size(), 1u); @@ -711,7 +711,7 @@ TEST_F(PeerFinderTest, on_endpoints_skips_failed_neighbor_connectivity_checks) EXPECT_TRUE(slot->checked); EXPECT_FALSE(slot->canAccept); - clock_.advance(Tuning::kSecondsPerMessage); + clock_.advance(tuning::kSecondsPerMessage); logic_.onEndpoints(slot, advertised); EXPECT_TRUE(logic_.livecache.empty()); @@ -740,7 +740,7 @@ TEST_F(PeerFinderTest, on_endpoints_waits_for_pending_connectivity_check) logic_.onEndpoints(slot, advertised); EXPECT_TRUE(slot->connectivityCheckInProgress); - clock_.advance(Tuning::kSecondsPerMessage); + clock_.advance(tuning::kSecondsPerMessage); logic_.onEndpoints(slot, advertised); EXPECT_EQ(checker_.asyncConnects.size(), 1u); EXPECT_TRUE(logic_.livecache.empty()); @@ -950,7 +950,7 @@ TEST(PeerFinderBootcache, periodic_activity_saves_after_cooldown) cache.periodicActivity(); EXPECT_TRUE(store.saves.empty()); - clock.advance(Tuning::kBootcacheCooldownTime + 1s); + clock.advance(tuning::kBootcacheCooldownTime + 1s); cache.periodicActivity(); ASSERT_EQ(store.saves.size(), 1u); @@ -967,23 +967,23 @@ TEST(PeerFinderBootcache, prunes_when_cache_exceeds_limit) TestStopwatch clock; Bootcache cache(store, clock, journal()); - for (std::uint16_t i = 0; i <= Tuning::kBootcacheSize; ++i) + for (std::uint16_t i = 0; i <= tuning::kBootcacheSize; ++i) { EXPECT_TRUE(cache.insert(endpoint( "65.0." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256) + ":" + std::to_string(10000 + i)))); } - EXPECT_LE(cache.size(), Tuning::kBootcacheSize); + EXPECT_LE(cache.size(), tuning::kBootcacheSize); } TEST(PeerFinderEndpoint, clamps_hops_to_overflow_bucket) { auto const address = endpoint("65.0.0.1:10001"); - Endpoint const ep(address, Tuning::kMaxHops + 10); + Endpoint const ep(address, tuning::kMaxHops + 10); EXPECT_EQ(ep.address, address); - EXPECT_EQ(ep.hops, Tuning::kMaxHops + 1); + EXPECT_EQ(ep.hops, tuning::kMaxHops + 1); } TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) @@ -1001,7 +1001,7 @@ TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) EXPECT_FALSE(inbound.reserved()); EXPECT_EQ(inbound.state(), State::Accept); EXPECT_EQ(inbound.remoteEndpoint(), remote); - EXPECT_EQ(inbound.localEndpoint(), std::optional{local}); + EXPECT_EQ(inbound.localEndpoint(), std::optional{local}); EXPECT_FALSE(inbound.publicKey()); EXPECT_FALSE(inbound.listeningPort()); EXPECT_FALSE(inbound.checked); @@ -1018,7 +1018,7 @@ TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) inbound.reserved(true); inbound.setListeningPort(2459); - EXPECT_EQ(inbound.localEndpoint(), std::optional{newLocal}); + EXPECT_EQ(inbound.localEndpoint(), std::optional{newLocal}); EXPECT_EQ(inbound.remoteEndpoint(), newRemote); EXPECT_EQ(inbound.publicKey(), std::optional{publicKey}); EXPECT_TRUE(inbound.reserved()); @@ -1055,7 +1055,7 @@ TEST(PeerFinderSlotImp, tracks_state_and_recent_endpoints) EXPECT_TRUE(outbound.recent.filter(recent, 1)); EXPECT_FALSE(outbound.recent.filter(recent, 0)); - clock.advance(Tuning::kLiveCacheSecondsToLive + 1s); + clock.advance(tuning::kLiveCacheSecondsToLive + 1s); outbound.expire(); EXPECT_FALSE(outbound.recent.filter(recent, 1)); } @@ -1112,7 +1112,7 @@ TEST(PeerFinderConfig, calculates_outbound_peers_and_clamps_ip_limits) { Config config; config.maxPeers = 1; - EXPECT_EQ(config.calcOutPeers(), Tuning::kMinOutCount); + EXPECT_EQ(config.calcOutPeers(), tuning::kMinOutCount); config.maxPeers = 100; EXPECT_EQ(config.calcOutPeers(), 15u); @@ -1267,4 +1267,4 @@ TEST(PeerFinderConfig, rejects_incomplete_or_out_of_range_peer_limits) } } // namespace -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/tests/libxrpl/protocol/ApiVersion.cpp b/src/tests/libxrpl/protocol/ApiVersion.cpp index 8af7787102..5bb6a158cf 100644 --- a/src/tests/libxrpl/protocol/ApiVersion.cpp +++ b/src/tests/libxrpl/protocol/ApiVersion.cpp @@ -6,21 +6,21 @@ using namespace xrpl; TEST(ApiVersion, invariants) { - static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumSupportedVersion); - static_assert(RPC::kApiMinimumSupportedVersion <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiMaximumSupportedVersion <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiBetaVersion <= RPC::kApiMaximumValidVersion); + static_assert(rpc::kApiMinimumSupportedVersion <= rpc::kApiMaximumSupportedVersion); + static_assert(rpc::kApiMinimumSupportedVersion <= rpc::kApiMaximumValidVersion); + static_assert(rpc::kApiMaximumSupportedVersion <= rpc::kApiMaximumValidVersion); + static_assert(rpc::kApiBetaVersion <= rpc::kApiMaximumValidVersion); } // Update when we change versions TEST(ApiVersion, versions) { - static_assert(RPC::kApiMinimumSupportedVersion >= 1); - static_assert(RPC::kApiMinimumSupportedVersion < 2); - static_assert(RPC::kApiMaximumSupportedVersion >= 2); - static_assert(RPC::kApiMaximumSupportedVersion < 3); - static_assert(RPC::kApiMaximumValidVersion >= 3); - static_assert(RPC::kApiMaximumValidVersion < 4); - static_assert(RPC::kApiBetaVersion >= 3); - static_assert(RPC::kApiBetaVersion < 4); + static_assert(rpc::kApiMinimumSupportedVersion >= 1); + static_assert(rpc::kApiMinimumSupportedVersion < 2); + static_assert(rpc::kApiMaximumSupportedVersion >= 2); + static_assert(rpc::kApiMaximumSupportedVersion < 3); + static_assert(rpc::kApiMaximumValidVersion >= 3); + static_assert(rpc::kApiMaximumValidVersion < 4); + static_assert(rpc::kApiBetaVersion >= 3); + static_assert(rpc::kApiBetaVersion < 4); } diff --git a/src/tests/libxrpl/resource/Logic.cpp b/src/tests/libxrpl/resource/Logic.cpp index a3362b4540..b38ca2e051 100644 --- a/src/tests/libxrpl/resource/Logic.cpp +++ b/src/tests/libxrpl/resource/Logic.cpp @@ -24,7 +24,7 @@ #include #include -namespace xrpl::Resource { +namespace xrpl::resource { class ResourceManagerTest : public ::testing::Test { @@ -68,13 +68,13 @@ protected: { Gossip::Item item; item.balance = 100 + randInt(499); - beast::IP::AddressV4::bytes_type const d = {{ + beast::ip::AddressV4::bytes_type const d = {{ 192, 0, 2, static_cast(v + i), }}; - item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}}; + item.address = beast::ip::Endpoint{beast::ip::AddressV4{d}}; gossip.items.push_back(std::move(item)); } return gossip; @@ -86,7 +86,7 @@ TEST_F(ResourceManagerTest, limited_warn_drop) TestLogic logic{j_}; Charge const fee{kDropThreshold + 1}; - beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")}; + beast::ip::Endpoint const addr{beast::ip::Endpoint::fromString("192.0.2.2")}; { Consumer c{logic.newInboundEndpoint(addr)}; @@ -158,7 +158,7 @@ TEST_F(ResourceManagerTest, unlimited_warn_drop) TestLogic logic{j_}; Charge const fee{kDropThreshold + 1}; - beast::IP::Endpoint const addr{beast::IP::Endpoint::fromString("192.0.2.2")}; + beast::ip::Endpoint const addr{beast::ip::Endpoint::fromString("192.0.2.2")}; Consumer c{logic.newUnlimitedEndpoint(addr)}; // Create load until we get a warning @@ -185,7 +185,7 @@ TEST_F(ResourceManagerTest, charges) TestLogic logic{j_}; { - beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.1")}; + beast::ip::Endpoint const address{beast::ip::Endpoint::fromString("192.0.2.1")}; Consumer c{logic.newInboundEndpoint(address)}; Charge const fee{1000}; JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second"; @@ -199,7 +199,7 @@ TEST_F(ResourceManagerTest, charges) } { - beast::IP::Endpoint const address{beast::IP::Endpoint::fromString("192.0.2.2")}; + beast::ip::Endpoint const address{beast::ip::Endpoint::fromString("192.0.2.2")}; Consumer c{logic.newInboundEndpoint(address)}; Charge const fee{1000}; JLOG(j_.info()) << "Charging " << c.toString() << " " << fee << " per second"; @@ -230,16 +230,16 @@ TEST_F(ResourceManagerTest, import) Gossip g; Gossip::Item item; item.balance = 100; - beast::IP::AddressV4::bytes_type const d = {{ + beast::ip::AddressV4::bytes_type const d = {{ 192, 0, 2, 1, }}; - item.address = beast::IP::Endpoint{beast::IP::AddressV4{d}}; + item.address = beast::ip::Endpoint{beast::ip::AddressV4{d}}; g.items.push_back(std::move(item)); logic.importConsumers("g", g); } -} // namespace xrpl::Resource +} // namespace xrpl::resource diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp b/src/xrpld/app/consensus/RCLConsensus.cpp index 20421ab916..28b910c8e5 100644 --- a/src/xrpld/app/consensus/RCLConsensus.cpp +++ b/src/xrpld/app/consensus/RCLConsensus.cpp @@ -847,7 +847,7 @@ RCLConsensus::Adaptor::validate(RCLCxLedger const& ledger, RCLTxSet const& txns, // Report our server version every flag ledger: if (ledger.ledger->isVotingLedger()) - v.setFieldU64(sfServerVersion, BuildInfo::getEncodedVersion()); + v.setFieldU64(sfServerVersion, build_info::getEncodedVersion()); // Report our load { diff --git a/src/xrpld/app/ledger/LedgerReplayer.h b/src/xrpld/app/ledger/LedgerReplayer.h index 6feb187df6..b2806ee813 100644 --- a/src/xrpld/app/ledger/LedgerReplayer.h +++ b/src/xrpld/app/ledger/LedgerReplayer.h @@ -24,7 +24,7 @@ namespace test { class LedgerReplayClient; } // namespace test -namespace LedgerReplayParameters { +namespace ledger_replay_parameters { // timeout value for LedgerReplayTask constexpr auto kTaskTimeout = std::chrono::milliseconds{500}; @@ -53,7 +53,7 @@ constexpr std::uint32_t kMaxTaskSize = 256; // to limit the number of LedgerReplay related jobs in JobQueue constexpr std::uint32_t kMaxQueuedTasks = 100; -} // namespace LedgerReplayParameters +} // namespace ledger_replay_parameters /** * Manages the lifetime of ledger replay tasks. diff --git a/src/xrpld/app/ledger/LedgerToJson.h b/src/xrpld/app/ledger/LedgerToJson.h index 1eac4d68f1..e1172e897a 100644 --- a/src/xrpld/app/ledger/LedgerToJson.h +++ b/src/xrpld/app/ledger/LedgerToJson.h @@ -18,7 +18,7 @@ struct LedgerFill { LedgerFill( ReadView const& l, - RPC::Context const* ctx, + rpc::Context const* ctx, int o = 0, std::vector q = {}) : ledger(l), options(o), txQueue(std::move(q)), context(ctx) @@ -40,7 +40,7 @@ struct LedgerFill ReadView const& ledger; int options; std::vector txQueue; - RPC::Context const* context; + rpc::Context const* context; std::optional closeTime; }; diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp b/src/xrpld/app/ledger/detail/InboundLedger.cpp index b3dafcf5e6..246c7d567b 100644 --- a/src/xrpld/app/ledger/detail/InboundLedger.cpp +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp @@ -879,7 +879,7 @@ InboundLedger::receiveNode( { JLOG(journal_.warn()) << "Got invalid node data for ledger " << hash_ << " from peer " << peer->id(); - peer->charge(Resource::kFeeInvalidData, "ledger_node.node_data invalid"); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_data invalid"); san.incInvalid(); return; } @@ -889,7 +889,7 @@ InboundLedger::receiveNode( { JLOG(journal_.warn()) << "Got invalid node id for ledger " << hash_ << " from peer " << peer->id(); - peer->charge(Resource::kFeeInvalidData, "ledger_node.node_id invalid"); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_id invalid"); san.incInvalid(); return; } @@ -903,7 +903,7 @@ InboundLedger::receiveNode( { JLOG(journal_.warn()) << "Got invalid node " << *nodeID << " for ledger " << hash_ << " from peer " << peer->id(); - peer->charge(Resource::kFeeInvalidData, "ledger_node invalid"); + peer->charge(resource::kFeeInvalidData, "ledger_node invalid"); return; } } @@ -1068,7 +1068,7 @@ InboundLedger::gotData( * Returns the number of useful nodes */ // VFALCO NOTE, it is not necessary to pass the entire Peer, -// we can get away with just a Resource::Consumer endpoint. +// we can get away with just a resource::Consumer endpoint. // // TODO Change peer to Consumer // @@ -1080,7 +1080,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co if (packet.nodes().empty()) { JLOG(journal_.warn()) << peer->id() << ": empty header data"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data empty header"); + peer->charge(resource::kFeeMalformedRequest, "ledger_data empty header"); return -1; } @@ -1095,7 +1095,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co if (!takeHeader(packet.nodes(0).nodedata())) { JLOG(journal_.warn()) << "Got invalid header data"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data invalid header"); + peer->charge(resource::kFeeMalformedRequest, "ledger_data invalid header"); return -1; } @@ -1109,7 +1109,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co << " from peer " << peer->id(); if (san.isInvalid()) { - peer->charge(Resource::kFeeInvalidData, "ledger_data invalid AS root"); + peer->charge(resource::kFeeInvalidData, "ledger_data invalid AS root"); return -1; } } @@ -1121,7 +1121,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co << " from peer " << peer->id(); if (san.isInvalid()) { - peer->charge(Resource::kFeeInvalidData, "ledger_data invalid TX root"); + peer->charge(resource::kFeeInvalidData, "ledger_data invalid TX root"); return -1; } } @@ -1131,7 +1131,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co JLOG(journal_.warn()) << "Included AS/TX root invalid for ledger " << hash_ << " from peer " << peer->id() << ": " << ex.what(); using namespace std::string_literals; - peer->charge(Resource::kFeeInvalidData, "ledger_data "s + ex.what()); + peer->charge(resource::kFeeInvalidData, "ledger_data "s + ex.what()); return -1; } @@ -1147,7 +1147,7 @@ InboundLedger::processData(std::shared_ptr peer, protocol::TMLedgerData co if (packet.nodes().empty()) { JLOG(journal_.info()) << peer->id() << ": response with no nodes"; - peer->charge(Resource::kFeeMalformedRequest, "ledger_data no nodes"); + peer->charge(resource::kFeeMalformedRequest, "ledger_data no nodes"); return -1; } diff --git a/src/xrpld/app/ledger/detail/InboundTransactions.cpp b/src/xrpld/app/ledger/detail/InboundTransactions.cpp index d735a97d28..62897fe617 100644 --- a/src/xrpld/app/ledger/detail/InboundTransactions.cpp +++ b/src/xrpld/app/ledger/detail/InboundTransactions.cpp @@ -138,7 +138,7 @@ public: if (ta == nullptr) { - peer->charge(Resource::kFeeUselessData, "ledger_data useless"); + peer->charge(resource::kFeeUselessData, "ledger_data useless"); return; } @@ -152,7 +152,7 @@ public: { JLOG(j_.warn()) << "Got invalid node data for TX set " << hash << " from peer " << peer->id(); - peer->charge(Resource::kFeeInvalidData, "ledger_node.node_data invalid"); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_data invalid"); return; } @@ -161,7 +161,7 @@ public: { JLOG(j_.warn()) << "Got invalid node id for TX set " << hash << " from peer " << peer->id(); - peer->charge(Resource::kFeeInvalidData, "ledger_node.node_id invalid"); + peer->charge(resource::kFeeInvalidData, "ledger_node.node_id invalid"); return; } @@ -171,11 +171,11 @@ public: auto const san = ta->takeNodes(std::move(data), peer); if (san.isInvalid()) { - peer->charge(Resource::kFeeInvalidData, "ledger_data invalid"); + peer->charge(resource::kFeeInvalidData, "ledger_data invalid"); } else if (!san.isUseful()) { - peer->charge(Resource::kFeeUselessData, "ledger_data useless"); + peer->charge(resource::kFeeUselessData, "ledger_data useless"); } } diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp index 7ac85b892e..344d5cb8fc 100644 --- a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp @@ -43,10 +43,10 @@ LedgerDeltaAcquire::LedgerDeltaAcquire( : TimeoutCounter( app, ledgerHash, - LedgerReplayParameters::kSubTaskTimeout, + ledger_replay_parameters::kSubTaskTimeout, {.jobType = JtReplayTask, .jobName = "LedReplDelta", - .jobLimit = LedgerReplayParameters::kMaxQueuedTasks}, + .jobLimit = ledger_replay_parameters::kMaxQueuedTasks}, app.getJournal("LedgerReplayDelta")) , inboundLedgers_(inboundLedgers) , ledgerSeq_(ledgerSeq) @@ -101,10 +101,10 @@ LedgerDeltaAcquire::trigger(std::size_t limit, ScopedLockType& sl) } else { - if (++noFeaturePeerCount_ >= LedgerReplayParameters::kMaxNoFeaturePeerCount) + if (++noFeaturePeerCount_ >= ledger_replay_parameters::kMaxNoFeaturePeerCount) { JLOG(journal_.debug()) << "Fall back for " << hash_; - timerInterval_ = LedgerReplayParameters::kSubTaskFallbackTimeout; + timerInterval_ = ledger_replay_parameters::kSubTaskFallbackTimeout; fallBack_ = true; } } @@ -119,7 +119,7 @@ void LedgerDeltaAcquire::onTimer(bool progress, ScopedLockType& sl) { JLOG(journal_.trace()) << "timeouts_=" << timeouts_ << " for " << hash_; - if (timeouts_ > LedgerReplayParameters::kSubTaskMaxTimeouts) + if (timeouts_ > ledger_replay_parameters::kSubTaskMaxTimeouts) { failed_ = true; JLOG(journal_.debug()) << "too many timeouts " << hash_; diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp index 9a0335fc3c..83d76bcd2a 100644 --- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp @@ -1042,8 +1042,8 @@ LedgerMaster::checkAccept(std::shared_ptr const& ledger) if (v->isFieldPresent(sfServerVersion)) { auto version = v->getFieldU64(sfServerVersion); - higherVersionCount += BuildInfo::isNewerVersion(version) ? 1 : 0; - xrpldCount += BuildInfo::isXrpldVersion(version) ? 1 : 0; + higherVersionCount += build_info::isNewerVersion(version) ? 1 : 0; + xrpldCount += build_info::isXrpldVersion(version) ? 1 : 0; } } // We report only if (1) we have accumulated validation messages @@ -2088,21 +2088,21 @@ LedgerMaster::makeFetchPack( if (!have) { JLOG(journal_.info()) << "Peer requests fetch pack for ledger we don't have: " << have; - peer->charge(Resource::kFeeRequestNoReply, "get_object ledger"); + peer->charge(resource::kFeeRequestNoReply, "get_object ledger"); return; } if (have->open()) { JLOG(journal_.warn()) << "Peer requests fetch pack from open ledger: " << have; - peer->charge(Resource::kFeeMalformedRequest, "get_object ledger open"); + peer->charge(resource::kFeeMalformedRequest, "get_object ledger open"); return; } if (have->header().seq < getEarliestFetch()) { JLOG(journal_.debug()) << "Peer requests fetch pack that is too early"; - peer->charge(Resource::kFeeMalformedRequest, "get_object ledger early"); + peer->charge(resource::kFeeMalformedRequest, "get_object ledger early"); return; } @@ -2112,7 +2112,7 @@ LedgerMaster::makeFetchPack( { JLOG(journal_.info()) << "Peer requests fetch pack for ledger whose predecessor we " << "don't have: " << have; - peer->charge(Resource::kFeeRequestNoReply, "get_object ledger no parent"); + peer->charge(resource::kFeeRequestNoReply, "get_object ledger no parent"); return; } diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp index e7cd031247..3d7b1e0f92 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp @@ -87,18 +87,18 @@ LedgerReplayTask::LedgerReplayTask( : TimeoutCounter( app, parameter.finishHash, - LedgerReplayParameters::kTaskTimeout, + ledger_replay_parameters::kTaskTimeout, {.jobType = JtReplayTask, .jobName = "LedReplTask", - .jobLimit = LedgerReplayParameters::kMaxQueuedTasks}, + .jobLimit = ledger_replay_parameters::kMaxQueuedTasks}, app.getJournal("LedgerReplayTask")) , inboundLedgers_(inboundLedgers) , replayer_(replayer) , parameter_(parameter) , maxTimeouts_( std::max( - LedgerReplayParameters::kTaskMaxTimeoutsMinimum, - parameter.totalLedgers * LedgerReplayParameters::kTaskMaxTimeoutsMultiplier)) + ledger_replay_parameters::kTaskMaxTimeoutsMinimum, + parameter.totalLedgers * ledger_replay_parameters::kTaskMaxTimeoutsMultiplier)) , skipListAcquirer_(skipListAcquirer) { JLOG(journal_.trace()) << "Create " << hash_; diff --git a/src/xrpld/app/ledger/detail/LedgerReplayer.cpp b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp index 3bb5ca9434..52184c1723 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayer.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp @@ -51,7 +51,7 @@ LedgerReplayer::replay( { XRPL_ASSERT( finishLedgerHash.isNonZero() && totalNumLedgers > 0 && - totalNumLedgers <= LedgerReplayParameters::kMaxTaskSize, + totalNumLedgers <= ledger_replay_parameters::kMaxTaskSize, "xrpl::LedgerReplayer::replay : valid inputs"); // NOLINTNEXTLINE(misc-const-correctness) @@ -64,7 +64,7 @@ LedgerReplayer::replay( std::scoped_lock const lock(mtx_); if (app_.isStopping()) return; - if (tasks_.size() >= LedgerReplayParameters::kMaxTasks) + if (tasks_.size() >= ledger_replay_parameters::kMaxTasks) { JLOG(j_.info()) << "Too many replay tasks, dropping new task " << parameter.finishHash; return; diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 7a581e2389..9d3820e9f7 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -135,7 +135,7 @@ fillJsonTx( { copyFrom(txJson[jss::tx_json], txn->getJson(JsonOptions::Values::DisableApiPriorV2, false)); txJson[jss::hash] = to_string(txn->getTransactionID()); - RPC::insertDeliverMax(txJson[jss::tx_json], txnType, fill.context->apiVersion); + rpc::insertDeliverMax(txJson[jss::tx_json], txnType, fill.context->apiVersion); if (stMeta) { @@ -144,7 +144,7 @@ fillJsonTx( // If applicable, insert delivered amount if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) { - RPC::insertDeliveredAmount( + rpc::insertDeliveredAmount( txJson[jss::meta], fill.ledger, txn, @@ -152,7 +152,7 @@ fillJsonTx( } // If applicable, insert mpt issuance id - RPC::insertMPTokenIssuanceID( + rpc::insertMPTokenIssuanceID( txJson[jss::meta], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } @@ -172,7 +172,7 @@ fillJsonTx( else { copyFrom(txJson, txn->getJson(JsonOptions::Values::None)); - RPC::insertDeliverMax(txJson, txnType, fill.context->apiVersion); + rpc::insertDeliverMax(txJson, txnType, fill.context->apiVersion); if (stMeta) { txJson[jss::metaData] = stMeta->getJson(JsonOptions::Values::None); @@ -180,7 +180,7 @@ fillJsonTx( // If applicable, insert delivered amount if (txnType == ttPAYMENT || txnType == ttCHECK_CASH) { - RPC::insertDeliveredAmount( + rpc::insertDeliveredAmount( txJson[jss::metaData], fill.ledger, txn, @@ -188,7 +188,7 @@ fillJsonTx( } // If applicable, insert mpt issuance id - RPC::insertMPTokenIssuanceID( + rpc::insertMPTokenIssuanceID( txJson[jss::metaData], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta}); } } @@ -337,7 +337,7 @@ fillJson(json::Value& json, LedgerFill const& fill) fill.ledger.header(), bFull, ((fill.context != nullptr) ? fill.context->apiVersion - : RPC::kApiMaximumSupportedVersion)); + : rpc::kApiMaximumSupportedVersion)); } if (bFull || ((fill.options & static_cast(LedgerFill::Options::DumpTxrp)) != 0)) diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp index 8ebd14083a..97b50a9a3a 100644 --- a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp @@ -37,10 +37,10 @@ SkipListAcquire::SkipListAcquire( : TimeoutCounter( app, ledgerHash, - LedgerReplayParameters::kSubTaskTimeout, + ledger_replay_parameters::kSubTaskTimeout, {.jobType = JtReplayTask, .jobName = "SkipListAcq", - .jobLimit = LedgerReplayParameters::kMaxQueuedTasks}, + .jobLimit = ledger_replay_parameters::kMaxQueuedTasks}, app.getJournal("LedgerReplaySkipList")) , inboundLedgers_(inboundLedgers) , peerSet_(std::move(peerSet)) @@ -96,10 +96,10 @@ SkipListAcquire::trigger(std::size_t limit, ScopedLockType& sl) { JLOG(journal_.trace()) << "Add a no feature peer " << peer->id() << " for " << hash_; - if (++noFeaturePeerCount_ >= LedgerReplayParameters::kMaxNoFeaturePeerCount) + if (++noFeaturePeerCount_ >= ledger_replay_parameters::kMaxNoFeaturePeerCount) { JLOG(journal_.debug()) << "Fall back for " << hash_; - timerInterval_ = LedgerReplayParameters::kSubTaskFallbackTimeout; + timerInterval_ = ledger_replay_parameters::kSubTaskFallbackTimeout; fallBack_ = true; } } @@ -114,7 +114,7 @@ void SkipListAcquire::onTimer(bool progress, ScopedLockType& sl) { JLOG(journal_.trace()) << "timeouts_=" << timeouts_ << " for " << hash_; - if (timeouts_ > LedgerReplayParameters::kSubTaskMaxTimeouts) + if (timeouts_ > ledger_replay_parameters::kSubTaskMaxTimeouts) { failed_ = true; JLOG(journal_.debug()) << "too many timeouts " << hash_; diff --git a/src/xrpld/app/main/Application.cpp b/src/xrpld/app/main/Application.cpp index 5c8fdad37c..52ec9ce544 100644 --- a/src/xrpld/app/main/Application.cpp +++ b/src/xrpld/app/main/Application.cpp @@ -230,7 +230,7 @@ public: std::optional> nodeIdentity_; ValidatorKeys const validatorKeys_; - std::unique_ptr resourceManager_; + std::unique_ptr resourceManager_; std::unique_ptr nodeStore_; NodeFamily nodeFamily_; @@ -375,7 +375,7 @@ public: , networkIDService_(std::make_unique(config_->networkId)) , validatorKeys_(*config_, journal_) , resourceManager_( - Resource::makeManager(collectorManager_->collector(), logs_->journal("Resource"))) + resource::makeManager(collectorManager_->collector(), logs_->journal("Resource"))) , nodeStore_(shaMapStore_->makeNodeStore( config_->prefetchWorkers > 0 ? config_->prefetchWorkers : 4)) , nodeFamily_(*this, *collectorManager_) @@ -673,7 +673,7 @@ public: return *loadManager_; } - Resource::Manager& + resource::Manager& getResourceManager() override { return *resourceManager_; @@ -1187,7 +1187,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) logs_->threshold(Severity::Debug); } - JLOG(journal_.info()) << "Process starting: " << BuildInfo::getFullVersionString() + JLOG(journal_.info()) << "Process starting: " << build_info::getFullVersionString() << ", Instance Cookie: " << instanceCookie_; if (numberOfThreads(*config_) < 2) @@ -1457,9 +1457,9 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) JLOG(journal_.fatal()) << "Startup RPC: " << jvCommand << std::endl; } - Resource::Charge loadType = Resource::kFeeReferenceRpc; - Resource::Consumer c; - RPC::JsonContext context{ + resource::Charge loadType = resource::kFeeReferenceRpc; + resource::Consumer c; + rpc::JsonContext context{ {.j = getJournal("RPCHandler"), .app = *this, .loadType = loadType, @@ -1469,11 +1469,11 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) .role = Role::ADMIN, .coro = {}, .infoSub = {}, - .apiVersion = RPC::kApiMaximumSupportedVersion}, + .apiVersion = rpc::kApiMaximumSupportedVersion}, jvCommand}; json::Value jvResult; - RPC::doCommand(context, jvResult); + rpc::doCommand(context, jvResult); if (!config_->quiet()) { @@ -1489,7 +1489,7 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline) void ApplicationImp::start(bool withTimers) { - JLOG(journal_.info()) << "Application starting. Version is " << BuildInfo::getVersionString(); + JLOG(journal_.info()) << "Application starting. Version is " << build_info::getVersionString(); if (withTimers) { diff --git a/src/xrpld/app/main/CollectorManager.cpp b/src/xrpld/app/main/CollectorManager.cpp index 9e1278607f..87b5286f97 100644 --- a/src/xrpld/app/main/CollectorManager.cpp +++ b/src/xrpld/app/main/CollectorManager.cpp @@ -30,8 +30,8 @@ public: if (server == "statsd") { - beast::IP::Endpoint const address( - beast::IP::Endpoint::fromString(get(params, Keys::kAddress))); + beast::ip::Endpoint const address( + beast::ip::Endpoint::fromString(get(params, Keys::kAddress))); std::string const& prefix(get(params, Keys::kPrefix)); collector_ = beast::insight::StatsDCollector::make(address, prefix, journal); diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index 1146cbdc08..1b20ff1d49 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -71,10 +71,10 @@ getEndpoint(std::string const& peer) peerClean = peer.substr(first + 1); } - std::optional endpoint = - beast::IP::Endpoint::fromStringChecked(peerClean); + std::optional endpoint = + beast::ip::Endpoint::fromStringChecked(peerClean); if (endpoint) - return beast::IP::toAsioEndpoint(endpoint.value()); + return beast::ip::toAsioEndpoint(endpoint.value()); } catch (std::exception const&) // NOLINT(bugprone-empty-catch) { @@ -92,8 +92,8 @@ GRPCServerImpl::CallData::CallData( BindListener bindListener, Handler handler, Forward forward, - RPC::Condition requiredCondition, - Resource::Charge loadType, + rpc::Condition requiredCondition, + resource::Charge loadType, std::vector const& secureGatewayIPs) : service_(service) , cq_(cq) @@ -195,7 +195,7 @@ GRPCServerImpl::CallData::process(std::shared_ptr context{ + rpc::GRPCContext context{ {app_.getJournal("gRPCServer"), app_, loadType, @@ -209,11 +209,11 @@ GRPCServerImpl::CallData::process(std::shared_ptr::isFinished() } template -Resource::Charge +resource::Charge GRPCServerImpl::CallData::getLoadType() { return loadType_; @@ -323,12 +323,12 @@ GRPCServerImpl::CallData::setIsUnlimited(Response& response, } template -Resource::Consumer +resource::Consumer GRPCServerImpl::CallData::getUsage() { auto endpoint = getClientEndpoint(); if (endpoint) - return app_.getResourceManager().newInboundEndpoint(beast::IP::fromAsio(endpoint.value())); + return app_.getResourceManager().newInboundEndpoint(beast::ip::fromAsio(endpoint.value())); Throw("Failed to get client endpoint"); } @@ -527,7 +527,7 @@ GRPCServerImpl::handleRpcs() std::vector> GRPCServerImpl::setupListeners() { - using RPC::Condition; + using rpc::Condition; std::vector> requests; auto addToRequests = [&requests](auto callData) { requests.push_back(std::move(callData)); }; @@ -545,7 +545,7 @@ GRPCServerImpl::setupListeners() doLedgerGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedger, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } { @@ -562,7 +562,7 @@ GRPCServerImpl::setupListeners() doLedgerDataGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerData, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } { @@ -579,7 +579,7 @@ GRPCServerImpl::setupListeners() doLedgerDiffGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerDiff, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } { @@ -596,7 +596,7 @@ GRPCServerImpl::setupListeners() doLedgerEntryGrpc, &org::xrpl::rpc::v1::XRPLedgerAPIService::Stub::GetLedgerEntry, Condition::NoCondition, - Resource::kFeeMediumBurdenRpc, + resource::kFeeMediumBurdenRpc, secureGatewayIPs_)); } return requests; diff --git a/src/xrpld/app/main/GRPCServer.h b/src/xrpld/app/main/GRPCServer.h index db948cab99..98b50fcd0c 100644 --- a/src/xrpld/app/main/GRPCServer.h +++ b/src/xrpld/app/main/GRPCServer.h @@ -102,7 +102,7 @@ private: // typedef for actual handler (that populates a response) // handlers are defined in rpc/GRPCHandlers.h template - using Handler = std::function(RPC::GRPCContext&)>; + using Handler = std::function(rpc::GRPCContext&)>; // This implementation is currently limited to v1 of the API static constexpr unsigned kApiVersion = 1; @@ -189,10 +189,10 @@ private: Forward forward_; // Condition required for this RPC - RPC::Condition requiredCondition_; + rpc::Condition requiredCondition_; // Load type for this RPC - Resource::Charge loadType_; + resource::Charge loadType_; std::vector const& secureGatewayIPs_; @@ -209,8 +209,8 @@ private: BindListener bindListener, Handler handler, Forward forward, - RPC::Condition requiredCondition, - Resource::Charge loadType, + rpc::Condition requiredCondition, + resource::Charge loadType, std::vector const& secureGatewayIPs); CallData(CallData const&) = delete; @@ -233,7 +233,7 @@ private: process(std::shared_ptr coro); // return load type of this RPC - Resource::Charge + resource::Charge getLoadType(); // return the Role used for this RPC @@ -241,7 +241,7 @@ private: getRole(bool isUnlimited); // register endpoint with ResourceManager and return usage - Resource::Consumer + resource::Consumer getUsage(); // Returns the ip of the client @@ -290,7 +290,7 @@ private: // forward request to a p2p node void - forwardToP2p(RPC::GRPCContext& context); + forwardToP2p(rpc::GRPCContext& context); }; // CallData diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index d0b40efce8..a23b84f2e8 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -506,7 +506,7 @@ run(int argc, char** argv) if (vm.contains("version")) { // LCOV_EXCL_START - std::cout << "xrpld version " << BuildInfo::getVersionString() << std::endl; + std::cout << "xrpld version " << build_info::getVersionString() << std::endl; std::cout << "Git commit hash: " << xrpl::git::getCommitHash() << std::endl; std::cout << "Git build branch: " << xrpl::git::getBuildBranch() << std::endl; return 0; @@ -716,7 +716,7 @@ run(int argc, char** argv) // happen after the config file is loaded. if (vm.contains("rpc_ip")) { - auto endpoint = beast::IP::Endpoint::fromStringChecked(vm["rpc_ip"].as()); + auto endpoint = beast::ip::Endpoint::fromStringChecked(vm["rpc_ip"].as()); if (!endpoint) { std::cerr << "Invalid rpc_ip = " << vm["rpc_ip"].as() << "\n"; @@ -826,7 +826,7 @@ run(int argc, char** argv) // We have an RPC command to process: beast::setCurrentThreadName("xrpld: rpc"); - return RPCCall::fromCommandLine( + return rpc_call::fromCommandLine( *config, vm["parameters"].as>(), *logs); // LCOV_EXCL_STOP } diff --git a/src/xrpld/app/misc/DeliverMax.h b/src/xrpld/app/misc/DeliverMax.h index 73ccc95800..1683219c8b 100644 --- a/src/xrpld/app/misc/DeliverMax.h +++ b/src/xrpld/app/misc/DeliverMax.h @@ -6,7 +6,7 @@ namespace json { class Value; } // namespace json -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Copy `Amount` field to `DeliverMax` field in transaction output JSON. @@ -22,4 +22,4 @@ insertDeliverMax(json::Value& txJson, TxType txnType, unsigned int apiVersion); /** @} */ -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 47cbebb901..8f31ce1eb3 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -1215,7 +1215,7 @@ NetworkOPsImp::processClusterTimer() n.set_nodename(node.name()); }); - Resource::Gossip const gossip = registry_.get().getResourceManager().exportConsumers(); + resource::Gossip const gossip = registry_.get().getResourceManager().exportConsumers(); for (auto& item : gossip.items) { protocol::TMLoadSource& node = *cluster.add_loadsources(); @@ -2487,7 +2487,7 @@ NetworkOPsImp::pubValidation(std::shared_ptr const& val) // for consumers supporting different API versions MultiApiJson multiObj{jvObj}; multiObj.visit( - RPC::kApiVersion<1>, // + rpc::kApiVersion<1>, // [](json::Value& jvTx) { // Type conversion for older API versions to string if (jvTx.isMember(jss::ledger_index)) @@ -2688,7 +2688,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters) if (!registry_.get().getApp().config().serverDomain.empty()) info[jss::server_domain] = registry_.get().getApp().config().serverDomain; - info[jss::build_version] = BuildInfo::getVersionString(); + info[jss::build_version] = build_info::getVersionString(); info[jss::server_state] = strOperatingMode(admin); @@ -3167,7 +3167,7 @@ NetworkOPsImp::pubLedger(std::shared_ptr const& lpAccepted) if (!streamMaps_[SBookChanges].empty()) { - json::Value const jvObj = xrpl::RPC::computeBookChanges(lpAccepted); + json::Value const jvObj = xrpl::rpc::computeBookChanges(lpAccepted); auto it = streamMaps_[SBookChanges].begin(); while (it != streamMaps_[SBookChanges].end()) @@ -3281,9 +3281,9 @@ NetworkOPsImp::transJson( if (meta) { jvObj[jss::meta] = meta->get().getJson(JsonOptions::Values::None); - RPC::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get()); - RPC::insertNFTSyntheticInJson(jvObj, transaction, meta->get()); - RPC::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get()); + rpc::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get()); + rpc::insertNFTSyntheticInJson(jvObj, transaction, meta->get()); + rpc::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get()); } // add CTID where the needed data for it exists @@ -3295,7 +3295,7 @@ NetworkOPsImp::transJson( if (transaction->isFieldPresent(sfNetworkID)) netID = transaction->getFieldU32(sfNetworkID); - if (std::optional ctid = RPC::encodeCTID(ledger->header().seq, txnSeq, netID); + if (std::optional ctid = rpc::encodeCTID(ledger->header().seq, txnSeq, netID); ctid) jvObj[jss::ctid] = *ctid; } @@ -3346,7 +3346,7 @@ NetworkOPsImp::transJson( forAllApiVersions( multiObj.visit(), // [&](json::Value& jvTx, std::integral_constant) { - RPC::insertDeliverMax(jvTx[jss::transaction], transaction->getTxnType(), Version); + rpc::insertDeliverMax(jvTx[jss::transaction], transaction->getTxnType(), Version); if constexpr (Version > 1) { @@ -3891,7 +3891,7 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo) int feeChargeCount = 0; if (auto sptr = subInfo.sinkWptr.lock(); sptr) { - sptr->getConsumer().charge(Resource::kFeeMediumBurdenRpc); + sptr->getConsumer().charge(resource::kFeeMediumBurdenRpc); ++feeChargeCount; } else diff --git a/src/xrpld/app/misc/detail/DeliverMax.cpp b/src/xrpld/app/misc/detail/DeliverMax.cpp index add3cf89ee..e512b078c7 100644 --- a/src/xrpld/app/misc/detail/DeliverMax.cpp +++ b/src/xrpld/app/misc/detail/DeliverMax.cpp @@ -3,7 +3,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { void insertDeliverMax(json::Value& txJson, TxType txnType, unsigned int apiVersion) @@ -19,4 +19,4 @@ insertDeliverMax(json::Value& txJson, TxType txnType, unsigned int apiVersion) } } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/app/misc/detail/Transaction.cpp b/src/xrpld/app/misc/detail/Transaction.cpp index e29181bfe9..59cc4b6c4c 100644 --- a/src/xrpld/app/misc/detail/Transaction.cpp +++ b/src/xrpld/app/misc/detail/Transaction.cpp @@ -182,7 +182,7 @@ Transaction::getJson(JsonOptions options, bool binary) const if (txnSeq_ && netID) { - std::optional const ctid = RPC::encodeCTID(ledgerIndex_, *txnSeq_, *netID); + std::optional const ctid = rpc::encodeCTID(ledgerIndex_, *txnSeq_, *netID); if (ctid) ret[jss::ctid] = *ctid; } diff --git a/src/xrpld/app/misc/detail/WorkBase.h b/src/xrpld/app/misc/detail/WorkBase.h index 73e5081036..fd36cb5318 100644 --- a/src/xrpld/app/misc/detail/WorkBase.h +++ b/src/xrpld/app/misc/detail/WorkBase.h @@ -223,7 +223,7 @@ WorkBase::onStart() req_.target(path_.empty() ? "/" : path_); req_.version(11); req_.set("Host", host_ + ":" + port_); - req_.set("User-Agent", BuildInfo::getFullVersionString()); + req_.set("User-Agent", build_info::getFullVersionString()); req_.prepare_payload(); boost::beast::http::async_write( impl().stream(), diff --git a/src/xrpld/app/rdb/PeerFinder.h b/src/xrpld/app/rdb/PeerFinder.h index 5d916000a3..3ff7b7268b 100644 --- a/src/xrpld/app/rdb/PeerFinder.h +++ b/src/xrpld/app/rdb/PeerFinder.h @@ -44,6 +44,6 @@ readPeerFinderDB(soci::session& session, std::function const& v); +savePeerFinderDB(soci::session& session, std::vector const& v); } // namespace xrpl diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp b/src/xrpld/app/rdb/detail/PeerFinder.cpp index 72a275c7cd..8d9af69ae3 100644 --- a/src/xrpld/app/rdb/detail/PeerFinder.cpp +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp @@ -108,7 +108,7 @@ updatePeerFinderDB(soci::session& session, int currentSchemaVersion, beast::Jour std::size_t count = 0; session << "SELECT COUNT(*) FROM PeerFinder_BootstrapCache;", soci::into(count); - std::vector list; + std::vector list; { list.reserve(count); @@ -125,8 +125,8 @@ updatePeerFinderDB(soci::session& session, int currentSchemaVersion, beast::Jour st.execute(); while (st.fetch()) { - PeerFinder::Store::Entry entry; - entry.endpoint = beast::IP::Endpoint::fromString(s); + peer_finder::Store::Entry entry; + entry.endpoint = beast::ip::Endpoint::fromString(s); if (!isUnspecified(entry.endpoint)) { entry.valence = valence; @@ -226,7 +226,7 @@ readPeerFinderDB(soci::session& session, std::function const& v) +savePeerFinderDB(soci::session& session, std::vector const& v) { soci::transaction tr(session); session << "DELETE FROM PeerFinder_BootstrapCache;"; diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 852e46218a..d43a7a566d 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -277,7 +277,7 @@ public: std::size_t txRelayPercentage = 25; // These override the command line client settings - std::optional rpcIp; + std::optional rpcIp; std::unordered_set> features; diff --git a/src/xrpld/overlay/Overlay.h b/src/xrpld/overlay/Overlay.h index 6cc229f5a0..9ab80e6697 100644 --- a/src/xrpld/overlay/Overlay.h +++ b/src/xrpld/overlay/Overlay.h @@ -55,7 +55,7 @@ public: explicit Setup() = default; std::shared_ptr context; - beast::IP::Address publicIp; + beast::ip::Address publicIp; int ipLimit = 0; std::uint32_t crawlOptions = 0; std::optional networkID; @@ -92,7 +92,7 @@ public: * performed asynchronously. */ virtual void - connect(beast::IP::Endpoint const& address) = 0; + connect(beast::ip::Endpoint const& address) = 0; /** * Returns the maximum number of peers we are configured to allow. diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h index 20a8730cf1..c2631cc7ce 100644 --- a/src/xrpld/overlay/Peer.h +++ b/src/xrpld/overlay/Peer.h @@ -15,9 +15,9 @@ namespace xrpl { -namespace Resource { +namespace resource { class Charge; -} // namespace Resource +} // namespace resource enum class ProtocolFeature { ValidatorListPropagation, @@ -51,7 +51,7 @@ public: virtual void send(std::shared_ptr const& m) = 0; - [[nodiscard]] virtual beast::IP::Endpoint + [[nodiscard]] virtual beast::ip::Endpoint getRemoteAddress() const = 0; /** @@ -76,7 +76,7 @@ public: * Adjust this peer's load balance based on the type of load imposed. */ virtual void - charge(Resource::Charge const& fee, std::string const& context) = 0; + charge(resource::Charge const& fee, std::string const& context) = 0; // // Identity diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 0f0b3242de..b78b8eb7b8 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -49,10 +49,10 @@ ConnectAttempt::ConnectAttempt( Application& app, boost::asio::io_context& ioContext, endpoint_type remoteEndpoint, - Resource::Consumer usage, + resource::Consumer usage, shared_context const& context, Peer::id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, beast::Journal journal, OverlayImpl& overlay) : Child(overlay) @@ -462,7 +462,7 @@ ConnectAttempt::processResponse() auto const result = overlay_.peerFinder().activate(slot_, publicKey, static_cast(member)); - if (result != PeerFinder::Result::Success) + if (result != peer_finder::Result::Success) { fail("Outbound " + std::string(to_string(result))); return; diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h b/src/xrpld/overlay/detail/ConnectAttempt.h index f9ba33571f..3ebffc529d 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.h +++ b/src/xrpld/overlay/detail/ConnectAttempt.h @@ -41,7 +41,7 @@ private: beast::WrappedSink sink_; beast::Journal const journal_; endpoint_type remoteEndpoint_; - Resource::Consumer usage_; + resource::Consumer usage_; boost::asio::strand strand_; boost::asio::basic_waitable_timer timer_; std::unique_ptr streamPtr_; @@ -49,7 +49,7 @@ private: stream_type& stream_; boost::beast::multi_buffer readBuf_; response_type response_; - std::shared_ptr slot_; + std::shared_ptr slot_; request_type req_; public: @@ -57,10 +57,10 @@ public: Application& app, boost::asio::io_context& ioContext, endpoint_type remoteEndpoint, - Resource::Consumer usage, + resource::Consumer usage, shared_context const& context, Peer::id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, beast::Journal journal, OverlayImpl& overlay); @@ -102,7 +102,7 @@ private: static boost::asio::ip::tcp::endpoint parseEndpoint(std::string const& s, boost::system::error_code& ec) { - beast::IP::Endpoint bep; + beast::ip::Endpoint bep; std::istringstream is(s); is >> bep; if (is.fail()) diff --git a/src/xrpld/overlay/detail/Handshake.cpp b/src/xrpld/overlay/detail/Handshake.cpp index a860d2d604..a12923d1c3 100644 --- a/src/xrpld/overlay/detail/Handshake.cpp +++ b/src/xrpld/overlay/detail/Handshake.cpp @@ -186,8 +186,8 @@ buildHandshake( boost::beast::http::fields& h, xrpl::uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, Application& app) { if (networkID) @@ -213,7 +213,7 @@ buildHandshake( if (!app.config().serverDomain.empty()) h.insert("Server-Domain", app.config().serverDomain); - if (beast::IP::isPublic(remoteIp)) + if (beast::ip::isPublic(remoteIp)) h.insert("Remote-IP", remoteIp.to_string()); if (!publicIp.is_unspecified()) @@ -231,8 +231,8 @@ verifyHandshake( boost::beast::http::fields const& headers, xrpl::uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remote, + beast::ip::Address publicIp, + beast::ip::Address remote, Application& app) { if (auto const iter = headers.find("Server-Domain"); iter != headers.end()) @@ -331,7 +331,7 @@ verifyHandshake( if (ec) throw std::runtime_error("Invalid Local-IP"); - if (beast::IP::isPublic(remote) && remote != localIp) + if (beast::ip::isPublic(remote) && remote != localIp) { throw std::runtime_error( "Incorrect Local-IP: " + remote.to_string() + " instead of " + localIp.to_string()); @@ -346,7 +346,7 @@ verifyHandshake( if (ec) throw std::runtime_error("Invalid Remote-IP"); - if (beast::IP::isPublic(remote) && !beast::IP::isUnspecified(publicIp)) + if (beast::ip::isPublic(remote) && !beast::ip::isUnspecified(publicIp)) { // We know our public IP and peer reports our connection came // from some other IP. @@ -374,7 +374,7 @@ makeRequest( m.method(boost::beast::http::verb::get); m.target("/"); m.version(11); - m.insert("User-Agent", BuildInfo::getFullVersionString()); + m.insert("User-Agent", build_info::getFullVersionString()); m.insert("Upgrade", supportedProtocolVersions()); m.insert("Connection", "Upgrade"); m.insert("Connect-As", "Peer"); @@ -390,8 +390,8 @@ http_response_type makeResponse( bool crawlPublic, http_request_type const& req, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, uint256 const& sharedValue, std::optional networkID, ProtocolVersion protocol, @@ -403,7 +403,7 @@ makeResponse( resp.insert("Connection", "Upgrade"); resp.insert("Upgrade", to_string(protocol)); resp.insert("Connect-As", "Peer"); - resp.insert("Server", BuildInfo::getFullVersionString()); + resp.insert("Server", build_info::getFullVersionString()); resp.insert("Crawl", crawlPublic ? "public" : "private"); resp.insert( "X-Protocol-Ctl", diff --git a/src/xrpld/overlay/detail/Handshake.h b/src/xrpld/overlay/detail/Handshake.h index 9a4e5ba507..d54cd3a0ea 100644 --- a/src/xrpld/overlay/detail/Handshake.h +++ b/src/xrpld/overlay/detail/Handshake.h @@ -47,8 +47,8 @@ buildHandshake( boost::beast::http::fields& h, uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, Application& app); /** @@ -68,8 +68,8 @@ verifyHandshake( boost::beast::http::fields const& headers, uint256 const& sharedValue, std::optional networkID, - beast::IP::Address publicIp, - beast::IP::Address remote, + beast::ip::Address publicIp, + beast::ip::Address remote, Application& app); /** @@ -109,8 +109,8 @@ http_response_type makeResponse( bool crawlPublic, http_request_type const& req, - beast::IP::Address publicIp, - beast::IP::Address remoteIp, + beast::ip::Address publicIp, + beast::ip::Address remoteIp, uint256 const& sharedValue, std::optional networkID, ProtocolVersion version, diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index f9972548d1..81ead14111 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -93,13 +93,13 @@ namespace xrpl { -namespace CrawlOptions { +namespace crawl_options { static constexpr auto kDisabled = 0; static constexpr auto kOverlay = (1 << 0); static constexpr auto kServerInfo = (1 << 1); static constexpr auto kServerCounts = (1 << 2); static constexpr auto kUnl = (1 << 3); -} // namespace CrawlOptions +} // namespace crawl_options //------------------------------------------------------------------------------ @@ -155,7 +155,7 @@ OverlayImpl::Timer::onTimer(error_code ec) if (overlay_.app_.config().txReduceRelayEnable) overlay_.sendTxQueue(); - if ((++overlay_.timerCount_ % Tuning::kCheckIdlePeers) == 0) + if ((++overlay_.timerCount_ % tuning::kCheckIdlePeers) == 0) overlay_.deleteIdlePeers(); asyncWait(); @@ -167,7 +167,7 @@ OverlayImpl::OverlayImpl( Application& app, Setup setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, @@ -182,7 +182,7 @@ OverlayImpl::OverlayImpl( , resourceManager_(resourceManager) , store_(app_.getJournal("PeerFinder")) , peerFinder_( - PeerFinder::makeManager( + peer_finder::makeManager( ioContext, stopwatch(), app_.getJournal("PeerFinder"), @@ -308,7 +308,7 @@ OverlayImpl::onHandoff( bool const reserved = static_cast(app_.getCluster().member(publicKey)) || app_.getPeerReservations().contains(publicKey); auto const result = peerFinder_->activate(slot, publicKey, reserved); - if (result != PeerFinder::Result::Success) + if (result != peer_finder::Result::Success) { peerFinder_->onClosed(slot); JLOG(journal.debug()) @@ -381,14 +381,14 @@ OverlayImpl::makePrefix(std::uint32_t id) std::shared_ptr OverlayImpl::makeRedirectResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress) { boost::beast::http::response msg; msg.version(request.version()); msg.result(boost::beast::http::status::service_unavailable); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); { std::ostringstream ostr; ostr << remoteAddress; @@ -408,7 +408,7 @@ OverlayImpl::makeRedirectResponse( std::shared_ptr OverlayImpl::makeErrorResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress, std::string const& text) @@ -417,7 +417,7 @@ OverlayImpl::makeErrorResponse( msg.version(request.version()); msg.result(boost::beast::http::status::bad_request); msg.reason("Bad Request (" + text + ")"); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Remote-Address", remoteAddress.to_string()); msg.insert(boost::beast::http::field::connection, "close"); msg.prepare_payload(); @@ -427,7 +427,7 @@ OverlayImpl::makeErrorResponse( //------------------------------------------------------------------------------ void -OverlayImpl::connect(beast::IP::Endpoint const& remoteEndpoint) +OverlayImpl::connect(beast::ip::Endpoint const& remoteEndpoint) { XRPL_ASSERT(work_, "xrpl::OverlayImpl::connect : work is set"); @@ -497,7 +497,7 @@ OverlayImpl::addActive(std::shared_ptr const& peer) } void -OverlayImpl::remove(std::shared_ptr const& slot) +OverlayImpl::remove(std::shared_ptr const& slot) { std::scoped_lock const lock(mutex_); auto const iter = peers_.find(slot); @@ -508,7 +508,7 @@ OverlayImpl::remove(std::shared_ptr const& slot) void OverlayImpl::start() { - PeerFinder::Config const config = PeerFinder::makeConfig( + peer_finder::Config const config = peer_finder::makeConfig( app_.config(), serverHandler_.setup().overlay.port(), app_.getValidationPublicKey().has_value(), @@ -541,7 +541,7 @@ OverlayImpl::start() resolver_.resolve( bootstrapIps, - [this](std::string const& name, std::vector const& addresses) { + [this](std::string const& name, std::vector const& addresses) { std::vector ips; ips.reserve(addresses.size()); for (auto const& addr : addresses) @@ -566,8 +566,8 @@ OverlayImpl::start() { resolver_.resolve( app_.config().ipsFixed, - [this](std::string const& name, std::vector const& addresses) { - std::vector ips; + [this](std::string const& name, std::vector const& addresses) { + std::vector ips; ips.reserve(addresses.size()); for (auto& addr : addresses) @@ -868,30 +868,30 @@ OverlayImpl::json() bool OverlayImpl::processCrawl(http_request_type const& req, Handoff& handoff) { - if (req.target() != "/crawl" || setup_.crawlOptions == CrawlOptions::kDisabled) + if (req.target() != "/crawl" || setup_.crawlOptions == crawl_options::kDisabled) return false; boost::beast::http::response msg; msg.version(req.version()); msg.result(boost::beast::http::status::ok); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "application/json"); msg.insert("Connection", "close"); msg.body()["version"] = json::Value(2u); - if ((setup_.crawlOptions & CrawlOptions::kOverlay) != 0u) + if ((setup_.crawlOptions & crawl_options::kOverlay) != 0u) { msg.body()["overlay"] = getOverlayInfo(); } - if ((setup_.crawlOptions & CrawlOptions::kServerInfo) != 0u) + if ((setup_.crawlOptions & crawl_options::kServerInfo) != 0u) { msg.body()["server"] = getServerInfo(); } - if ((setup_.crawlOptions & CrawlOptions::kServerCounts) != 0u) + if ((setup_.crawlOptions & crawl_options::kServerCounts) != 0u) { msg.body()["counts"] = getServerCounts(); } - if ((setup_.crawlOptions & CrawlOptions::kUnl) != 0u) + if ((setup_.crawlOptions & crawl_options::kUnl) != 0u) { msg.body()["unl"] = getUnlInfo(); } @@ -915,7 +915,7 @@ OverlayImpl::processValidatorList(http_request_type const& req, Handoff& handoff boost::beast::http::response msg; msg.version(req.version()); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "application/json"); msg.insert("Connection", "close"); @@ -972,7 +972,7 @@ OverlayImpl::processHealth(http_request_type const& req, Handoff& handoff) return false; boost::beast::http::response msg; msg.version(req.version()); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "application/json"); msg.insert("Connection", "close"); @@ -1535,7 +1535,7 @@ setupOverlay(BasicConfig const& config, beast::Journal j) { boost::system::error_code ec; setup.publicIp = boost::asio::ip::make_address(ip, ec); - if (ec || !beast::IP::isPublic(setup.publicIp)) + if (ec || !beast::ip::isPublic(setup.publicIp)) Throw("Configured public IP is invalid"); } @@ -1577,19 +1577,19 @@ setupOverlay(BasicConfig const& config, beast::Journal j) { if (get(section, Keys::kOverlay, true)) { - setup.crawlOptions |= CrawlOptions::kOverlay; + setup.crawlOptions |= crawl_options::kOverlay; } if (get(section, Keys::kServer, true)) { - setup.crawlOptions |= CrawlOptions::kServerInfo; + setup.crawlOptions |= crawl_options::kServerInfo; } if (get(section, Keys::kCounts, false)) { - setup.crawlOptions |= CrawlOptions::kServerCounts; + setup.crawlOptions |= crawl_options::kServerCounts; } if (get(section, Keys::kUnl, true)) { - setup.crawlOptions |= CrawlOptions::kUnl; + setup.crawlOptions |= crawl_options::kUnl; } } } @@ -1632,7 +1632,7 @@ makeOverlay( Application& app, Overlay::Setup const& setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, diff --git a/src/xrpld/overlay/detail/OverlayImpl.h b/src/xrpld/overlay/detail/OverlayImpl.h index cd2c7d630b..f274bbba5a 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.h +++ b/src/xrpld/overlay/detail/OverlayImpl.h @@ -109,11 +109,11 @@ private: Setup setup_; beast::Journal const journal_; ServerHandler& serverHandler_; - Resource::Manager& resourceManager_; - PeerFinder::StoreSqdb store_; - std::unique_ptr peerFinder_; + resource::Manager& resourceManager_; + peer_finder::StoreSqdb store_; + std::unique_ptr peerFinder_; TrafficCount traffic_; - hash_map, std::weak_ptr> peers_; + hash_map, std::weak_ptr> peers_; hash_map> ids_; Resolver& resolver_; std::atomic nextId_; @@ -141,7 +141,7 @@ public: Application& app, Setup setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, @@ -157,13 +157,13 @@ public: void stop() override; - PeerFinder::Manager& + peer_finder::Manager& peerFinder() { return *peerFinder_; } - Resource::Manager& + resource::Manager& resourceManager() { return resourceManager_; @@ -182,7 +182,7 @@ public: endpoint_type remoteEndpoint) override; void - connect(beast::IP::Endpoint const& remoteEndpoint) override; + connect(beast::ip::Endpoint const& remoteEndpoint) override; int limit() override; @@ -252,7 +252,7 @@ public: addActive(std::shared_ptr const& peer); void - remove(std::shared_ptr const& slot); + remove(std::shared_ptr const& slot); /** * Called when a peer has connected successfully @@ -451,13 +451,13 @@ private: std::shared_ptr makeRedirectResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress); static std::shared_ptr makeErrorResponse( - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type const& request, address_type remoteAddress, std::string const& msg); diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 688d0ac314..ca6fb180d4 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -125,11 +125,11 @@ constexpr std::chrono::seconds kPeerTimerInterval{60}; PeerImp::PeerImp( Application& app, id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay) : Child(overlay) @@ -157,7 +157,7 @@ PeerImp::PeerImp( , creationTime_(clock_type::now()) , squelch_(app_.getJournal("Squelch")) , usage_(consumer) - , fee_{.fee = Resource::kFeeTrivialPeer, .context = ""} + , fee_{.fee = resource::kFeeTrivialPeer, .context = ""} , slot_(slot) , request_(std::move(request)) , headers_(request_) @@ -303,7 +303,7 @@ PeerImp::send(std::shared_ptr const& m) auto sendqSize = self->sendQueue_.size(); - if (sendqSize < Tuning::kTargetSendQueue) + if (sendqSize < tuning::kTargetSendQueue) { // To detect a peer that does not read from their // side of the connection, we expect a peer to have @@ -312,7 +312,7 @@ PeerImp::send(std::shared_ptr const& m) } else if ( auto sink = self->journal_.debug(); - sink && (sendqSize % Tuning::kSendQueueLogFreq) == 0) + sink && (sendqSize % tuning::kSendQueueLogFreq) == 0) { std::string const n = self->name(); sink << n << " sendq: " << sendqSize; @@ -374,10 +374,10 @@ PeerImp::removeTxQueue(uint256 const& hash) } void -PeerImp::charge(Resource::Charge const& fee, std::string const& context) +PeerImp::charge(resource::Charge const& fee, std::string const& context) { dispatch(strand_, [self = shared_from_this(), fee, context]() { - if ((self->usage_.charge(fee, context) == Resource::Disposition::Drop) && + if ((self->usage_.charge(fee, context) == resource::Disposition::Drop) && self->usage_.disconnect(self->pJournal_)) { // Idempotent: only the first worker to observe Drop counts the @@ -718,7 +718,7 @@ PeerImp::onTimer(error_code const& ec) return; } - if (largeSendq_++ >= Tuning::kSendqIntervals) + if (largeSendq_++ >= tuning::kSendqIntervals) { fail("Large send queue"); return; @@ -948,7 +948,7 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytesTransferred) readBuffer_.commit(bytesTransferred); - auto hint = Tuning::kReadBufferBytes; + auto hint = tuning::kReadBufferBytes; while (readBuffer_.size() > 0) { @@ -980,7 +980,7 @@ PeerImp::onReadMessage(error_code ec, std::size_t bytesTransferred) // Timeout on writes only stream_.async_read_some( - readBuffer_.prepare(std::max(Tuning::kReadBufferBytes, hint)), + readBuffer_.prepare(std::max(tuning::kReadBufferBytes, hint)), bind_executor( strand_, [self = shared_from_this()](error_code const& ec, std::size_t bytesTransferred) { @@ -1056,7 +1056,7 @@ PeerImp::onMessageBegin( { auto const name = protocolMessageName(type); loadEvent_ = app_.getJobQueue().makeLoadEvent(JtPeer, name); - fee_ = {.fee = Resource::kFeeTrivialPeer, .context = name}; + fee_ = {.fee = resource::kFeeTrivialPeer, .context = name}; auto const category = TrafficCount::categorize(*m, static_cast(type), true); @@ -1100,12 +1100,12 @@ PeerImp::onMessage(std::shared_ptr const& m) if (s == 0) { - fee_.update(Resource::kFeeUselessData, "empty"); + fee_.update(resource::kFeeUselessData, "empty"); return; } if (s > 100) - fee_.update(Resource::kFeeModerateBurdenPeer, "oversize"); + fee_.update(resource::kFeeModerateBurdenPeer, "oversize"); app_.getJobQueue().addJob(JtManifest, "RcvManifests", [this, that = shared_from_this(), m]() { overlay_.onManifests(m, that); @@ -1118,7 +1118,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (m->type() == protocol::TMPing::ptPING) { // We have received a ping request, reply with a pong - fee_.update(Resource::kFeeModerateBurdenPeer, "ping request"); + fee_.update(resource::kFeeModerateBurdenPeer, "ping request"); m->set_type(protocol::TMPing::ptPONG); send(std::make_shared(*m, protocol::mtPING)); return; @@ -1159,7 +1159,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // VFALCO NOTE I think we should drop the peer immediately if (!cluster()) { - fee_.update(Resource::kFeeUselessData, "unknown cluster"); + fee_.update(resource::kFeeUselessData, "unknown cluster"); return; } @@ -1186,15 +1186,15 @@ PeerImp::onMessage(std::shared_ptr const& m) int const loadSources = m->loadsources().size(); if (loadSources != 0) { - Resource::Gossip gossip; + resource::Gossip gossip; gossip.items.reserve(loadSources); for (int i = 0; i < m->loadsources().size(); ++i) { protocol::TMLoadSource const& node = m->loadsources(i); - Resource::Gossip::Item item; - item.address = beast::IP::Endpoint::fromString(node.name()); + resource::Gossip::Item item; + item.address = beast::ip::Endpoint::fromString(node.name()); item.balance = node.cost(); - if (item.address != beast::IP::Endpoint()) + if (item.address != beast::ip::Endpoint()) gossip.items.push_back(item); } overlay_.resourceManager().importConsumers(name(), gossip); @@ -1234,17 +1234,17 @@ PeerImp::onMessage(std::shared_ptr const& m) // implication for the protocol. if (m->endpoints_v2().size() >= 1024) { - fee_.update(Resource::kFeeUselessData, "endpoints too large"); + fee_.update(resource::kFeeUselessData, "endpoints too large"); return; } - std::vector endpoints; + std::vector endpoints; endpoints.reserve(m->endpoints_v2().size()); auto malformed = 0; for (auto const& tm : m->endpoints_v2()) { - auto result = beast::IP::Endpoint::fromStringChecked(tm.endpoint()); + auto result = beast::ip::Endpoint::fromStringChecked(tm.endpoint()); if (!result) { @@ -1256,7 +1256,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // If hops == 0, this Endpoint describes the peer we are connected // to -- in that case, we take the remote address seen on the - // socket and store that in the IP::Endpoint. If this is the first + // socket and store that in the ip::Endpoint. If this is the first // time, then we'll verify that their listener can receive incoming // by performing a connectivity test. if hops > 0, then we just // take the address/port we were given @@ -1271,7 +1271,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (malformed > 0) { fee_.update( - Resource::kFeeInvalidData * malformed, + resource::kFeeInvalidData * malformed, std::to_string(malformed) + " malformed endpoints"); } @@ -1332,7 +1332,7 @@ PeerImp::handleTransaction( { JLOG(pJournal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (handleTransaction)."; - fee_.update(Resource::kFeeModerateBurdenPeer, "inner batch txn"); + fee_.update(resource::kFeeModerateBurdenPeer, "inner batch txn"); return; } // LCOV_EXCL_STOP @@ -1345,7 +1345,7 @@ PeerImp::handleTransaction( // we have seen this transaction recently if (any(flags & HashRouterFlags::BAD)) { - fee_.update(Resource::kFeeUselessData, "known bad"); + fee_.update(resource::kFeeUselessData, "known bad"); JLOG(pJournal_.debug()) << "Ignoring known bad tx " << txID; } @@ -1419,7 +1419,7 @@ void PeerImp::onMessage(std::shared_ptr const& m) { auto badData = [&](std::string const& msg) { - fee_.update(Resource::kFeeInvalidData, "get_ledger " + msg); + fee_.update(resource::kFeeInvalidData, "get_ledger " + msg); JLOG(pJournal_.warn()) << "TMGetLedger: " << msg; }; auto const itype{m->itype()}; @@ -1499,7 +1499,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // Verify query depth if (m->has_querydepth()) { - if (m->querydepth() > Tuning::kMaxQueryDepth || itype == protocol::liBASE) + if (m->querydepth() > tuning::kMaxQueryDepth || itype == protocol::liBASE) { badData("Invalid query depth"); return; @@ -1517,10 +1517,10 @@ PeerImp::onMessage(std::shared_ptr const& m) bool tooManyNodeIds = false; if (itype != protocol::liBASE) { - nodeIDs.reserve(std::min(m->nodeids_size(), Tuning::kSoftMaxReplyNodes)); + nodeIDs.reserve(std::min(m->nodeids_size(), tuning::kSoftMaxReplyNodes)); for (auto const& nodeId : m->nodeids()) { - if (nodeIDs.size() >= Tuning::kSoftMaxReplyNodes) + if (nodeIDs.size() >= tuning::kSoftMaxReplyNodes) { // The peer requested too many node IDs. Continue processing the received node // IDs up to the limit. If the request is legitimate then at least they will get @@ -1531,7 +1531,7 @@ PeerImp::onMessage(std::shared_ptr const& m) auto parsed = deserializeSHAMapNodeID(nodeId); if (!parsed) { - peer->charge(Resource::kFeeInvalidData, "TMGetLedger: Invalid node ID"); + peer->charge(resource::kFeeInvalidData, "TMGetLedger: Invalid node ID"); return; } nodeIDs.push_back(std::move(*parsed)); @@ -1543,7 +1543,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // below is skipped for relay responses. if (tooManyNodeIds) { - peer->charge(Resource::kFeeModerateBurdenPeer, "TMGetLedger: too many node IDs"); + peer->charge(resource::kFeeModerateBurdenPeer, "TMGetLedger: too many node IDs"); // Truncate the request to what was actually parsed and charged for, so that if this // request ends up being relayed to another peer, we don't forward the oversized list. @@ -1553,7 +1553,7 @@ PeerImp::onMessage(std::shared_ptr const& m) } if (!m->has_requestcookie()) { - peer->charge(Resource::kFeeModerateBurdenPeer, "TMGetLedger: get ledger request"); + peer->charge(resource::kFeeModerateBurdenPeer, "TMGetLedger: get ledger request"); } peer->processLedgerRequest(m, std::move(nodeIDs)); @@ -1566,11 +1566,11 @@ PeerImp::onMessage(std::shared_ptr const& m) JLOG(pJournal_.trace()) << "onMessage, TMProofPathRequest"; if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "proof_path_request disabled"); + fee_.update(resource::kFeeMalformedRequest, "proof_path_request disabled"); return; } - fee_.update(Resource::kFeeModerateBurdenPeer, "received a proof path request"); + fee_.update(resource::kFeeModerateBurdenPeer, "received a proof path request"); std::weak_ptr const weak = shared_from_this(); app_.getJobQueue().addJob(JtReplayReq, "RcvProofPReq", [weak, m]() { if (auto peer = weak.lock()) @@ -1580,11 +1580,11 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (reply.error() == protocol::TMReplyError::reBAD_REQUEST) { - peer->charge(Resource::kFeeMalformedRequest, "proof_path_request"); + peer->charge(resource::kFeeMalformedRequest, "proof_path_request"); } else { - peer->charge(Resource::kFeeRequestNoReply, "proof_path_request"); + peer->charge(resource::kFeeRequestNoReply, "proof_path_request"); } } else @@ -1600,13 +1600,13 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "proof_path_response disabled"); + fee_.update(resource::kFeeMalformedRequest, "proof_path_response disabled"); return; } if (!ledgerReplayMsgHandler_.processProofPathResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "proof_path_response"); + fee_.update(resource::kFeeInvalidData, "proof_path_response"); } } @@ -1616,11 +1616,11 @@ PeerImp::onMessage(std::shared_ptr const& m) JLOG(pJournal_.trace()) << "onMessage, TMReplayDeltaRequest"; if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "replay_delta_request disabled"); + fee_.update(resource::kFeeMalformedRequest, "replay_delta_request disabled"); return; } - fee_.fee = Resource::kFeeModerateBurdenPeer; + fee_.fee = resource::kFeeModerateBurdenPeer; std::weak_ptr const weak = shared_from_this(); app_.getJobQueue().addJob(JtReplayReq, "RcvReplDReq", [weak, m]() { if (auto peer = weak.lock()) @@ -1630,11 +1630,11 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (reply.error() == protocol::TMReplyError::reBAD_REQUEST) { - peer->charge(Resource::kFeeMalformedRequest, "replay_delta_request"); + peer->charge(resource::kFeeMalformedRequest, "replay_delta_request"); } else { - peer->charge(Resource::kFeeRequestNoReply, "replay_delta_request"); + peer->charge(resource::kFeeRequestNoReply, "replay_delta_request"); } } else @@ -1650,13 +1650,13 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (!ledgerReplayEnabled_) { - fee_.update(Resource::kFeeMalformedRequest, "replay_delta_response disabled"); + fee_.update(resource::kFeeMalformedRequest, "replay_delta_response disabled"); return; } if (!ledgerReplayMsgHandler_.processReplayDeltaResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "replay_delta_response"); + fee_.update(resource::kFeeInvalidData, "replay_delta_response"); } } @@ -1664,7 +1664,7 @@ void PeerImp::onMessage(std::shared_ptr const& m) { auto badData = [&](std::string const& msg) { - fee_.update(Resource::kFeeInvalidData, msg); + fee_.update(resource::kFeeInvalidData, msg); JLOG(pJournal_.warn()) << "TMLedgerData: " << msg; }; @@ -1715,7 +1715,7 @@ PeerImp::onMessage(std::shared_ptr const& m) } // Verify ledger nodes. - if (m->nodes_size() <= 0 || m->nodes_size() > Tuning::kHardMaxReplyNodes) + if (m->nodes_size() <= 0 || m->nodes_size() > tuning::kHardMaxReplyNodes) { badData("Invalid Ledger/TXset nodes " + std::to_string(m->nodes_size())); return; @@ -1875,14 +1875,14 @@ PeerImp::onMessage(std::shared_ptr const& m) (publicKeyType(makeSlice(set.nodepubkey())) != KeyType::Secp256k1)) { JLOG(pJournal_.warn()) << "Proposal: malformed"; - fee_.update(Resource::kFeeInvalidSignature, " signature can't be longer than 72 bytes"); + fee_.update(resource::kFeeInvalidSignature, " signature can't be longer than 72 bytes"); return; } if (!stringIsUInt256Sized(set.currenttxhash()) || !stringIsUInt256Sized(set.previousledger())) { JLOG(pJournal_.warn()) << "Proposal: malformed"; - fee_.update(Resource::kFeeMalformedRequest, "bad hashes"); + fee_.update(resource::kFeeMalformedRequest, "bad hashes"); return; } @@ -2164,13 +2164,13 @@ PeerImp::checkTracking(std::uint32_t seq1, std::uint32_t seq2) { std::uint32_t const diff = std::max(seq1, seq2) - std::min(seq1, seq2); - if (diff < Tuning::kConvergedLedgerLimit) + if (diff < tuning::kConvergedLedgerLimit) { // The peer's ledger sequence is close to the validation's tracking_ = Tracking::Converged; } - if ((diff > Tuning::kDivergedLedgerLimit) && (tracking_.load() != Tracking::Diverged)) + if ((diff > tuning::kDivergedLedgerLimit) && (tracking_.load() != Tracking::Diverged)) { // The peer's ledger sequence is way off the validation's std::scoped_lock const sl(recentLock_); @@ -2185,7 +2185,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { if (!stringIsUInt256Sized(m->hash())) { - fee_.update(Resource::kFeeMalformedRequest, "bad hash"); + fee_.update(resource::kFeeMalformedRequest, "bad hash"); return; } @@ -2197,7 +2197,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (std::ranges::find(recentTxSets_, hash) != recentTxSets_.end()) { - fee_.update(Resource::kFeeUselessData, "duplicate (tsHAVE)"); + fee_.update(resource::kFeeUselessData, "duplicate (tsHAVE)"); return; } @@ -2218,7 +2218,7 @@ PeerImp::onValidatorListMessage( { JLOG(pJournal_.warn()) << "Ignored malformed " << messageType; // This shouldn't ever happen with a well-behaved peer - fee_.update(Resource::kFeeHeavyBurdenPeer, "no blobs"); + fee_.update(resource::kFeeHeavyBurdenPeer, "no blobs"); return; } @@ -2232,7 +2232,7 @@ PeerImp::onValidatorListMessage( // Charging this fee here won't hurt the peer in the normal // course of operation (ie. refresh every 5 minutes), but // will add up if the peer is misbehaving. - fee_.update(Resource::kFeeUselessData, "duplicate"); + fee_.update(resource::kFeeUselessData, "duplicate"); return; } @@ -2323,27 +2323,27 @@ PeerImp::onValidatorListMessage( // Charging this fee here won't hurt the peer in the normal // course of operation (ie. refresh every 5 minutes), but // will add up if the peer is misbehaving. - fee_.update(Resource::kFeeUselessData, " duplicate (same_sequence or known_sequence)"); + fee_.update(resource::kFeeUselessData, " duplicate (same_sequence or known_sequence)"); break; case ListDisposition::Stale: // There are very few good reasons for a peer to send an // old list, particularly more than once. - fee_.update(Resource::kFeeInvalidData, "expired"); + fee_.update(resource::kFeeInvalidData, "expired"); break; case ListDisposition::Untrusted: // Charging this fee here won't hurt the peer in the normal // course of operation (ie. refresh every 5 minutes), but // will add up if the peer is misbehaving. - fee_.update(Resource::kFeeUselessData, "untrusted"); + fee_.update(resource::kFeeUselessData, "untrusted"); break; case ListDisposition::Invalid: // This shouldn't ever happen with a well-behaved peer - fee_.update(Resource::kFeeInvalidSignature, "invalid list disposition"); + fee_.update(resource::kFeeInvalidSignature, "invalid list disposition"); break; case ListDisposition::UnsupportedVersion: // During a version transition, this may be legitimate. // If it happens frequently, that's probably bad. - fee_.update(Resource::kFeeInvalidData, "version"); + fee_.update(resource::kFeeInvalidData, "version"); break; // LCOV_EXCL_START default: @@ -2411,7 +2411,7 @@ PeerImp::onMessage(std::shared_ptr const& m) JLOG(pJournal_.debug()) << "ValidatorList: received validator list from peer using " << "protocol version " << to_string(protocol_) << " which shouldn't support this feature."; - fee_.update(Resource::kFeeUselessData, "unsupported peer"); + fee_.update(resource::kFeeUselessData, "unsupported peer"); return; } onValidatorListMessage( @@ -2421,7 +2421,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { JLOG(pJournal_.warn()) << "ValidatorList: Exception, " << e.what(); using namespace std::string_literals; - fee_.update(Resource::kFeeInvalidData, e.what()); + fee_.update(resource::kFeeInvalidData, e.what()); } } @@ -2435,7 +2435,7 @@ PeerImp::onMessage(std::shared_ptr const& m JLOG(pJournal_.debug()) << "ValidatorListCollection: received validator list from peer " << "using protocol version " << to_string(protocol_) << " which shouldn't support this feature."; - fee_.update(Resource::kFeeUselessData, "unsupported peer"); + fee_.update(resource::kFeeUselessData, "unsupported peer"); return; } if (m->version() < 2) @@ -2444,7 +2444,7 @@ PeerImp::onMessage(std::shared_ptr const& m << "ValidatorListCollection: received invalid validator list " "version " << m->version() << " from peer using protocol version " << to_string(protocol_); - fee_.update(Resource::kFeeInvalidData, "wrong version"); + fee_.update(resource::kFeeInvalidData, "wrong version"); return; } onValidatorListMessage( @@ -2454,7 +2454,7 @@ PeerImp::onMessage(std::shared_ptr const& m { JLOG(pJournal_.warn()) << "ValidatorListCollection: Exception, " << e.what(); using namespace std::string_literals; - fee_.update(Resource::kFeeInvalidData, e.what()); + fee_.update(resource::kFeeInvalidData, e.what()); } } @@ -2464,7 +2464,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (m->validation().size() < 50) { JLOG(pJournal_.warn()) << "Validation: Too small"; - fee_.update(Resource::kFeeMalformedRequest, "too small"); + fee_.update(resource::kFeeMalformedRequest, "too small"); return; } @@ -2491,7 +2491,7 @@ PeerImp::onMessage(std::shared_ptr const& m) val->getSeenTime())) { JLOG(pJournal_.trace()) << "Validation: Not current"; - fee_.update(Resource::kFeeUselessData, "not current"); + fee_.update(resource::kFeeUselessData, "not current"); return; } @@ -2560,7 +2560,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { JLOG(pJournal_.warn()) << "Exception processing validation: " << e.what(); using namespace std::string_literals; - fee_.update(Resource::kFeeMalformedRequest, e.what()); + fee_.update(resource::kFeeMalformedRequest, e.what()); } } @@ -2575,7 +2575,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (packet.query()) { // this is a query - if (sendQueue_.size() >= Tuning::kDropSendQueue) + if (sendQueue_.size() >= tuning::kDropSendQueue) { JLOG(pJournal_.debug()) << "GetObject: Large send queue"; return; @@ -2592,7 +2592,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!txReduceRelayEnabled()) { JLOG(pJournal_.error()) << "TMGetObjectByHash: tx reduce-relay is disabled"; - fee_.update(Resource::kFeeMalformedRequest, "disabled"); + fee_.update(resource::kFeeMalformedRequest, "disabled"); return; } @@ -2609,19 +2609,19 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!stringIsUInt256Sized(packet.ledgerhash())) { JLOG(pJournal_.debug()) << "GetObj: malformed ledgerhash from peer " << id_; - fee_.update(Resource::kFeeMalformedRequest, "get object ledger hash"); + 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) + 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"); + << " > " << tuning::kHardMaxReplyNodes << ")"; + fee_.update(resource::kFeeInvalidData, "oversized get object request"); return; } @@ -2643,7 +2643,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // 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"); + peer->charge(resource::kFeeRequestNoReply, "get object handler exception"); } }); if (!queued) @@ -2660,7 +2660,7 @@ PeerImp::onMessage(std::shared_ptr const& m) // 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"); + fee_.update(resource::kFeeModerateBurdenPeer, "received a get object by hash request"); } else { @@ -2739,7 +2739,7 @@ PeerImp::processGetObjectByHash(std::shared_ptr con // 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); + int const iterLimit = std::min(requested, tuning::kHardMaxReplyNodes); for (int i = 0; i < iterLimit; ++i) { @@ -2770,7 +2770,7 @@ PeerImp::processGetObjectByHash(std::shared_ptr con // 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)); + // 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"); @@ -2785,7 +2785,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!txReduceRelayEnabled()) { JLOG(pJournal_.error()) << "TMHaveTransactions: tx reduce-relay is disabled"; - fee_.update(Resource::kFeeMalformedRequest, "disabled"); + fee_.update(resource::kFeeMalformedRequest, "disabled"); return; } @@ -2810,7 +2810,7 @@ PeerImp::handleHaveTransactions(std::shared_ptr co if (!stringIsUInt256Sized(m->hashes(i))) { JLOG(pJournal_.error()) << "TMHaveTransactions with invalid hash size"; - fee_.update(Resource::kFeeMalformedRequest, "hash size"); + fee_.update(resource::kFeeMalformedRequest, "hash size"); return; } @@ -2848,7 +2848,7 @@ PeerImp::onMessage(std::shared_ptr const& m) if (!txReduceRelayEnabled()) { JLOG(pJournal_.error()) << "TMTransactions: tx reduce-relay is disabled"; - fee_.update(Resource::kFeeMalformedRequest, "disabled"); + fee_.update(resource::kFeeMalformedRequest, "disabled"); return; } @@ -2872,14 +2872,14 @@ PeerImp::onMessage(std::shared_ptr const& m) dispatch(strand_, [self = shared_from_this(), m]() { if (!m->has_validatorpubkey()) { - self->fee_.update(Resource::kFeeInvalidData, "squelch no pubkey"); + self->fee_.update(resource::kFeeInvalidData, "squelch no pubkey"); return; } auto validator = m->validatorpubkey(); auto const slice{makeSlice(validator)}; if (!publicKeyType(slice)) { - self->fee_.update(Resource::kFeeInvalidData, "squelch bad pubkey"); + self->fee_.update(resource::kFeeInvalidData, "squelch bad pubkey"); return; } PublicKey const key(slice); @@ -2899,7 +2899,7 @@ PeerImp::onMessage(std::shared_ptr const& m) } else if (!self->squelch_.addSquelch(key, std::chrono::seconds{duration})) { - self->fee_.update(Resource::kFeeInvalidData, "squelch duration"); + self->fee_.update(resource::kFeeInvalidData, "squelch duration"); } JLOG(self->pJournal_.debug()) @@ -2939,11 +2939,11 @@ PeerImp::doFetchPack(std::shared_ptr const& packet) if (!stringIsUInt256Sized(packet->ledgerhash())) { JLOG(pJournal_.warn()) << "FetchPack hash size malformed"; - fee_.update(Resource::kFeeMalformedRequest, "hash size"); + fee_.update(resource::kFeeMalformedRequest, "hash size"); return; } - fee_.fee = Resource::kFeeHeavyBurdenPeer; + fee_.fee = resource::kFeeHeavyBurdenPeer; uint256 const hash = uint256::fromRaw(packet->ledgerhash()); @@ -2966,7 +2966,7 @@ PeerImp::doTransactions(std::shared_ptr const& pack if (packet->objects_size() > reduce_relay::kMaxTxQueueSize) { JLOG(pJournal_.error()) << "doTransactions, invalid number of hashes"; - fee_.update(Resource::kFeeMalformedRequest, "too big"); + fee_.update(resource::kFeeMalformedRequest, "too big"); return; } @@ -2976,7 +2976,7 @@ PeerImp::doTransactions(std::shared_ptr const& pack if (!stringIsUInt256Sized(obj.hash())) { - fee_.update(Resource::kFeeMalformedRequest, "hash size"); + fee_.update(resource::kFeeMalformedRequest, "hash size"); return; } @@ -2988,7 +2988,7 @@ PeerImp::doTransactions(std::shared_ptr const& pack { JLOG(pJournal_.error()) << "doTransactions, transaction not found " << Slice(hash.data(), hash.size()); - fee_.update(Resource::kFeeMalformedRequest, "tx not found"); + fee_.update(resource::kFeeMalformedRequest, "tx not found"); return; } @@ -3039,7 +3039,7 @@ PeerImp::checkTransaction( { JLOG(pJournal_.warn()) << "Ignoring Network relayed Tx containing " "tfInnerBatchTxn (checkSignature)."; - charge(Resource::kFeeModerateBurdenPeer, "inner batch txn"); + charge(resource::kFeeModerateBurdenPeer, "inner batch txn"); return; } // LCOV_EXCL_STOP @@ -3051,7 +3051,7 @@ PeerImp::checkTransaction( JLOG(pJournal_.info()) << "Marking transaction " << stx->getTransactionID() << "as BAD because it's expired"; app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); - charge(Resource::kFeeUselessData, "expired tx"); + charge(resource::kFeeUselessData, "expired tx"); return; } @@ -3082,7 +3082,7 @@ PeerImp::checkTransaction( if (!batch) { JLOG(pJournal_.debug()) << "Charging for pseudo-transaction tx " << tx->getID(); - charge(Resource::kFeeUselessData, "pseudo tx"); + charge(resource::kFeeUselessData, "pseudo tx"); } return; @@ -3104,7 +3104,7 @@ PeerImp::checkTransaction( // Probably not necessary to set HashRouterFlags::BAD, but // doesn't hurt. app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); - charge(Resource::kFeeInvalidSignature, "check transaction signature failure"); + charge(resource::kFeeInvalidSignature, "check transaction signature failure"); return; } } @@ -3123,7 +3123,7 @@ PeerImp::checkTransaction( JLOG(pJournal_.debug()) << "Exception checking transaction: " << reason; } app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); - charge(Resource::kFeeInvalidSignature, "tx (impossible)"); + charge(resource::kFeeInvalidSignature, "tx (impossible)"); return; } @@ -3135,7 +3135,7 @@ PeerImp::checkTransaction( JLOG(pJournal_.warn()) << "Exception in " << __func__ << ": " << ex.what(); app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD); using namespace std::string_literals; - charge(Resource::kFeeInvalidData, "tx "s + ex.what()); + charge(resource::kFeeInvalidData, "tx "s + ex.what()); } } @@ -3154,7 +3154,7 @@ PeerImp::checkPropose( { std::string const desc{"Proposal fails sig check"}; JLOG(pJournal_.warn()) << desc; - charge(Resource::kFeeInvalidSignature, desc); + charge(resource::kFeeInvalidSignature, desc); return; } @@ -3198,7 +3198,7 @@ PeerImp::checkValidation( { std::string const desc{"Validation forwarded by peer is invalid"}; JLOG(pJournal_.debug()) << desc; - charge(Resource::kFeeInvalidSignature, desc); + charge(resource::kFeeInvalidSignature, desc); return; } @@ -3223,7 +3223,7 @@ PeerImp::checkValidation( { JLOG(pJournal_.trace()) << "Exception processing validation: " << ex.what(); using namespace std::string_literals; - charge(Resource::kFeeMalformedRequest, "validation "s + ex.what()); + charge(resource::kFeeMalformedRequest, "validation "s + ex.what()); } } @@ -3380,7 +3380,7 @@ PeerImp::getLedger(std::shared_ptr const& m) { // Do not resource charge a peer responding to a relay if (!m->has_requestcookie()) - charge(Resource::kFeeMalformedRequest, "get_ledger ledgerSeq"); + charge(resource::kFeeMalformedRequest, "get_ledger ledgerSeq"); ledger.reset(); JLOG(pJournal_.warn()) << "getLedger: Invalid ledger sequence " << ledgerSeq; @@ -3462,7 +3462,7 @@ PeerImp::processLedgerRequest( } else { - if (sendQueue_.size() >= Tuning::kDropSendQueue) + if (sendQueue_.size() >= tuning::kDropSendQueue) { JLOG(pJournal_.debug()) << "processLedgerRequest: Large send queue"; return; @@ -3522,12 +3522,12 @@ PeerImp::processLedgerRequest( auto const queryDepth{m->has_querydepth() ? m->querydepth() : defaultDepth}; std::vector data; - data.reserve(Tuning::kSoftMaxReplyNodes); + data.reserve(tuning::kSoftMaxReplyNodes); auto const useLedgerNodeDepth = supportsFeature(ProtocolFeature::LedgerNodeDepth); for (auto const& nodeID : nodeIDs) { - if (ledgerData.nodes_size() >= Tuning::kSoftMaxReplyNodes) + if (ledgerData.nodes_size() >= tuning::kSoftMaxReplyNodes) break; data.clear(); @@ -3541,7 +3541,7 @@ PeerImp::processLedgerRequest( for (auto const& d : data) { - if (ledgerData.nodes_size() >= Tuning::kHardMaxReplyNodes) + if (ledgerData.nodes_size() >= tuning::kHardMaxReplyNodes) break; protocol::TMLedgerNode* node{ledgerData.add_nodes()}; @@ -3639,30 +3639,30 @@ PeerImp::processLedgerRequest( // // 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 +resource::Charge PeerImp::computeGetObjectByHashFee(int const requested, int const found) { - int const billable = std::max(0, requested - static_cast(Tuning::kFreeObjectsPerRequest)); + 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) + int sizeBand = tuning::kCostBandSmall; + if (requested > tuning::kBandMediumMax) { - sizeBand = Tuning::kCostBandLarge; + sizeBand = tuning::kCostBandLarge; } - else if (requested > Tuning::kBandSmallMax) + else if (requested > tuning::kBandSmallMax) { - sizeBand = Tuning::kCostBandMedium; + sizeBand = tuning::kCostBandMedium; } - int const dynamic = (billableHits * Tuning::kCostPerLookupHit) + - (billableMisses * Tuning::kCostPerLookupMiss) + sizeBand; + int const dynamic = (billableHits * tuning::kCostPerLookupHit) + + (billableMisses * tuning::kCostPerLookupMiss) + sizeBand; - return Resource::Charge(dynamic, "GetObject differential"); + return resource::Charge(dynamic, "GetObject differential"); } int diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index de90e60955..3fcfe6359a 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -97,7 +97,7 @@ private: // Updated at each stage of the connection process to reflect // the current conditions as closely as possible. - beast::IP::Endpoint const remoteAddress_; + beast::ip::Endpoint const remoteAddress_; // These are up here to prevent warnings about order of initializations // @@ -161,11 +161,11 @@ private: struct ChargeWithContext { - Resource::Charge fee = Resource::kFeeTrivialPeer; + resource::Charge fee = resource::kFeeTrivialPeer; std::string context{}; // NOLINT(readability-redundant-member-init) void - update(Resource::Charge f, std::string const& add) + update(resource::Charge f, std::string const& add) { XRPL_ASSERT(f >= fee, "xrpl::PeerImp::ChargeWithContext::update : fee increases"); fee = f; @@ -179,7 +179,7 @@ private: std::mutex mutable recentLock_; protocol::TMStatusChange lastStatus_; - Resource::Consumer usage_; + resource::Consumer usage_; ChargeWithContext fee_; // One-shot guard so concurrent JobQueue workers cannot double-count @@ -187,7 +187,7 @@ private: // 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_; + std::shared_ptr const slot_; boost::beast::multi_buffer readBuffer_; http_request_type request_; http_response_type response_; @@ -258,11 +258,11 @@ public: PeerImp( Application& app, id_t id, - std::shared_ptr const& slot, + std::shared_ptr const& slot, http_request_type&& request, PublicKey const& publicKey, ProtocolVersion protocol, - Resource::Consumer consumer, + resource::Consumer consumer, std::unique_ptr&& streamPtr, OverlayImpl& overlay); @@ -275,9 +275,9 @@ public: Application& app, std::unique_ptr&& streamPtr, Buffers const& buffers, - std::shared_ptr&& slot, + std::shared_ptr&& slot, http_response_type&& response, - Resource::Consumer usage, + resource::Consumer usage, PublicKey const& publicKey, ProtocolVersion protocol, id_t id, @@ -291,7 +291,7 @@ public: return pJournal_; } - std::shared_ptr const& + std::shared_ptr const& slot() { return slot_; @@ -339,16 +339,18 @@ public: void sendEndpoints(FwdIt first, FwdIt last) requires( - std::is_same_v::value_type, PeerFinder::Endpoint>); + std::is_same_v< // + typename std::iterator_traits::value_type, + peer_finder::Endpoint>); - beast::IP::Endpoint + beast::ip::Endpoint getRemoteAddress() const override { return remoteAddress_; } void - charge(Resource::Charge const& fee, std::string const& context) override; + charge(resource::Charge const& fee, std::string const& context) override; // // Identity @@ -697,7 +699,7 @@ protected: * * 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` + * 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. * @@ -711,25 +713,25 @@ protected: * request based on how much work was actually performed. * * The charge has three components on top of the base - * `Resource::kFeeModerateBurdenPeer`: + * `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 + * 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 + * exceed `tuning::kHardMaxReplyNodes` when this * helper is called directly, even though processing - * caps the iterations to `Tuning::kHardMaxReplyNodes`. + * 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. + * @return A `resource::Charge` whose cost reflects the work performed. */ - static Resource::Charge + static resource::Charge computeGetObjectByHashFee(int const requested, int const found); /** @@ -740,9 +742,9 @@ protected: * 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_`. + * @return The current `resource::Charge` accumulated on `fee_`. */ - Resource::Charge + resource::Charge currentFeeCharge() const { return fee_.fee; @@ -756,9 +758,9 @@ PeerImp::PeerImp( Application& app, std::unique_ptr&& streamPtr, Buffers const& buffers, - std::shared_ptr&& slot, + std::shared_ptr&& slot, http_response_type&& response, - Resource::Consumer usage, + resource::Consumer usage, PublicKey const& publicKey, ProtocolVersion protocol, id_t id, @@ -788,7 +790,7 @@ PeerImp::PeerImp( , creationTime_(clock_type::now()) , squelch_(app_.getJournal("Squelch")) , usage_(usage) - , fee_{.fee = Resource::kFeeTrivialPeer} + , fee_{.fee = resource::kFeeTrivialPeer} , slot_(std::move(slot)) , response_(std::move(response)) , headers_(response_) @@ -815,7 +817,8 @@ PeerImp::PeerImp( template void PeerImp::sendEndpoints(FwdIt first, FwdIt last) - requires(std::is_same_v::value_type, PeerFinder::Endpoint>) + requires( + std::is_same_v::value_type, peer_finder::Endpoint>) { protocol::TMEndpoints tm; diff --git a/src/xrpld/overlay/detail/Tuning.h b/src/xrpld/overlay/detail/Tuning.h index 5488fab07b..7561a4f385 100644 --- a/src/xrpld/overlay/detail/Tuning.h +++ b/src/xrpld/overlay/detail/Tuning.h @@ -5,7 +5,7 @@ #include #include -namespace xrpl::Tuning { +namespace xrpl::tuning { /** * How many ledgers off a server can be and we will @@ -74,7 +74,7 @@ constexpr std::size_t kReadBufferBytes = 16384; * 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. + * `resource::kDropThreshold` and gets disconnected. * * The numbers below are picked to keep three things true given * `kDropThreshold = 25000`: @@ -165,4 +165,4 @@ static constexpr auto kLegitHashesPerType = 4; static constexpr auto kBandSmallMax = kLegitHashesPerType * SHAMapInnerNode::kBranchFactor; static constexpr auto kBandMediumMax = kBandSmallMax * SHAMapInnerNode::kBranchFactor; -} // namespace xrpl::Tuning +} // namespace xrpl::tuning diff --git a/src/xrpld/overlay/make_Overlay.h b/src/xrpld/overlay/make_Overlay.h index a62d4b49de..c730a05c54 100644 --- a/src/xrpld/overlay/make_Overlay.h +++ b/src/xrpld/overlay/make_Overlay.h @@ -27,7 +27,7 @@ makeOverlay( Application& app, Overlay::Setup const& setup, ServerHandler& serverHandler, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, Resolver& resolver, boost::asio::io_context& ioContext, BasicConfig const& config, diff --git a/src/xrpld/peerfinder/PeerfinderManager.h b/src/xrpld/peerfinder/PeerfinderManager.h index f96ea31943..5934e3bc4a 100644 --- a/src/xrpld/peerfinder/PeerfinderManager.h +++ b/src/xrpld/peerfinder/PeerfinderManager.h @@ -6,7 +6,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { Config makeConfig( @@ -16,4 +16,4 @@ makeConfig( int ipLimit, bool verifyEndpoints); -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp index a893b969e0..b222f6c077 100644 --- a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp @@ -5,7 +5,7 @@ #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { Config makeConfig( @@ -36,4 +36,4 @@ makeConfig( verifyEndpoints); } -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h b/src/xrpld/peerfinder/detail/StoreSqdb.h index b0973a42b3..ce13d72c15 100644 --- a/src/xrpld/peerfinder/detail/StoreSqdb.h +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h @@ -14,7 +14,7 @@ #include #include -namespace xrpl::PeerFinder { +namespace xrpl::peer_finder { /** * Database persistence for PeerFinder using SQLite @@ -50,7 +50,7 @@ public: std::size_t n(0); readPeerFinderDB(sqlDb_, [&](std::string const& s, int valence) { - beast::IP::Endpoint const endpoint(beast::IP::Endpoint::fromString(s)); + beast::ip::Endpoint const endpoint(beast::ip::Endpoint::fromString(s)); if (!isUnspecified(endpoint)) { @@ -90,4 +90,4 @@ private: } }; -} // namespace xrpl::PeerFinder +} // namespace xrpl::peer_finder diff --git a/src/xrpld/perflog/detail/PerfLogImp.h b/src/xrpld/perflog/detail/PerfLogImp.h index 14477512ff..7efdbe1b7f 100644 --- a/src/xrpld/perflog/detail/PerfLogImp.h +++ b/src/xrpld/perflog/detail/PerfLogImp.h @@ -109,7 +109,7 @@ class PerfLogImp : public PerfLog Application& app_; beast::Journal const j_; std::function const signalStop_; - Counters counters_{xrpl::RPC::getHandlerNames(), JobTypes::instance()}; + Counters counters_{xrpl::rpc::getHandlerNames(), JobTypes::instance()}; std::ofstream logFile_; std::thread thread_; std::mutex mutex_; diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h index 3c10ece78f..16f7ea8e43 100644 --- a/src/xrpld/rpc/BookChanges.h +++ b/src/xrpld/rpc/BookChanges.h @@ -32,7 +32,7 @@ class Transaction; class TxMeta; class STTx; -namespace RPC { +namespace rpc { template json::Value @@ -233,5 +233,5 @@ computeBookChanges(std::shared_ptr const& lpAccepted) return jvObj; } -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/CTID.h b/src/xrpld/rpc/CTID.h index 7566e0e143..71ded5834f 100644 --- a/src/xrpld/rpc/CTID.h +++ b/src/xrpld/rpc/CTID.h @@ -11,7 +11,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { // CTID stands for Concise Transaction ID. // @@ -111,4 +111,4 @@ decodeCTID(T const ctid) noexcept return std::make_tuple(ledgerSeq, txnIndex, networkID); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/Context.h b/src/xrpld/rpc/Context.h index 81ba068d8f..e85bf6dfe6 100644 --- a/src/xrpld/rpc/Context.h +++ b/src/xrpld/rpc/Context.h @@ -18,7 +18,7 @@ class Application; class NetworkOPs; class LedgerMaster; -namespace RPC { +namespace rpc { /** * The context of information needed to call an RPC. @@ -27,10 +27,10 @@ struct Context { beast::Journal const j; Application& app; - Resource::Charge& loadType; + resource::Charge& loadType; NetworkOPs& netOps; LedgerMaster& ledgerMaster; - Resource::Consumer& consumer; + resource::Consumer& consumer; Role role; std::shared_ptr coro; InfoSub::pointer infoSub; @@ -59,5 +59,5 @@ struct GRPCContext : public Context RequestType params; }; -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/DeliveredAmount.h b/src/xrpld/rpc/DeliveredAmount.h index dc635c6861..a7878070d6 100644 --- a/src/xrpld/rpc/DeliveredAmount.h +++ b/src/xrpld/rpc/DeliveredAmount.h @@ -17,7 +17,7 @@ class Transaction; class TxMeta; class STTx; -namespace RPC { +namespace rpc { struct JsonContext; @@ -41,23 +41,23 @@ insertDeliveredAmount( void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const&, + rpc::JsonContext const&, std::shared_ptr const&, TxMeta const&); void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const&, + rpc::JsonContext const&, std::shared_ptr const&, TxMeta const&); std::optional getDeliveredAmount( - RPC::Context const& context, + rpc::Context const& context, std::shared_ptr const& serializedTx, TxMeta const& transactionMeta, LedgerIndex const& ledgerIndex); /** @} */ -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/GRPCHandlers.h b/src/xrpld/rpc/GRPCHandlers.h index 9dc7e0b13a..cabd55d53b 100644 --- a/src/xrpld/rpc/GRPCHandlers.h +++ b/src/xrpld/rpc/GRPCHandlers.h @@ -14,22 +14,22 @@ namespace xrpl { /* * These handlers are for gRPC. They each take in a protobuf message that is - * nested inside RPC::GRPCContext, where T is the request type + * nested inside rpc::GRPCContext, where T is the request type * The return value is the response type, as well as a status * If the status is not Status::OK (meaning an error occurred), then only * the status will be sent to the client, and the response will be omitted */ std::pair -doLedgerGrpc(RPC::GRPCContext& context); +doLedgerGrpc(rpc::GRPCContext& context); std::pair -doLedgerEntryGrpc(RPC::GRPCContext& context); +doLedgerEntryGrpc(rpc::GRPCContext& context); std::pair -doLedgerDataGrpc(RPC::GRPCContext& context); +doLedgerDataGrpc(rpc::GRPCContext& context); std::pair -doLedgerDiffGrpc(RPC::GRPCContext& context); +doLedgerDiffGrpc(rpc::GRPCContext& context); } // namespace xrpl diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h b/src/xrpld/rpc/MPTokenIssuanceID.h index f56826bfb8..678fda0369 100644 --- a/src/xrpld/rpc/MPTokenIssuanceID.h +++ b/src/xrpld/rpc/MPTokenIssuanceID.h @@ -8,7 +8,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Add a `mpt_issuance_id` field to the `meta` input/output parameter. @@ -32,4 +32,4 @@ insertMPTokenIssuanceID( TxMeta const& transactionMeta); /** @} */ -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/Output.h b/src/xrpld/rpc/Output.h index 30b5c090d7..f528efef56 100644 --- a/src/xrpld/rpc/Output.h +++ b/src/xrpld/rpc/Output.h @@ -5,7 +5,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { using Output = std::function; @@ -15,4 +15,4 @@ stringOutput(std::string& s) return [&](boost::string_ref const& b) { s.append(b.data(), b.size()); }; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/RPCCall.h b/src/xrpld/rpc/RPCCall.h index a72b35e344..2fec78f93b 100644 --- a/src/xrpld/rpc/RPCCall.h +++ b/src/xrpld/rpc/RPCCall.h @@ -26,7 +26,7 @@ namespace xrpl { /** * Processes XRPL RPC calls. */ -namespace RPCCall { +namespace rpc_call { int fromCommandLine(Config const& config, std::vector const& vCmd, Logs& logs); @@ -47,7 +47,7 @@ fromNetwork( std::function callbackFuncP = std::function(), std::unordered_map headers = {}); -} // namespace RPCCall +} // namespace rpc_call json::Value rpcCmdToJson( diff --git a/src/xrpld/rpc/RPCHandler.h b/src/xrpld/rpc/RPCHandler.h index fcd0f54265..637a492943 100644 --- a/src/xrpld/rpc/RPCHandler.h +++ b/src/xrpld/rpc/RPCHandler.h @@ -8,7 +8,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { struct JsonContext; @@ -16,9 +16,9 @@ struct JsonContext; * Execute an RPC command and store the results in a json::Value. */ Status -doCommand(RPC::JsonContext&, json::Value&); +doCommand(rpc::JsonContext&, json::Value&); Role roleRequired(unsigned int version, bool betaEnabled, std::string const& method); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/Role.h b/src/xrpld/rpc/Role.h index 2c2ae6b781..48c89333bd 100644 --- a/src/xrpld/rpc/Role.h +++ b/src/xrpld/rpc/Role.h @@ -40,13 +40,13 @@ requestRole( Role const& required, Port const& port, json::Value const& params, - beast::IP::Endpoint const& remoteIp, + beast::ip::Endpoint const& remoteIp, std::string_view user); -Resource::Consumer +resource::Consumer requestInboundEndpoint( - Resource::Manager& manager, - beast::IP::Endpoint const& remoteAddress, + resource::Manager& manager, + beast::ip::Endpoint const& remoteAddress, Role const& role, std::string_view user, std::string_view forwardedFor); @@ -66,7 +66,7 @@ isUnlimited(Role const& role); */ bool ipAllowed( - beast::IP::Address const& remoteIp, + beast::ip::Address const& remoteIp, std::vector const& nets4, std::vector const& nets6); diff --git a/src/xrpld/rpc/ServerHandler.h b/src/xrpld/rpc/ServerHandler.h index 054bec9b5b..a09fc1c18a 100644 --- a/src/xrpld/rpc/ServerHandler.h +++ b/src/xrpld/rpc/ServerHandler.h @@ -80,7 +80,7 @@ private: using stream_type = boost::beast::ssl_stream; Application& app_; - Resource::Manager& resourceManager_; + resource::Manager& resourceManager_; beast::Journal journal_; NetworkOPs& networkOPs_; std::unique_ptr server_; @@ -109,7 +109,7 @@ private: boost::asio::io_context&, JobQueue&, NetworkOPs&, - Resource::Manager&, + resource::Manager&, CollectorManager& cm); public: @@ -120,7 +120,7 @@ public: boost::asio::io_context& ioContext, JobQueue& jobQueue, NetworkOPs& networkOPs, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, CollectorManager& cm); ~ServerHandler(); @@ -196,7 +196,7 @@ private: processRequest( Port const& port, std::string const& request, - beast::IP::Endpoint const& remoteIPAddress, + beast::ip::Endpoint const& remoteIPAddress, Output const&, std::shared_ptr coro, std::string_view forwardedFor, @@ -215,7 +215,7 @@ makeServerHandler( boost::asio::io_context&, JobQueue&, NetworkOPs&, - Resource::Manager&, + resource::Manager&, CollectorManager& cm); } // namespace xrpl diff --git a/src/xrpld/rpc/Status.h b/src/xrpld/rpc/Status.h index dda1e89d31..e2716bd579 100644 --- a/src/xrpld/rpc/Status.h +++ b/src/xrpld/rpc/Status.h @@ -11,7 +11,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { /** * Status represents the results of an operation that might fail. @@ -56,9 +56,11 @@ public: { } - /* Returns a representation of the integer status Code as a string. - If the Status is OK, the result is an empty string. - */ + /** + * If the Status is OK, the result is an empty string. + * + * @return a representation of the integer status Code as a string. + */ [[nodiscard]] std::string codeString() const; @@ -86,7 +88,7 @@ public: [[nodiscard]] TER toTER() const { - XRPL_ASSERT(type_ == Type::TER, "xrpl::RPC::Status::toTER : type is TER"); + XRPL_ASSERT(type_ == Type::TER, "xrpl::rpc::Status::toTER : type is TER"); return TER::fromInt(code_); } @@ -97,7 +99,8 @@ public: [[nodiscard]] ErrorCodeI toErrorCode() const { - XRPL_ASSERT(type_ == Type::ErrorCodeI, "xrpl::RPC::Status::toTER : type is error code"); + XRPL_ASSERT( + type_ == Type::ErrorCodeI, "xrpl::rpc::Status::toErrorCode : type is error code"); return ErrorCodeI(code_); } @@ -155,4 +158,4 @@ private: Strings messages_; }; -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/DeliveredAmount.cpp b/src/xrpld/rpc/detail/DeliveredAmount.cpp index 8d8aac33bf..7b8c5e0623 100644 --- a/src/xrpld/rpc/detail/DeliveredAmount.cpp +++ b/src/xrpld/rpc/detail/DeliveredAmount.cpp @@ -16,7 +16,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { /* GetLedgerIndex and GetCloseTime are lambdas that allow the close time and @@ -114,7 +114,7 @@ insertDeliveredAmount( template static std::optional getDeliveredAmount( - RPC::Context const& context, + rpc::Context const& context, std::shared_ptr const& serializedTx, TxMeta const& transactionMeta, GetLedgerIndex const& getLedgerIndex) @@ -133,7 +133,7 @@ getDeliveredAmount( std::optional getDeliveredAmount( - RPC::Context const& context, + rpc::Context const& context, std::shared_ptr const& serializedTx, TxMeta const& transactionMeta, LedgerIndex const& ledgerIndex) @@ -145,7 +145,7 @@ getDeliveredAmount( void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const& context, + rpc::JsonContext const& context, std::shared_ptr const& transaction, TxMeta const& transactionMeta) { @@ -155,7 +155,7 @@ insertDeliveredAmount( void insertDeliveredAmount( json::Value& meta, - RPC::JsonContext const& context, + rpc::JsonContext const& context, std::shared_ptr const& transaction, TxMeta const& transactionMeta) { @@ -178,4 +178,4 @@ insertDeliveredAmount( } } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/Handler.cpp b/src/xrpld/rpc/detail/Handler.cpp index 4f5ce34c1f..326af4f4ee 100644 --- a/src/xrpld/rpc/detail/Handler.cpp +++ b/src/xrpld/rpc/detail/Handler.cpp @@ -18,7 +18,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace { /** @@ -33,8 +33,8 @@ byRef(Function const& f) if (result.type() != json::ValueType::Object) { // LCOV_EXCL_START - UNREACHABLE("xrpl::RPC::byRef : result is object"); - result = RPC::makeObjectValue(result); + UNREACHABLE("xrpl::rpc::byRef : result is object"); + result = rpc::makeObjectValue(result); // LCOV_EXCL_STOP } @@ -49,7 +49,7 @@ handle(JsonContext& context, Object& object) XRPL_ASSERT( context.apiVersion >= HandlerImpl::minApiVer && context.apiVersion <= HandlerImpl::maxApiVer, - "xrpl::RPC::handle : valid API version"); + "xrpl::rpc::handle : valid API version"); HandlerImpl handler(context); auto status = handler.check(); @@ -382,10 +382,10 @@ private: unsigned minVer, unsigned maxVer) { - XRPL_ASSERT(minVer <= maxVer, "xrpl::RPC::HandlerTable : valid API version range"); + XRPL_ASSERT(minVer <= maxVer, "xrpl::rpc::HandlerTable : valid API version range"); XRPL_ASSERT( - maxVer <= RPC::kApiMaximumValidVersion, - "xrpl::RPC::HandlerTable : valid max API version"); + maxVer <= rpc::kApiMaximumValidVersion, + "xrpl::rpc::HandlerTable : valid max API version"); return std::any_of( range.first, @@ -427,8 +427,8 @@ public: [[nodiscard]] Handler const* getHandler(unsigned version, bool betaEnabled, std::string const& name) const { - if (version < RPC::kApiMinimumSupportedVersion || - version > (betaEnabled ? RPC::kApiBetaVersion : RPC::kApiMaximumSupportedVersion)) + if (version < rpc::kApiMinimumSupportedVersion || + version > (betaEnabled ? rpc::kApiBetaVersion : rpc::kApiMaximumSupportedVersion)) return nullptr; auto const range = table_.equal_range(name); @@ -457,8 +457,8 @@ private: addHandler() { static_assert(HandlerImpl::minApiVer <= HandlerImpl::maxApiVer); - static_assert(HandlerImpl::maxApiVer <= RPC::kApiMaximumValidVersion); - static_assert(RPC::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer); + static_assert(HandlerImpl::maxApiVer <= rpc::kApiMaximumValidVersion); + static_assert(rpc::kApiMinimumSupportedVersion <= HandlerImpl::minApiVer); if (overlappingApiVersion( table_.equal_range(HandlerImpl::name), @@ -488,4 +488,4 @@ getHandlerNames() return HandlerTable::instance().getHandlerNames(); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/Handler.h b/src/xrpld/rpc/detail/Handler.h index 37259c8648..7342c5fcbf 100644 --- a/src/xrpld/rpc/detail/Handler.h +++ b/src/xrpld/rpc/detail/Handler.h @@ -20,7 +20,7 @@ namespace json { class Object; } // namespace json -namespace xrpl::RPC { +namespace xrpl::rpc { // Under what condition can we call this RPC? enum class Condition { @@ -38,7 +38,7 @@ struct Handler char const* name; Method valueMethod; Role role; - RPC::Condition condition; + rpc::Condition condition; unsigned minApiVer = kApiMinimumSupportedVersion; unsigned maxApiVer = kApiMaximumValidVersion; @@ -92,7 +92,7 @@ conditionMet(Condition conditionRequired, T& context) if (!context.app.config().standalone() && conditionRequired != Condition::NoCondition) { - if (context.ledgerMaster.getValidatedLedgerAge() > Tuning::kMaxValidatedLedgerAge) + if (context.ledgerMaster.getValidatedLedgerAge() > tuning::kMaxValidatedLedgerAge) { if (context.apiVersion == 1) return RpcNoCurrent; @@ -122,4 +122,4 @@ conditionMet(Condition conditionRequired, T& context) return RpcSuccess; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/LegacyPathFind.cpp b/src/xrpld/rpc/detail/LegacyPathFind.cpp index 0bfa19a1f4..837d98084e 100644 --- a/src/xrpld/rpc/detail/LegacyPathFind.cpp +++ b/src/xrpld/rpc/detail/LegacyPathFind.cpp @@ -9,7 +9,7 @@ #include -namespace xrpl::RPC { +namespace xrpl::rpc { LegacyPathFind::LegacyPathFind(bool isAdmin, Application& app) { @@ -21,13 +21,13 @@ LegacyPathFind::LegacyPathFind(bool isAdmin, Application& app) } auto const& jobCount = app.getJobQueue().getJobCountGE(JtClient); - if (jobCount > Tuning::kMaxPathfindJobCount || app.getFeeTrack().isLoadedLocal()) + if (jobCount > tuning::kMaxPathfindJobCount || app.getFeeTrack().isLoadedLocal()) return; while (true) { int prevVal = inProgress.load(); - if (prevVal >= Tuning::kMaxPathfindsInProgress) + if (prevVal >= tuning::kMaxPathfindsInProgress) return; if (inProgress.compare_exchange_strong( @@ -47,4 +47,4 @@ LegacyPathFind::~LegacyPathFind() std::atomic LegacyPathFind::inProgress(0); -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/LegacyPathFind.h b/src/xrpld/rpc/detail/LegacyPathFind.h index 226191848f..30e176f245 100644 --- a/src/xrpld/rpc/detail/LegacyPathFind.h +++ b/src/xrpld/rpc/detail/LegacyPathFind.h @@ -6,7 +6,7 @@ namespace xrpl { class Application; -namespace RPC { +namespace rpc { class LegacyPathFind { @@ -26,5 +26,5 @@ private: bool isOk_{false}; }; -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp index e34980aee2..4f57bab9ab 100644 --- a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp @@ -16,7 +16,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { bool canHaveMPTokenIssuanceID( @@ -67,4 +67,4 @@ insertMPTokenIssuanceID( response[jss::mpt_issuance_id] = to_string(result.value()); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/PathRequest.cpp b/src/xrpld/rpc/detail/PathRequest.cpp index 3c09917dad..fb132199bc 100644 --- a/src/xrpld/rpc/detail/PathRequest.cpp +++ b/src/xrpld/rpc/detail/PathRequest.cpp @@ -73,7 +73,7 @@ PathRequest::PathRequest( PathRequest::PathRequest( Application& app, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, int id, PathRequestManager& owner, beast::Journal journal) @@ -344,7 +344,7 @@ PathRequest::parseJson(json::Value const& jvParams) { json::Value const& jvSrcCurrencies = jvParams[jss::source_currencies]; if (!jvSrcCurrencies.isArray() || jvSrcCurrencies.size() == 0 || - jvSrcCurrencies.size() > RPC::Tuning::kMaxSrcCur) + jvSrcCurrencies.size() > rpc::tuning::kMaxSrcCur) { jvStatus_ = rpcError(RpcSrcCurMalformed); return PFR_PJ_INVALID; @@ -556,7 +556,7 @@ PathRequest::findPaths( [&](TAsset const& a) { if (!sameAccount || a != saDstAmount_.asset()) { - if (sourceAssets.size() >= RPC::Tuning::kMaxAutoSrcCur) + if (sourceAssets.size() >= rpc::tuning::kMaxAutoSrcCur) return false; if constexpr (std::is_same_v) { diff --git a/src/xrpld/rpc/detail/PathRequest.h b/src/xrpld/rpc/detail/PathRequest.h index d40d9c82d6..f56b3d0652 100644 --- a/src/xrpld/rpc/detail/PathRequest.h +++ b/src/xrpld/rpc/detail/PathRequest.h @@ -64,7 +64,7 @@ public: PathRequest( Application& app, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, int id, PathRequestManager&, beast::Journal journal); @@ -137,7 +137,7 @@ private: std::weak_ptr wpSubscriber_; // Who this request came from std::function fCompletion_; - Resource::Consumer& consumer_; // Charge according to source currencies + resource::Consumer& consumer_; // Charge according to source currencies json::Value jvId_; json::Value jvStatus_; // Last result diff --git a/src/xrpld/rpc/detail/PathRequestManager.cpp b/src/xrpld/rpc/detail/PathRequestManager.cpp index 4953634181..117bfbda1e 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.cpp +++ b/src/xrpld/rpc/detail/PathRequestManager.cpp @@ -254,7 +254,7 @@ json::Value PathRequestManager::makeLegacyPathRequest( PathRequest::pointer& req, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request) { @@ -285,7 +285,7 @@ PathRequestManager::makeLegacyPathRequest( json::Value PathRequestManager::doLegacyPathRequest( - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request) { diff --git a/src/xrpld/rpc/detail/PathRequestManager.h b/src/xrpld/rpc/detail/PathRequestManager.h index f6eb80d291..29a80e66c0 100644 --- a/src/xrpld/rpc/detail/PathRequestManager.h +++ b/src/xrpld/rpc/detail/PathRequestManager.h @@ -65,7 +65,7 @@ public: makeLegacyPathRequest( PathRequest::pointer& req, std::function completion, - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request); @@ -73,7 +73,7 @@ public: // with the ledger specified by the caller json::Value doLegacyPathRequest( - Resource::Consumer& consumer, + resource::Consumer& consumer, std::shared_ptr const& inLedger, json::Value const& request); diff --git a/src/xrpld/rpc/detail/RPCCall.cpp b/src/xrpld/rpc/detail/RPCCall.cpp index b5d5c680cd..a752858527 100644 --- a/src/xrpld/rpc/detail/RPCCall.cpp +++ b/src/xrpld/rpc/detail/RPCCall.cpp @@ -74,7 +74,7 @@ createHTTPPost( // CHECKME this uses a different version than the replies below use. Is // this by design or an accident or should it be using - // BuildInfo::getFullVersionString () as well? + // build_info::getFullVersionString () as well? s << "POST " << (strPath.empty() ? "/" : strPath) << " HTTP/1.0\r\n" << "User-Agent: " << systemName() << "-json-rpc/v1\r\n" @@ -149,7 +149,7 @@ private: return jvResult; } - return RPC::makeParamError( + return rpc::makeParamError( std::string("Invalid currency/issuer '") + strCurrencyIssuer + "'"); } @@ -355,7 +355,7 @@ private: } catch (std::exception const&) { - return RPC::invalidFieldError(jss::limit); + return rpc::invalidFieldError(jss::limit); } } @@ -369,7 +369,7 @@ private: } catch (std::exception const&) { - return RPC::invalidFieldError(jss::proof); + return rpc::invalidFieldError(jss::proof); } } @@ -1182,7 +1182,7 @@ private: std::string param = jvParams[index++].asString(); if (param.empty()) - return RPC::makeParamError("Invalid first parameter"); + return rpc::makeParamError("Invalid first parameter"); if (param[0] != 'r') { @@ -1196,7 +1196,7 @@ private: } if (size <= index) - return RPC::makeParamError("Invalid hotwallet"); + return rpc::makeParamError("Invalid hotwallet"); param = jvParams[index++].asString(); } @@ -1726,7 +1726,7 @@ rpcClient( { boost::asio::io_context isService; - RPCCall::fromNetwork( + rpc_call::fromNetwork( isService, setup.client.ip, setup.client.port, @@ -1813,12 +1813,12 @@ rpcClient( //------------------------------------------------------------------------------ -namespace RPCCall { +namespace rpc_call { int fromCommandLine(Config const& config, std::vector const& vCmd, Logs& logs) { - auto const result = rpcClient(vCmd, config, logs, RPC::kApiCommandLineVersion); + auto const result = rpcClient(vCmd, config, logs, rpc::kApiCommandLineVersion); std::cout << result.second.toStyledString(); @@ -1883,6 +1883,6 @@ fromNetwork( j); } -} // namespace RPCCall +} // namespace rpc_call } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp b/src/xrpld/rpc/detail/RPCHandler.cpp index 6f46aed62d..96d6bf72d7 100644 --- a/src/xrpld/rpc/detail/RPCHandler.cpp +++ b/src/xrpld/rpc/detail/RPCHandler.cpp @@ -24,7 +24,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace { @@ -114,7 +114,7 @@ fillHandler(JsonContext& context, Handler const*& result) { // Count all jobs at jtCLIENT priority or higher. int const jobCount = context.app.getJobQueue().getJobCountGE(JtClient); - if (jobCount > Tuning::kMaxJobQueueClients) + if (jobCount > tuning::kMaxJobQueueClients) { JLOG(context.j.debug()) << "Too busy for command: " << jobCount; return RpcTooBusy; @@ -179,8 +179,8 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object& perfLog.rpcError(name, curId); JLOG(context.j.info()) << "Caught throw: " << e.what(); - if (context.loadType == Resource::kFeeReferenceRpc) - context.loadType = Resource::kFeeExceptionRpc; + if (context.loadType == resource::kFeeReferenceRpc) + context.loadType = resource::kFeeExceptionRpc; injectError(RpcInternal, result); return RpcInternal; @@ -190,7 +190,7 @@ callMethod(JsonContext& context, Method method, std::string const& name, Object& } // namespace Status -doCommand(RPC::JsonContext& context, json::Value& result) +doCommand(rpc::JsonContext& context, json::Value& result) { Handler const* handler = nullptr; if (auto error = fillHandler(context, handler)) @@ -226,7 +226,7 @@ doCommand(RPC::JsonContext& context, json::Value& result) Role roleRequired(unsigned int version, bool betaEnabled, std::string const& method) { - auto handler = RPC::getHandler(version, betaEnabled, method); + auto handler = rpc::getHandler(version, betaEnabled, method); if (handler == nullptr) return Role::FORBID; @@ -234,4 +234,4 @@ roleRequired(unsigned int version, bool betaEnabled, std::string const& method) return handler->role; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 1764375812..4fa0fab6f7 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -42,7 +42,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { std::uint64_t getStartHint(SLE::const_ref sle, AccountID const& accountID) @@ -116,7 +116,7 @@ parseAccountIds(json::Value const& jvArray) } std::optional -readLimitField(unsigned int& limit, Tuning::LimitRange const& range, JsonContext const& context) +readLimitField(unsigned int& limit, tuning::LimitRange const& range, JsonContext const& context) { limit = range.rDefault; if (!context.params.isMember(jss::limit) || context.params[jss::limit].isNull()) @@ -124,11 +124,11 @@ readLimitField(unsigned int& limit, Tuning::LimitRange const& range, JsonContext auto const& jvLimit = context.params[jss::limit]; if (!jvLimit.isUInt() && (!jvLimit.isInt() || jvLimit.asInt() < 0)) - return RPC::expectedFieldError(jss::limit, "unsigned integer"); + return rpc::expectedFieldError(jss::limit, "unsigned integer"); limit = jvLimit.asUInt(); if (limit == 0) - return RPC::invalidFieldError(jss::limit); + return rpc::invalidFieldError(jss::limit); if (!isUnlimited(context.role)) limit = std::max(range.rmin, std::min(range.rmax, limit)); @@ -184,7 +184,7 @@ getSeedFromRPC(json::Value const& params, json::Value& error) if (count != 1) { - error = RPC::makeParamError( + error = rpc::makeParamError( "Exactly one of the following must be specified: " + std::string(jss::passphrase) + ", " + std::string(jss::seed) + " or " + std::string(jss::seed_hex)); return std::nullopt; @@ -194,7 +194,7 @@ getSeedFromRPC(json::Value const& params, json::Value& error) auto const& param = params[seedType->first]; if (!param.isString()) { - error = RPC::expectedFieldError(seedType->first, "string"); + error = rpc::expectedFieldError(seedType->first, "string"); return std::nullopt; } @@ -232,13 +232,13 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int if (count == 0 || secretType == nullptr) { - error = RPC::missingFieldError(jss::secret); + error = rpc::missingFieldError(jss::secret); return {}; } if (count > 1) { - error = RPC::makeParamError( + error = rpc::makeParamError( "Exactly one of the following must be specified: " + std::string(jss::passphrase) + ", " + std::string(jss::secret) + ", " + std::string(jss::seed) + " or " + std::string(jss::seed_hex)); @@ -252,7 +252,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (!params[jss::key_type].isString()) { - error = RPC::expectedFieldError(jss::key_type, "string"); + error = rpc::expectedFieldError(jss::key_type, "string"); return {}; } @@ -262,11 +262,11 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (apiVersion > 1u) { - error = RPC::makeError(RpcBadKeyType); + error = rpc::makeError(RpcBadKeyType); } else { - error = RPC::invalidFieldError(jss::key_type); + error = rpc::invalidFieldError(jss::key_type); } return {}; } @@ -275,7 +275,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int // https://developercommunity.visualstudio.com/t/assigning-constexpr-char--to-static-cha/10021357?entry=problem) if (strcmp(secretType, jss::secret.cStr()) == 0) { - error = RPC::makeParamError( + error = rpc::makeParamError( "The secret field is not allowed if " + std::string(jss::key_type) + " is used."); return {}; } @@ -288,7 +288,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int // https://developercommunity.visualstudio.com/t/assigning-constexpr-char--to-static-cha/10021357?entry=problem) if (strcmp(secretType, jss::seed_hex.cStr()) != 0) { - seed = RPC::parseXrplLibSeed(params[secretType]); + seed = rpc::parseXrplLibSeed(params[secretType]); if (seed) { @@ -296,7 +296,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int // requested another key type, return an error. if (keyType.value_or(KeyType::Ed25519) != KeyType::Ed25519) { - error = RPC::makeError(RpcBadSeed, "Specified seed is for an Ed25519 wallet."); + error = rpc::makeError(RpcBadSeed, "Specified seed is for an Ed25519 wallet."); return {}; } @@ -317,7 +317,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (!params[jss::secret].isString()) { - error = RPC::expectedFieldError(jss::secret, "string"); + error = rpc::expectedFieldError(jss::secret, "string"); return {}; } @@ -329,7 +329,7 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int { if (!containsError(error)) { - error = RPC::makeError(RpcBadSeed, RPC::invalidFieldMessage(secretType)); + error = rpc::makeError(RpcBadSeed, rpc::invalidFieldMessage(secretType)); } return {}; @@ -341,10 +341,10 @@ keypairForSignature(json::Value const& params, json::Value& error, unsigned int return generateKeyPair(*keyType, *seed); } -std::pair +std::pair chooseLedgerEntryType(json::Value const& params) { - std::pair result{RPC::Status::kOK, ltANY}; + std::pair result{rpc::Status::kOK, ltANY}; if (params.isMember(jss::type)) { static constexpr auto kTypes = @@ -363,10 +363,10 @@ chooseLedgerEntryType(json::Value const& params) auto const& p = params[jss::type]; if (!p.isString()) { - result.first = RPC::Status{RpcInvalidParams, "Invalid field 'type', not string."}; + result.first = rpc::Status{RpcInvalidParams, "Invalid field 'type', not string."}; XRPL_ASSERT( - result.first.type() == RPC::Status::Type::ErrorCodeI, - "xrpl::RPC::chooseLedgerEntryType : first valid result type"); + result.first.type() == rpc::Status::Type::ErrorCodeI, + "xrpl::rpc::chooseLedgerEntryType : first valid result type"); return result; } @@ -379,10 +379,10 @@ chooseLedgerEntryType(json::Value const& params) }); if (iter == kTypes.end()) { - result.first = RPC::Status{RpcInvalidParams, "Invalid field 'type'."}; + result.first = rpc::Status{RpcInvalidParams, "Invalid field 'type'."}; XRPL_ASSERT( - result.first.type() == RPC::Status::Type::ErrorCodeI, - "xrpl::RPC::chooseLedgerEntryType : second valid result " + result.first.type() == rpc::Status::Type::ErrorCodeI, + "xrpl::rpc::chooseLedgerEntryType : second valid result " "type"); return result; } @@ -466,4 +466,4 @@ parseSubUnsubJson( return RpcSuccess; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/RPCHelpers.h b/src/xrpld/rpc/detail/RPCHelpers.h index 881b758487..24c06021c0 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.h +++ b/src/xrpld/rpc/detail/RPCHelpers.h @@ -27,7 +27,7 @@ namespace xrpl { class ReadView; -namespace RPC { +namespace rpc { struct JsonContext; @@ -85,7 +85,7 @@ parseAccountIds(json::Value const& jvArray); * std::nullopt on success. */ std::optional -readLimitField(unsigned int& limit, Tuning::LimitRange const& range, JsonContext const& context); +readLimitField(unsigned int& limit, tuning::LimitRange const& range, JsonContext const& context); /** * @brief Extracts a Seed from RPC parameters. @@ -123,7 +123,7 @@ parseXrplLibSeed(json::Value const& params); * @param params The JSON value containing RPC parameters. * @return A pair consisting of the RPC status and the chosen LedgerEntryType. */ -std::pair +std::pair chooseLedgerEntryType(json::Value const& params); /** @@ -172,6 +172,6 @@ parseSubUnsubJson( json::StaticString const& name, beast::Journal j); -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp index 6843c34b19..52e68e87f1 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp @@ -30,7 +30,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace { @@ -40,7 +40,7 @@ isValidatedOld(LedgerMaster& ledgerMaster, bool standalone) if (standalone) return false; - return ledgerMaster.getValidatedLedgerAge() > Tuning::kMaxValidatedLedgerAge; + return ledgerMaster.getValidatedLedgerAge() > tuning::kMaxValidatedLedgerAge; } template @@ -282,19 +282,19 @@ getLedger(T& ledger, LedgerShortcut shortcut, Context const& context) return {RpcNotSynced, "notSynced"}; } - XRPL_ASSERT(!ledger->open(), "xrpl::RPC::getLedger : validated is not open"); + XRPL_ASSERT(!ledger->open(), "xrpl::rpc::getLedger : validated is not open"); } else { if (shortcut == LedgerShortcut::Current) { ledger = context.ledgerMaster.getCurrentLedger(); - XRPL_ASSERT(ledger->open(), "xrpl::RPC::getLedger : current is open"); + XRPL_ASSERT(ledger->open(), "xrpl::rpc::getLedger : current is open"); } else if (shortcut == LedgerShortcut::Closed) { ledger = context.ledgerMaster.getClosedLedger(); - XRPL_ASSERT(!ledger->open(), "xrpl::RPC::getLedger : closed is not open"); + XRPL_ASSERT(!ledger->open(), "xrpl::rpc::getLedger : closed is not open"); } else { @@ -386,7 +386,7 @@ lookupLedger(std::shared_ptr& ledger, JsonContext const& context } std::expected, json::Value> -getOrAcquireLedger(RPC::JsonContext const& context) +getOrAcquireLedger(rpc::JsonContext const& context) { auto const hasHash = context.params.isMember(jss::ledger_hash); auto const hasIndex = context.params.isMember(jss::ledger_index); @@ -398,7 +398,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) if ((static_cast(hasHash) + static_cast(hasIndex)) != 1) { return std::unexpected( - RPC::makeParamError( + rpc::makeParamError( "Exactly one of 'ledger_hash' or " "'ledger_index' can be specified.")); } @@ -407,16 +407,16 @@ getOrAcquireLedger(RPC::JsonContext const& context) { auto const& jsonHash = context.params.get(jss::ledger_hash, json::ValueType::Null); if (!jsonHash.isString() || !ledgerHash.parseHex(jsonHash.asString())) - return std::unexpected(RPC::expectedFieldError(jss::ledger_hash, "hex string")); + return std::unexpected(rpc::expectedFieldError(jss::ledger_hash, "hex string")); } else { auto const& jsonIndex = context.params.get(jss::ledger_index, json::ValueType::Null); if (!jsonIndex.isInt() && !jsonIndex.isUInt()) - return std::unexpected(RPC::expectedFieldError(jss::ledger_index, "number")); + return std::unexpected(rpc::expectedFieldError(jss::ledger_index, "number")); // We need a validated ledger to get the hash from the sequence - if (ledgerMaster.getValidatedLedgerAge() > RPC::Tuning::kMaxValidatedLedgerAge) + if (ledgerMaster.getValidatedLedgerAge() > rpc::tuning::kMaxValidatedLedgerAge) { if (context.apiVersion == 1) return std::unexpected(rpcError(RpcNoCurrent)); @@ -427,9 +427,9 @@ getOrAcquireLedger(RPC::JsonContext const& context) auto ledger = ledgerMaster.getValidatedLedger(); if (ledgerIndex >= ledger->header().seq) - return std::unexpected(RPC::makeParamError("Ledger index too large")); + return std::unexpected(rpc::makeParamError("Ledger index too large")); if (ledgerIndex <= 0) - return std::unexpected(RPC::makeParamError("Ledger index too small")); + return std::unexpected(rpc::makeParamError("Ledger index too small")); auto const j = context.app.getJournal("RPCHandler"); // Try to get the hash of the desired ledger from the validated @@ -441,7 +441,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) // ledger auto const refIndex = getCandidateLedger(ledgerIndex); auto refHash = hashOfSeq(*ledger, refIndex, j); - XRPL_ASSERT(refHash, "xrpl::RPC::getOrAcquireLedger : nonzero ledger hash"); + XRPL_ASSERT(refHash, "xrpl::rpc::getOrAcquireLedger : nonzero ledger hash"); // NOLINTBEGIN(bugprone-unchecked-optional-access) assert above ledger = ledgerMaster.getLedgerByHash(*refHash); @@ -453,7 +453,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) if (auto il = context.app.getInboundLedgers().acquire( *refHash, refIndex, InboundLedger::Reason::GENERIC)) { - json::Value jvResult = RPC::makeError( + json::Value jvResult = rpc::makeError( RpcLgrNotFound, "acquiring ledger containing requested index"); jvResult[jss::acquiring] = getJson(LedgerFill(*il, &context)); return std::unexpected(jvResult); @@ -462,7 +462,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) if (auto il = context.app.getInboundLedgers().find(*refHash)) // NOLINTEND(bugprone-unchecked-optional-access) { - json::Value jvResult = RPC::makeError( + json::Value jvResult = rpc::makeError( RpcLgrNotFound, "acquiring ledger containing requested index"); jvResult[jss::acquiring] = il->getJson(0); return std::unexpected(jvResult); @@ -474,7 +474,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) neededHash = hashOfSeq(*ledger, ledgerIndex, j); } - XRPL_ASSERT(neededHash, "xrpl::RPC::getOrAcquireLedger : nonzero needed hash"); + XRPL_ASSERT(neededHash, "xrpl::rpc::getOrAcquireLedger : nonzero needed hash"); ledgerHash = neededHash ? *neededHash : beast::kZero; // kludge } @@ -494,7 +494,7 @@ getOrAcquireLedger(RPC::JsonContext const& context) return std::unexpected(il->getJson(0)); return std::unexpected( - RPC::makeError(RpcNotReady, "findCreate failed to return an inbound ledger")); + rpc::makeError(RpcNotReady, "findCreate failed to return an inbound ledger")); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h b/src/xrpld/rpc/detail/RPCLedgerHelpers.h index cbd47d38e6..cad5141917 100644 --- a/src/xrpld/rpc/detail/RPCLedgerHelpers.h +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h @@ -21,7 +21,7 @@ namespace xrpl { class ReadView; class Transaction; -namespace RPC { +namespace rpc { struct JsonContext; @@ -172,8 +172,8 @@ ledgerFromSpecifier( * On failure, contains a json::Value describing the error. */ std::expected, json::Value> -getOrAcquireLedger(RPC::JsonContext const& context); +getOrAcquireLedger(rpc::JsonContext const& context); -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/RPCSub.cpp b/src/xrpld/rpc/detail/RPCSub.cpp index 92e9849939..8cea8b6bdd 100644 --- a/src/xrpld/rpc/detail/RPCSub.cpp +++ b/src/xrpld/rpc/detail/RPCSub.cpp @@ -75,7 +75,7 @@ public: } path_ = pUrl.path; - JLOG(j_.info()) << "RPCCall::fromNetwork sub: ip=" << ip_ << " port=" << port_ + JLOG(j_.info()) << "rpc_call::fromNetwork sub: ip=" << ip_ << " port=" << port_ << " ssl= " << (ssl_ ? "yes" : "no") << " path='" << path_ << "'"; } @@ -87,14 +87,14 @@ public: std::scoped_lock const sl(lock_); auto jm = broadcast ? j_.debug() : j_.info(); - JLOG(jm) << "RPCCall::fromNetwork push: " << jvObj; + JLOG(jm) << "rpc_call::fromNetwork push: " << jvObj; deque_.emplace_back(seq_++, jvObj); if (!sending_) { // Start a sending thread. - JLOG(j_.info()) << "RPCCall::fromNetwork start"; + JLOG(j_.info()) << "rpc_call::fromNetwork start"; sending_ = jobQueue_.addJob(JtClientSubscribe, "RPCSubSendThr", [this]() { sendThread(); }); @@ -156,9 +156,9 @@ private: // XXX Might not need this in a try. try { - JLOG(j_.info()) << "RPCCall::fromNetwork: " << ip_; + JLOG(j_.info()) << "rpc_call::fromNetwork: " << ip_; - RPCCall::fromNetwork( + rpc_call::fromNetwork( ioContext_, ip_, port_, @@ -173,7 +173,7 @@ private: } catch (std::exception const& e) { - JLOG(j_.info()) << "RPCCall::fromNetwork exception: " << e.what(); + JLOG(j_.info()) << "rpc_call::fromNetwork exception: " << e.what(); } } } while (bSend); diff --git a/src/xrpld/rpc/detail/Role.cpp b/src/xrpld/rpc/detail/Role.cpp index 68c5fcc484..34970b0580 100644 --- a/src/xrpld/rpc/detail/Role.cpp +++ b/src/xrpld/rpc/detail/Role.cpp @@ -40,7 +40,7 @@ passwordUnrequiredOrSentCorrect(Port const& port, json::Value const& params) bool ipAllowed( - beast::IP::Address const& remoteIp, + beast::ip::Address const& remoteIp, std::vector const& nets4, std::vector const& nets6) { @@ -78,7 +78,7 @@ ipAllowed( } bool -isAdmin(Port const& port, json::Value const& params, beast::IP::Address const& remoteIp) +isAdmin(Port const& port, json::Value const& params, beast::ip::Address const& remoteIp) { return ipAllowed(remoteIp, port.adminNetsV4, port.adminNetsV6) && passwordUnrequiredOrSentCorrect(port, params); @@ -89,7 +89,7 @@ requestRole( Role const& required, Port const& port, json::Value const& params, - beast::IP::Endpoint const& remoteIp, + beast::ip::Endpoint const& remoteIp, std::string_view user) { if (isAdmin(port, params, remoteIp.address())) @@ -122,16 +122,16 @@ isUnlimited( Role const& required, Port const& port, json::Value const& params, - beast::IP::Endpoint const& remoteIp, + beast::ip::Endpoint const& remoteIp, std::string const& user) { return isUnlimited(requestRole(required, port, params, remoteIp, user)); } -Resource::Consumer +resource::Consumer requestInboundEndpoint( - Resource::Manager& manager, - beast::IP::Endpoint const& remoteAddress, + resource::Manager& manager, + beast::ip::Endpoint const& remoteAddress, Role const& role, std::string_view user, std::string_view forwardedFor) diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 768cbf0dc0..0181d5b10f 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -93,7 +93,7 @@ statusRequestResponse(http_request_type const& request, boost::beast::http::stat response msg; msg.version(request.version()); msg.result(status); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "text/html"); msg.insert("Connection", "close"); msg.body() = "Invalid protocol."; @@ -129,7 +129,7 @@ ServerHandler::ServerHandler( boost::asio::io_context& ioContext, JobQueue& jobQueue, NetworkOPs& networkOPs, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, CollectorManager& cm) : app_(app) , resourceManager_(resourceManager) @@ -337,7 +337,7 @@ ServerHandler::onWSMessage( { json::Value jv; auto const size = boost::asio::buffer_size(buffers); - if (size > RPC::Tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject()) + if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject()) { json::Value jvResult(json::ValueType::Object); jvResult[jss::type] = jss::error; @@ -426,11 +426,11 @@ ServerHandler::processSession( // Requests without "command" are invalid. json::Value jr(json::ValueType::Object); - Resource::Charge loadType = Resource::kFeeReferenceRpc; + resource::Charge loadType = resource::kFeeReferenceRpc; try { - auto apiVersion = RPC::getAPIVersionNumber(jv, app_.config().betaRpcApi); - if (apiVersion == RPC::kApiInvalidVersion || + auto apiVersion = rpc::getAPIVersionNumber(jv, app_.config().betaRpcApi); + if (apiVersion == rpc::kApiInvalidVersion || (!jv.isMember(jss::command) && !jv.isMember(jss::method)) || (jv.isMember(jss::command) && !jv[jss::command].isString()) || (jv.isMember(jss::method) && !jv[jss::method].isString()) || @@ -439,7 +439,7 @@ ServerHandler::processSession( { jr[jss::type] = jss::response; jr[jss::status] = jss::error; - jr[jss::error] = apiVersion == RPC::kApiInvalidVersion ? jss::invalid_API_version + jr[jss::error] = apiVersion == rpc::kApiInvalidVersion ? jss::invalid_API_version : jss::missingCommand; jr[jss::request] = jv; if (jv.isMember(jss::id)) @@ -451,11 +451,11 @@ ServerHandler::processSession( if (jv.isMember(jss::api_version)) jr[jss::api_version] = jv[jss::api_version]; - is->getConsumer().charge(Resource::kFeeMalformedRpc); + is->getConsumer().charge(resource::kFeeMalformedRpc); return jr; } - auto required = RPC::roleRequired( + auto required = rpc::roleRequired( apiVersion, app_.config().betaRpcApi, jv.isMember(jss::command) ? jv[jss::command].asString() : jv[jss::method].asString()); @@ -463,16 +463,16 @@ ServerHandler::processSession( required, session->port(), jv, - beast::IP::fromAsio(session->remoteEndpoint().address()), + beast::ip::fromAsio(session->remoteEndpoint().address()), is->user()); if (Role::FORBID == role) { - loadType = Resource::kFeeMalformedRpc; + loadType = resource::kFeeMalformedRpc; jr[jss::result] = rpcError(RpcForbidden); } else { - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = app_.getJournal("RPCHandler"), .app = app_, .loadType = loadType, @@ -487,7 +487,7 @@ ServerHandler::processSession( {.user = is->user(), .forwardedFor = is->forwardedFor()}}; auto start = std::chrono::system_clock::now(); - RPC::doCommand(context, jr[jss::result]); + rpc::doCommand(context, jr[jss::result]); auto end = std::chrono::system_clock::now(); logDuration(jv, end - start, journal_); } @@ -495,7 +495,7 @@ ServerHandler::processSession( catch (std::exception const& ex) { // LCOV_EXCL_START - jr[jss::result] = RPC::makeError(RpcInternal); + jr[jss::result] = rpc::makeError(RpcInternal); JLOG(journal_.error()) << "Exception while processing WS: " << ex.what() << "\n" << "Input JSON: " << json::Compact{json::Value{jv}}; // LCOV_EXCL_STOP @@ -601,7 +601,7 @@ void ServerHandler::processRequest( Port const& port, std::string const& request, - beast::IP::Endpoint const& remoteIPAddress, + beast::ip::Endpoint const& remoteIPAddress, Output const& output, std::shared_ptr coro, std::string_view forwardedFor, @@ -612,7 +612,7 @@ ServerHandler::processRequest( json::Value jsonOrig; { json::Reader reader; - if ((request.size() > RPC::Tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) || + if ((request.size() > rpc::tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) || !jsonOrig || !jsonOrig.isObject()) { httpReply( @@ -652,21 +652,21 @@ ServerHandler::processRequest( continue; } - unsigned apiVersion = RPC::kApiVersionIfUnspecified; + unsigned apiVersion = rpc::kApiVersionIfUnspecified; if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() && jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][0u].isObject()) { - apiVersion = RPC::getAPIVersionNumber( + apiVersion = rpc::getAPIVersionNumber( jsonRPC[jss::params][json::UInt(0)], app_.config().betaRpcApi); } - if (apiVersion == RPC::kApiVersionIfUnspecified && batch) + if (apiVersion == rpc::kApiVersionIfUnspecified && batch) { // for batch request, api_version may be at a different level - apiVersion = RPC::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi); + apiVersion = rpc::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi); } - if (apiVersion == RPC::kApiInvalidVersion) + if (apiVersion == rpc::kApiInvalidVersion) { if (!batch) { @@ -685,7 +685,7 @@ ServerHandler::processRequest( auto required = Role::FORBID; if (jsonRPC.isMember(jss::method) && jsonRPC[jss::method].isString()) { - required = RPC::roleRequired( + required = rpc::roleRequired( apiVersion, app_.config().betaRpcApi, jsonRPC[jss::method].asString()); } @@ -700,7 +700,7 @@ ServerHandler::processRequest( role = requestRole(required, port, json::ValueType::Object, remoteIPAddress, user); } - Resource::Consumer usage; + resource::Consumer usage; if (isUnlimited(role)) { usage = resourceManager_.newUnlimitedEndpoint(remoteIPAddress); @@ -725,7 +725,7 @@ ServerHandler::processRequest( if (role == Role::FORBID) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(403, "Forbidden", output, rpcJ); @@ -739,7 +739,7 @@ ServerHandler::processRequest( if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "Null method", output, rpcJ); @@ -754,7 +754,7 @@ ServerHandler::processRequest( json::Value const& method = jsonRPC[jss::method]; if (!method.isString()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "method is not string", output, rpcJ); @@ -769,7 +769,7 @@ ServerHandler::processRequest( std::string const strMethod = method.asString(); if (strMethod.empty()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "method is empty", output, rpcJ); @@ -797,7 +797,7 @@ ServerHandler::processRequest( } else if (!params.isArray() || params.size() != 1) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); httpReply(400, "params unparsable", output, rpcJ); return; } @@ -806,7 +806,7 @@ ServerHandler::processRequest( params = std::move(params[0u]); if (!params.isObjectOrNull()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); httpReply(400, "params unparsable", output, rpcJ); return; } @@ -822,7 +822,7 @@ ServerHandler::processRequest( { if (!params[jss::ripplerpc].isString()) { - usage.charge(Resource::kFeeMalformedRpc); + usage.charge(resource::kFeeMalformedRpc); if (!batch) { httpReply(400, "ripplerpc is not a string", output, rpcJ); @@ -853,9 +853,9 @@ ServerHandler::processRequest( params[jss::command] = strMethod; JLOG(journal_.trace()) << "doRpcCommand:" << strMethod << ":" << params; - Resource::Charge loadType = Resource::kFeeReferenceRpc; + resource::Charge loadType = resource::kFeeReferenceRpc; - RPC::JsonContext context{ + rpc::JsonContext context{ {.j = journal_, .app = app_, .loadType = loadType, @@ -874,12 +874,12 @@ ServerHandler::processRequest( try { - RPC::doCommand(context, result); + rpc::doCommand(context, result); } catch (std::exception const& ex) { // LCOV_EXCL_START - result = RPC::makeError(RpcInternal); + result = rpc::makeError(RpcInternal); JLOG(journal_.error()) << "Internal error : " << ex.what() << " when processing request: " << json::Compact{json::Value{params}}; @@ -984,7 +984,7 @@ ServerHandler::processRequest( reply[jss::error][jss::error_code].isInt()) { int const errCode = reply[jss::error][jss::error_code].asInt(); - return RPC::errorCodeHttpStatus(static_cast(errCode)); + return rpc::errorCodeHttpStatus(static_cast(errCode)); } } // Return OK. @@ -1043,7 +1043,7 @@ ServerHandler::statusResponse(http_request_type const& request) const msg.body() = "Server cannot accept clients: " + reason + ""; } msg.version(request.version()); - msg.insert("Server", BuildInfo::getFullVersionString()); + msg.insert("Server", build_info::getFullVersionString()); msg.insert("Content-Type", "text/html"); msg.insert("Connection", "close"); msg.prepare_payload(); @@ -1208,7 +1208,7 @@ setupClient(ServerHandler::Setup& setup) if (iter == setup.ports.cend()) return; setup.client.secure = iter->protocol.contains("https"); - if (beast::IP::isUnspecified(iter->ip)) + if (beast::ip::isUnspecified(iter->ip)) { // VFALCO HACK! to make localhost work setup.client.ip = iter->ip.is_v6() ? "::1" : "127.0.0.1"; @@ -1256,7 +1256,7 @@ makeServerHandler( boost::asio::io_context& ioContext, JobQueue& jobQueue, NetworkOPs& networkOPs, - Resource::Manager& resourceManager, + resource::Manager& resourceManager, CollectorManager& cm) { return std::make_unique( diff --git a/src/xrpld/rpc/detail/Status.cpp b/src/xrpld/rpc/detail/Status.cpp index 58d2f8cb80..147f2b31e0 100644 --- a/src/xrpld/rpc/detail/Status.cpp +++ b/src/xrpld/rpc/detail/Status.cpp @@ -9,7 +9,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { std::string Status::codeString() const @@ -25,7 +25,7 @@ Status::codeString() const std::string s1, s2; [[maybe_unused]] auto const success = transResultInfo(toTER(), s1, s2); - XRPL_ASSERT(success, "xrpl::RPC::codeString : valid TER result"); + XRPL_ASSERT(success, "xrpl::rpc::codeString : valid TER result"); return s1 + ": " + s2; } @@ -39,7 +39,7 @@ Status::codeString() const } // LCOV_EXCL_START - UNREACHABLE("xrpl::RPC::codeString : invalid type"); + UNREACHABLE("xrpl::rpc::codeString : invalid type"); return ""; // LCOV_EXCL_STOP } @@ -85,4 +85,4 @@ Status::toString() const return ""; } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index e1c5180b5c..9c97577b27 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -64,7 +64,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { namespace detail { // Used to pass extra parameters used when returning a @@ -218,7 +218,7 @@ checkPayment( { if (txJson[jss::DeliverMax] != txJson[jss::Amount]) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Cannot specify differing 'Amount' and 'DeliverMax'"); } } @@ -231,31 +231,31 @@ checkPayment( } if (!txJson.isMember(jss::Amount)) - return RPC::missingFieldError("tx_json.Amount"); + return rpc::missingFieldError("tx_json.Amount"); STAmount amount; if (!amountFromJsonNoThrow(amount, txJson[jss::Amount])) - return RPC::invalidFieldError("tx_json.Amount"); + return rpc::invalidFieldError("tx_json.Amount"); if (!txJson.isMember(jss::Destination)) - return RPC::missingFieldError("tx_json.Destination"); + return rpc::missingFieldError("tx_json.Destination"); auto const dstAccountID = parseBase58(txJson[jss::Destination].asString()); if (!dstAccountID) - return RPC::invalidFieldError("tx_json.Destination"); + return rpc::invalidFieldError("tx_json.Destination"); if (params.isMember(jss::build_path) && (!doPath || (!app.getOpenLedger().current()->rules().enabled(featureMPTokensV2) && amount.holds()))) { - return RPC::makeError(RpcInvalidParams, "Field 'build_path' not allowed in this context."); + return rpc::makeError(RpcInvalidParams, "Field 'build_path' not allowed in this context."); } if (txJson.isMember(jss::Paths) && params.isMember(jss::build_path)) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "Cannot specify both 'tx_json.Paths' and 'build_path'"); } @@ -266,7 +266,7 @@ checkPayment( if (!txJson[sfDomainID.jsonName].isString() || !num.parseHex(txJson[sfDomainID.jsonName].asString())) { - return RPC::makeError(RpcDomainMalformed, "Unable to parse 'DomainID'."); + return rpc::makeError(RpcDomainMalformed, "Unable to parse 'DomainID'."); } domain = num; @@ -279,7 +279,7 @@ checkPayment( if (txJson.isMember(jss::SendMax)) { if (!amountFromJsonNoThrow(sendMax, txJson[jss::SendMax])) - return RPC::invalidFieldError("tx_json.SendMax"); + return rpc::invalidFieldError("tx_json.SendMax"); } else { @@ -291,7 +291,7 @@ checkPayment( } if (sendMax.native() && amount.native()) - return RPC::makeError(RpcInvalidParams, "Cannot build XRP to XRP paths."); + return rpc::makeError(RpcInvalidParams, "Cannot build XRP to XRP paths."); { LegacyPathFind const lpf(isUnlimited(role), app); @@ -357,19 +357,19 @@ checkTxJsonFields( if (!txJson.isObject()) { - ret.first = RPC::objectFieldError(jss::tx_json); + ret.first = rpc::objectFieldError(jss::tx_json); return ret; } if (!txJson.isMember(jss::TransactionType)) { - ret.first = RPC::missingFieldError("tx_json.TransactionType"); + ret.first = rpc::missingFieldError("tx_json.TransactionType"); return ret; } if (!txJson.isMember(jss::Account)) { - ret.first = RPC::makeError(RpcSrcActMissing, RPC::missingFieldMessage("tx_json.Account")); + ret.first = rpc::makeError(RpcSrcActMissing, rpc::missingFieldMessage("tx_json.Account")); return ret; } @@ -377,12 +377,12 @@ checkTxJsonFields( if (!srcAddressID) { - ret.first = RPC::makeError(RpcSrcActMalformed, RPC::invalidFieldMessage("tx_json.Account")); + ret.first = rpc::makeError(RpcSrcActMalformed, rpc::invalidFieldMessage("tx_json.Account")); return ret; } // Check for current ledger. - if (verify && !config.standalone() && (validatedLedgerAge > Tuning::kMaxValidatedLedgerAge)) + if (verify && !config.standalone() && (validatedLedgerAge > tuning::kMaxValidatedLedgerAge)) { if (apiVersion == 1) { @@ -415,12 +415,12 @@ checkNetworkID(json::Value const& txJson, uint32_t appNetworkId) if (!txJson.isMember(jss::NetworkID)) { return std::unexpected( - RPC::makeError(RpcInvalidParams, RPC::missingFieldMessage("tx_json.NetworkID"))); + rpc::makeError(RpcInvalidParams, rpc::missingFieldMessage("tx_json.NetworkID"))); } if (!txJson[jss::NetworkID].isIntegral() || txJson[jss::NetworkID].asUInt() != appNetworkId) { return std::unexpected( - RPC::makeError(RpcInvalidParams, RPC::invalidFieldMessage("tx_json.NetworkID"))); + rpc::makeError(RpcInvalidParams, rpc::invalidFieldMessage("tx_json.NetworkID"))); } } return std::expected(); @@ -490,13 +490,13 @@ transactionPreProcessImpl( { if (signatureTemplate == nullptr) { // Invalid target field - return RPC::makeError(RpcInvalidParams, signatureTarget->get().getName()); + return rpc::makeError(RpcInvalidParams, signatureTarget->get().getName()); } signingArgs.setSignatureTarget(signatureTarget); } if (!params.isMember(jss::tx_json)) - return RPC::missingFieldError(jss::tx_json); + return rpc::missingFieldError(jss::tx_json); json::Value& txJson(params[jss::tx_json]); @@ -510,14 +510,14 @@ transactionPreProcessImpl( app.getFeeTrack(), getAPIVersionNumber(params, app.config().betaRpcApi)); - if (RPC::containsError(txJsonResult)) + if (rpc::containsError(txJsonResult)) return std::move(txJsonResult); // This test covers the case where we're offline so the sequence number // cannot be determined locally. If we're offline then the caller must // provide the sequence number. if (!verify && !txJson.isMember(jss::Sequence)) - return RPC::missingFieldError("tx_json.Sequence"); + return rpc::missingFieldError("tx_json.Sequence"); SLE::const_pointer sle; if (verify) @@ -565,7 +565,7 @@ transactionPreProcessImpl( app.getTxQ(), app); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -573,7 +573,7 @@ transactionPreProcessImpl( json::Value err = checkPayment( params, txJson, srcAddressID, role, app, verify && signingArgs.editFields()); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -614,8 +614,8 @@ transactionPreProcessImpl( if (!ptrDelegatedAddressID) { - return RPC::makeError( - RpcSrcActMalformed, RPC::invalidFieldMessage("tx_json.Delegate")); + return rpc::makeError( + RpcSrcActMalformed, rpc::invalidFieldMessage("tx_json.Delegate")); } auto delegatedAddressID = *ptrDelegatedAddressID; @@ -672,17 +672,17 @@ transactionPreProcessImpl( } catch (STObject::FieldErr const& err) { - return RPC::makeError(RpcInvalidParams, err.what()); + return rpc::makeError(RpcInvalidParams, err.what()); } catch (std::exception&) { - return RPC::makeError( + return rpc::makeError( RpcInternal, "Exception occurred constructing serialized transaction"); } std::string reason; if (!passesLocalChecks(*stTx, reason)) - return RPC::makeError(RpcInvalidParams, reason); + return rpc::makeError(RpcInvalidParams, reason); // If multisign then return multiSignature, else set TxnSignature field. if (signingArgs.isMultiSigning()) @@ -716,7 +716,7 @@ transactionConstructImpl( tpTrans = std::make_shared(stTx, reason, app); if (tpTrans->getStatus() != TransStatus::NEW) { - ret.first = RPC::makeError(RpcInternal, "Unable to construct transaction: " + reason); + ret.first = rpc::makeError(RpcInternal, "Unable to construct transaction: " + reason); return ret; } } @@ -741,7 +741,7 @@ transactionConstructImpl( } if (checkValidity(app.getHashRouter(), *sttxNew, rules).first != Validity::Valid) { - ret.first = RPC::makeError(RpcInternal, "Invalid signature."); + ret.first = rpc::makeError(RpcInternal, "Invalid signature."); return ret; } @@ -766,7 +766,7 @@ transactionConstructImpl( if (!tpTrans) { - ret.first = RPC::makeError(RpcInternal, "Unable to sterilize transaction."); + ret.first = rpc::makeError(RpcInternal, "Unable to sterilize transaction."); return ret; } ret.second = std::move(tpTrans); @@ -789,7 +789,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) jvResult[jss::tx_json] = tpTrans->getJson(JsonOptions::Values::None); } - RPC::insertDeliverMax( + rpc::insertDeliverMax( jvResult[jss::tx_json], tpTrans->getSTransaction()->getTxnType(), apiVersion); jvResult[jss::tx_blob] = strHex(tpTrans->getSTransaction()->getSerializer().peekData()); @@ -808,7 +808,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) } catch (std::exception&) { - jvResult = RPC::makeError(RpcInternal, "Exception occurred during JSON handling."); + jvResult = rpc::makeError(RpcInternal, "Exception occurred during JSON handling."); } return jvResult; } @@ -922,7 +922,7 @@ getCurrentNetworkFee( { std::stringstream ss; ss << "Fee of " << fee << " exceeds the requested tx limit of " << *limit; - return RPC::makeError(RpcHighFee, ss.str()); + return rpc::makeError(RpcHighFee, ss.str()); } return fee.jsonClipped(); @@ -943,10 +943,10 @@ checkFee( return json::Value(); if (!doAutoFill) - return RPC::missingFieldError("tx_json.Fee"); + return rpc::missingFieldError("tx_json.Fee"); - int mult = Tuning::kDefaultAutoFillFeeMultiplier; - int div = Tuning::kDefaultAutoFillFeeDivisor; + int mult = tuning::kDefaultAutoFillFeeMultiplier; + int div = tuning::kDefaultAutoFillFeeDivisor; if (request.isMember(jss::fee_mult_max)) { if (request[jss::fee_mult_max].isInt()) @@ -954,15 +954,15 @@ checkFee( mult = request[jss::fee_mult_max].asInt(); if (mult < 0) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); + rpc::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); } } else { - return RPC::makeError( - RpcHighFee, RPC::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); + return rpc::makeError( + RpcHighFee, rpc::expectedFieldMessage(jss::fee_mult_max, "a positive integer")); } } if (request.isMember(jss::fee_div_max)) @@ -972,15 +972,15 @@ checkFee( div = request[jss::fee_div_max].asInt(); if (div <= 0) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage(jss::fee_div_max, "a positive integer")); + rpc::expectedFieldMessage(jss::fee_div_max, "a positive integer")); } } else { - return RPC::makeError( - RpcHighFee, RPC::expectedFieldMessage(jss::fee_div_max, "a positive integer")); + return rpc::makeError( + RpcHighFee, rpc::expectedFieldMessage(jss::fee_div_max, "a positive integer")); } } @@ -1071,7 +1071,7 @@ transactionSubmit( } catch (std::exception&) { - return RPC::makeError(RpcInternal, "Exception occurred during transaction submission."); + return rpc::makeError(RpcInternal, "Exception occurred during transaction submission."); } return transactionFormatResultImpl(txn.second, apiVersion); @@ -1084,21 +1084,21 @@ static json::Value checkMultiSignFields(json::Value const& jvRequest) { if (!jvRequest.isMember(jss::tx_json)) - return RPC::missingFieldError(jss::tx_json); + return rpc::missingFieldError(jss::tx_json); json::Value const& txJson(jvRequest[jss::tx_json]); if (!txJson.isObject()) - return RPC::invalidFieldMessage(jss::tx_json); + return rpc::invalidFieldMessage(jss::tx_json); // There are a couple of additional fields we need to check before // we serialize. If we serialize first then we generate less useful // error messages. if (!txJson.isMember(jss::Sequence)) - return RPC::missingFieldError("tx_json.Sequence"); + return rpc::missingFieldError("tx_json.Sequence"); if (!txJson.isMember(sfSigningPubKey.getJsonName())) - return RPC::missingFieldError("tx_json.SigningPubKey"); + return rpc::missingFieldError("tx_json.SigningPubKey"); // Multi-signing into a signature_target object field is fine, // because it means the signature is not for the transaction @@ -1106,7 +1106,7 @@ checkMultiSignFields(json::Value const& jvRequest) if (!jvRequest.isMember(jss::signature_target) && !txJson[sfSigningPubKey.getJsonName()].asString().empty()) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "When multi-signing 'tx_json.SigningPubKey' must be empty."); } @@ -1120,7 +1120,7 @@ static json::Value sortAndValidateSigners(STArray& signers, AccountID const& signingForID) { if (signers.empty()) - return RPC::makeParamError("Signers array may not be empty."); + return rpc::makeParamError("Signers array may not be empty."); // Signers must be sorted by Account. std::ranges::sort(signers, [](STObject const& a, STObject const& b) { @@ -1137,7 +1137,7 @@ sortAndValidateSigners(STArray& signers, AccountID const& signingForID) std::ostringstream err; err << "Duplicate Signers:Signer:Account entries (" << toBase58((*dupIter)[sfAccount]) << ") are not allowed."; - return RPC::makeParamError(err.str()); + return rpc::makeParamError(err.str()); } // An account may not sign for itself. @@ -1147,7 +1147,7 @@ sortAndValidateSigners(STArray& signers, AccountID const& signingForID) { std::ostringstream err; err << "A Signer may not be the transaction's Account (" << toBase58(signingForID) << ")."; - return RPC::makeParamError(err.str()); + return rpc::makeParamError(err.str()); } return {}; } @@ -1174,23 +1174,23 @@ transactionSignFor( char const accountField[] = "account"; if (!jvRequest.isMember(accountField)) - return RPC::missingFieldError(accountField); + return rpc::missingFieldError(accountField); // Turn the signer's account into an AccountID for multi-sign. auto const signerAccountID = parseBase58(jvRequest[accountField].asString()); if (!signerAccountID) { - return RPC::makeError(RpcSrcActMalformed, RPC::invalidFieldMessage(accountField)); + return rpc::makeError(RpcSrcActMalformed, rpc::invalidFieldMessage(accountField)); } if (!jvRequest.isMember(jss::tx_json)) - return RPC::missingFieldError(jss::tx_json); + return rpc::missingFieldError(jss::tx_json); { json::Value& txJson(jvRequest[jss::tx_json]); if (!txJson.isObject()) - return RPC::objectFieldError(jss::tx_json); + return rpc::objectFieldError(jss::tx_json); if (auto checkResult = detail::checkNetworkID(txJson, app.getNetworkIDService().getNetworkID()); @@ -1211,7 +1211,7 @@ transactionSignFor( using namespace detail; { json::Value err = checkMultiSignFields(jvRequest); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1225,7 +1225,7 @@ transactionSignFor( return preprocResult.first; XRPL_ASSERT( - signForParams.validMultiSign(), "xrpl::RPC::transactionSignFor : valid multi-signature"); + signForParams.validMultiSign(), "xrpl::rpc::transactionSignFor : valid multi-signature"); { SLE::const_pointer const accountState = ledger->read(keylet::account(*signerAccountID)); @@ -1263,7 +1263,7 @@ transactionSignFor( // For delegated transactions, the delegate account is // the one forbidden from appearing in its own Signers array. auto err = sortAndValidateSigners(signers, sttx->getInitiator()); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1299,7 +1299,7 @@ transactionSubmitMultiSigned( using namespace detail; { json::Value err = checkMultiSignFields(jvRequest); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1314,7 +1314,7 @@ transactionSubmitMultiSigned( app.getFeeTrack(), getAPIVersionNumber(jvRequest, app.config().betaRpcApi)); - if (RPC::containsError(txJsonResult)) + if (rpc::containsError(txJsonResult)) return std::move(txJsonResult); SLE::const_pointer const sle = ledger->read(keylet::account(srcAddressID)); @@ -1332,12 +1332,12 @@ transactionSubmitMultiSigned( json::Value err = checkFee(jvRequest, role, false, app.config(), app.getFeeTrack(), app.getTxQ(), app); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; err = checkPayment(jvRequest, txJson, srcAddressID, role, app, false); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; } @@ -1359,17 +1359,17 @@ transactionSubmitMultiSigned( } catch (STObject::FieldErr const& err) { - return RPC::makeError(RpcInvalidParams, err.what()); + return rpc::makeError(RpcInvalidParams, err.what()); } catch (std::exception& ex) { std::string const reason(ex.what()); - return RPC::makeError( + return rpc::makeError( RpcInternal, "Exception while serializing transaction: " + reason); } std::string reason; if (!passesLocalChecks(*stTx, reason)) - return RPC::makeError(RpcInvalidParams, reason); + return rpc::makeError(RpcInvalidParams, reason); } // Validate the fields in the serialized transaction. @@ -1383,7 +1383,7 @@ transactionSubmitMultiSigned( std::ostringstream err; err << "Invalid " << sfSigningPubKey.fieldName << " field. Field must be empty when multi-signing."; - return RPC::makeError(RpcInvalidParams, err.str()); + return rpc::makeError(RpcInvalidParams, err.str()); } // There may not be a TxnSignature field. @@ -1397,26 +1397,26 @@ transactionSubmitMultiSigned( { std::ostringstream err; err << "Invalid " << sfFee.fieldName << " field. Fees must be specified in XRP."; - return RPC::makeError(RpcInvalidParams, err.str()); + return rpc::makeError(RpcInvalidParams, err.str()); } if (fee <= STAmount{0}) { std::ostringstream err; err << "Invalid " << sfFee.fieldName << " field. Fees must be greater than zero."; - return RPC::makeError(RpcInvalidParams, err.str()); + return rpc::makeError(RpcInvalidParams, err.str()); } } // Verify that the Signers field is present. if (!stTx->isFieldPresent(sfSigners)) - return RPC::missingFieldError("tx_json.Signers"); + return rpc::missingFieldError("tx_json.Signers"); // If the Signers field is present the SField guarantees it to be an array. // Get a reference to the Signers array so we can verify and sort it. auto& signers = stTx->peekFieldArray(sfSigners); if (signers.empty()) - return RPC::makeParamError("tx_json.Signers array may not be empty."); + return rpc::makeParamError("tx_json.Signers array may not be empty."); // The Signers array may only contain Signer objects. if (std::ranges::find_if_not(signers, [](STObject const& obj) { @@ -1427,14 +1427,14 @@ transactionSubmitMultiSigned( obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3); }) != signers.end()) { - return RPC::makeParamError("Signers array may only contain Signer entries."); + return rpc::makeParamError("Signers array may only contain Signer entries."); } // The array must be sorted and validated. // For delegated transactions, getInitiator() returns sfDelegate, // that account is the one forbidden from appearing in its own Signers array. auto err = sortAndValidateSigners(signers, stTx->getInitiator()); - if (RPC::containsError(err)) + if (rpc::containsError(err)) return err; // Make sure the SerializedTransaction makes a legitimate Transaction. @@ -1452,10 +1452,10 @@ transactionSubmitMultiSigned( } catch (std::exception&) { - return RPC::makeError(RpcInternal, "Exception occurred during transaction submission."); + return rpc::makeError(RpcInternal, "Exception occurred during transaction submission."); } return transactionFormatResultImpl(txn.second, apiVersion); } -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/detail/TransactionSign.h b/src/xrpld/rpc/detail/TransactionSign.h index dcb417dd16..9b07cbde20 100644 --- a/src/xrpld/rpc/detail/TransactionSign.h +++ b/src/xrpld/rpc/detail/TransactionSign.h @@ -20,7 +20,7 @@ class LoadFeeTrack; class Transaction; class TxQ; -namespace RPC { +namespace rpc { json::Value getCurrentNetworkFee( @@ -30,8 +30,8 @@ getCurrentNetworkFee( TxQ const& txQ, Application const& app, json::Value const& tx, - int mult = Tuning::kDefaultAutoFillFeeMultiplier, - int div = Tuning::kDefaultAutoFillFeeDivisor); + int mult = tuning::kDefaultAutoFillFeeMultiplier, + int div = tuning::kDefaultAutoFillFeeDivisor); /** * Fill in the fee on behalf of the client. @@ -140,5 +140,5 @@ transactionSubmitMultiSigned( Application& app, ProcessTransactionFn const& processTransaction); -} // namespace RPC +} // namespace rpc } // namespace xrpl diff --git a/src/xrpld/rpc/detail/Tuning.h b/src/xrpld/rpc/detail/Tuning.h index b904822698..5c47ab4365 100644 --- a/src/xrpld/rpc/detail/Tuning.h +++ b/src/xrpld/rpc/detail/Tuning.h @@ -6,7 +6,7 @@ * Tuned constants. */ /** @{ */ -namespace xrpl::RPC::Tuning { +namespace xrpl::rpc::tuning { /** * Represents RPC limit parameter values that have a min, default and max. @@ -98,5 +98,5 @@ static constexpr int kMaxSrcCur = 18; */ static constexpr int kMaxAutoSrcCur = 88; -} // namespace xrpl::RPC::Tuning +} // namespace xrpl::rpc::tuning /** @} */ diff --git a/src/xrpld/rpc/handlers/ChannelVerify.cpp b/src/xrpld/rpc/handlers/ChannelVerify.cpp index 64f616e829..ab54745a5d 100644 --- a/src/xrpld/rpc/handlers/ChannelVerify.cpp +++ b/src/xrpld/rpc/handlers/ChannelVerify.cpp @@ -27,13 +27,13 @@ namespace xrpl { // signature: signature to verify // } json::Value -doChannelVerify(RPC::JsonContext& context) +doChannelVerify(rpc::JsonContext& context) { auto const& params(context.params); for (auto const& p : {jss::public_key, jss::channel_id, jss::amount, jss::signature}) { if (!params.isMember(p)) - return RPC::missingFieldError(p); + return rpc::missingFieldError(p); } std::optional pk; diff --git a/src/xrpld/rpc/handlers/Handlers.h b/src/xrpld/rpc/handlers/Handlers.h index 7b347b2ecc..d192e8726c 100644 --- a/src/xrpld/rpc/handlers/Handlers.h +++ b/src/xrpld/rpc/handlers/Handlers.h @@ -7,147 +7,147 @@ namespace xrpl { json::Value -doAccountCurrencies(RPC::JsonContext&); +doAccountCurrencies(rpc::JsonContext&); json::Value -doAccountInfo(RPC::JsonContext&); +doAccountInfo(rpc::JsonContext&); json::Value -doAccountLines(RPC::JsonContext&); +doAccountLines(rpc::JsonContext&); json::Value -doAccountChannels(RPC::JsonContext&); +doAccountChannels(rpc::JsonContext&); json::Value -doAccountNFTs(RPC::JsonContext&); +doAccountNFTs(rpc::JsonContext&); json::Value -doAccountObjects(RPC::JsonContext&); +doAccountObjects(rpc::JsonContext&); json::Value -doAccountOffers(RPC::JsonContext&); +doAccountOffers(rpc::JsonContext&); json::Value -doAccountTx(RPC::JsonContext&); +doAccountTx(rpc::JsonContext&); json::Value -doAMMInfo(RPC::JsonContext&); +doAMMInfo(rpc::JsonContext&); json::Value -doBookOffers(RPC::JsonContext&); +doBookOffers(rpc::JsonContext&); json::Value -doBookChanges(RPC::JsonContext&); +doBookChanges(rpc::JsonContext&); json::Value -doBlackList(RPC::JsonContext&); +doBlackList(rpc::JsonContext&); json::Value -doCanDelete(RPC::JsonContext&); +doCanDelete(rpc::JsonContext&); json::Value -doChannelAuthorize(RPC::JsonContext&); +doChannelAuthorize(rpc::JsonContext&); json::Value -doChannelVerify(RPC::JsonContext&); +doChannelVerify(rpc::JsonContext&); json::Value -doConnect(RPC::JsonContext&); +doConnect(rpc::JsonContext&); json::Value -doConsensusInfo(RPC::JsonContext&); +doConsensusInfo(rpc::JsonContext&); json::Value -doDepositAuthorized(RPC::JsonContext&); +doDepositAuthorized(rpc::JsonContext&); json::Value -doFeature(RPC::JsonContext&); +doFeature(rpc::JsonContext&); json::Value -doFee(RPC::JsonContext&); +doFee(rpc::JsonContext&); json::Value -doFetchInfo(RPC::JsonContext&); +doFetchInfo(rpc::JsonContext&); json::Value -doGatewayBalances(RPC::JsonContext&); +doGatewayBalances(rpc::JsonContext&); json::Value -doGetCounts(RPC::JsonContext&); +doGetCounts(rpc::JsonContext&); json::Value -doGetAggregatePrice(RPC::JsonContext&); +doGetAggregatePrice(rpc::JsonContext&); json::Value -doLedgerAccept(RPC::JsonContext&); +doLedgerAccept(rpc::JsonContext&); json::Value -doLedgerCleaner(RPC::JsonContext&); +doLedgerCleaner(rpc::JsonContext&); json::Value -doLedgerClosed(RPC::JsonContext&); +doLedgerClosed(rpc::JsonContext&); json::Value -doLedgerCurrent(RPC::JsonContext&); +doLedgerCurrent(rpc::JsonContext&); json::Value -doLedgerData(RPC::JsonContext&); +doLedgerData(rpc::JsonContext&); json::Value -doLedgerEntry(RPC::JsonContext&); +doLedgerEntry(rpc::JsonContext&); json::Value -doLedgerHeader(RPC::JsonContext&); +doLedgerHeader(rpc::JsonContext&); json::Value -doLedgerRequest(RPC::JsonContext&); +doLedgerRequest(rpc::JsonContext&); json::Value -doLogLevel(RPC::JsonContext&); +doLogLevel(rpc::JsonContext&); json::Value -doLogRotate(RPC::JsonContext&); +doLogRotate(rpc::JsonContext&); json::Value -doManifest(RPC::JsonContext&); +doManifest(rpc::JsonContext&); json::Value -doNFTBuyOffers(RPC::JsonContext&); +doNFTBuyOffers(rpc::JsonContext&); json::Value -doNFTSellOffers(RPC::JsonContext&); +doNFTSellOffers(rpc::JsonContext&); json::Value -doNoRippleCheck(RPC::JsonContext&); +doNoRippleCheck(rpc::JsonContext&); json::Value -doOwnerInfo(RPC::JsonContext&); +doOwnerInfo(rpc::JsonContext&); json::Value -doPathFind(RPC::JsonContext&); +doPathFind(rpc::JsonContext&); json::Value -doPause(RPC::JsonContext&); +doPause(rpc::JsonContext&); json::Value -doPeers(RPC::JsonContext&); +doPeers(rpc::JsonContext&); json::Value -doPing(RPC::JsonContext&); +doPing(rpc::JsonContext&); json::Value -doPrint(RPC::JsonContext&); +doPrint(rpc::JsonContext&); json::Value -doRandom(RPC::JsonContext&); +doRandom(rpc::JsonContext&); json::Value -doResume(RPC::JsonContext&); +doResume(rpc::JsonContext&); json::Value -doPeerReservationsAdd(RPC::JsonContext&); +doPeerReservationsAdd(rpc::JsonContext&); json::Value -doPeerReservationsDel(RPC::JsonContext&); +doPeerReservationsDel(rpc::JsonContext&); json::Value -doPeerReservationsList(RPC::JsonContext&); +doPeerReservationsList(rpc::JsonContext&); json::Value -doRipplePathFind(RPC::JsonContext&); +doRipplePathFind(rpc::JsonContext&); json::Value -doServerDefinitions(RPC::JsonContext&); +doServerDefinitions(rpc::JsonContext&); json::Value -doServerInfo(RPC::JsonContext&); // for humans +doServerInfo(rpc::JsonContext&); // for humans json::Value -doServerState(RPC::JsonContext&); // for machines +doServerState(rpc::JsonContext&); // for machines json::Value -doSign(RPC::JsonContext&); +doSign(rpc::JsonContext&); json::Value -doSignFor(RPC::JsonContext&); +doSignFor(rpc::JsonContext&); json::Value -doSimulate(RPC::JsonContext&); +doSimulate(rpc::JsonContext&); json::Value -doStop(RPC::JsonContext&); +doStop(rpc::JsonContext&); json::Value -doSubmit(RPC::JsonContext&); +doSubmit(rpc::JsonContext&); json::Value -doSubmitMultiSigned(RPC::JsonContext&); +doSubmitMultiSigned(rpc::JsonContext&); json::Value -doSubscribe(RPC::JsonContext&); +doSubscribe(rpc::JsonContext&); json::Value -doTransactionEntry(RPC::JsonContext&); +doTransactionEntry(rpc::JsonContext&); json::Value -doTxJson(RPC::JsonContext&); +doTxJson(rpc::JsonContext&); json::Value -doTxHistory(RPC::JsonContext&); +doTxHistory(rpc::JsonContext&); json::Value -doTxReduceRelay(RPC::JsonContext&); +doTxReduceRelay(rpc::JsonContext&); json::Value -doUnlList(RPC::JsonContext&); +doUnlList(rpc::JsonContext&); json::Value -doUnsubscribe(RPC::JsonContext&); +doUnsubscribe(rpc::JsonContext&); json::Value -doValidationCreate(RPC::JsonContext&); +doValidationCreate(rpc::JsonContext&); json::Value -doWalletPropose(RPC::JsonContext&); +doWalletPropose(rpc::JsonContext&); json::Value -doValidators(RPC::JsonContext&); +doValidators(rpc::JsonContext&); json::Value -doValidatorListSites(RPC::JsonContext&); +doValidatorListSites(rpc::JsonContext&); json::Value -doValidatorInfo(RPC::JsonContext&); +doValidatorInfo(rpc::JsonContext&); json::Value -doVaultInfo(RPC::JsonContext&); +doVaultInfo(rpc::JsonContext&); } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp index b6d2fe259f..034cd8383f 100644 --- a/src/xrpld/rpc/handlers/VaultInfo.cpp +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp @@ -27,7 +27,7 @@ parseVault(json::Value const& params, json::Value& jvResult) { if (!uNodeIndex.parseHex(params[jss::vault_id].asString())) { - RPC::injectError(RpcInvalidParams, jvResult); + rpc::injectError(RpcInvalidParams, jvResult); return std::nullopt; } // else uNodeIndex holds the value we need @@ -37,14 +37,14 @@ parseVault(json::Value const& params, json::Value& jvResult) auto const id = parseBase58(params[jss::owner].asString()); if (!id) { - RPC::injectError(RpcActMalformed, jvResult); + rpc::injectError(RpcActMalformed, jvResult); return std::nullopt; } if (!(params[jss::seq].isInt() || params[jss::seq].isUInt()) || params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(json::Value::kMaxUInt)) { - RPC::injectError(RpcInvalidParams, jvResult); + rpc::injectError(RpcInvalidParams, jvResult); return std::nullopt; } @@ -53,7 +53,7 @@ parseVault(json::Value const& params, json::Value& jvResult) else { // Invalid combination of fields vault_id/owner/seq - RPC::injectError(RpcInvalidParams, jvResult); + rpc::injectError(RpcInvalidParams, jvResult); return std::nullopt; } @@ -61,10 +61,10 @@ parseVault(json::Value const& params, json::Value& jvResult) } json::Value -doVaultInfo(RPC::JsonContext& context) +doVaultInfo(rpc::JsonContext& context) { std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp b/src/xrpld/rpc/handlers/account/AccountChannels.cpp index 8a5c7dc6e3..d50bf1cf07 100644 --- a/src/xrpld/rpc/handlers/account/AccountChannels.cpp +++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp @@ -69,17 +69,17 @@ addChannel(json::Value& jsonLines, SLE const& line) // marker: opaque // optional, resume previous query // } json::Value -doAccountChannels(RPC::JsonContext& context) +doAccountChannels(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -97,7 +97,7 @@ doAccountChannels(RPC::JsonContext& context) if (params.isMember(jss::destination_account)) { if (!params[jss::destination_account].isString()) - return RPC::invalidFieldError(jss::destination_account); + return rpc::invalidFieldError(jss::destination_account); strDst = params[jss::destination_account].asString(); } @@ -108,7 +108,7 @@ doAccountChannels(RPC::JsonContext& context) return rpcError(RpcActMalformed); unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountChannels, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountChannels, context)) return *err; json::Value jsonChannels{json::ValueType::Array}; @@ -126,7 +126,7 @@ doAccountChannels(RPC::JsonContext& context) if (params.isMember(jss::marker)) { if (!params[jss::marker].isString()) - return RPC::expectedFieldError(jss::marker, "string"); + 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. @@ -157,7 +157,7 @@ doAccountChannels(RPC::JsonContext& context) if (!sle) return rpcError(RpcInvalidParams); - if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + if (!rpc::isRelatedToAccount(*ledger, sle, accountID)) return rpcError(RpcInvalidParams); } @@ -182,7 +182,7 @@ doAccountChannels(RPC::JsonContext& context) if (++count == limit) { marker = sleCur->key(); - nextHint = RPC::getStartHint(sleCur, visitData.accountID); + nextHint = rpc::getStartHint(sleCur, visitData.accountID); } if (count <= limit && sleCur->getType() == ltPAYCHAN && @@ -213,7 +213,7 @@ doAccountChannels(RPC::JsonContext& context) for (auto const& item : visitData.items) addChannel(jsonChannels, *item); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; result[jss::channels] = std::move(jsonChannels); return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp index 058c10e224..d9cd41cbbc 100644 --- a/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp +++ b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp @@ -19,30 +19,30 @@ namespace xrpl { json::Value -doAccountCurrencies(RPC::JsonContext& context) +doAccountCurrencies(rpc::JsonContext& context) { auto& params = context.params; if (!(params.isMember(jss::account) || params.isMember(jss::ident))) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); std::string strIdent; if (params.isMember(jss::account)) { if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); strIdent = params[jss::account].asString(); } else if (params.isMember(jss::ident)) { if (!params[jss::ident].isString()) - return RPC::invalidFieldError(jss::ident); + return rpc::invalidFieldError(jss::ident); strIdent = params[jss::ident].asString(); } // Get the current ledger std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -50,7 +50,7 @@ doAccountCurrencies(RPC::JsonContext& context) auto id = parseBase58(strIdent); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index 3a96593452..c618ad3b3a 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -84,7 +84,7 @@ injectSLE(json::Value& jv, SLE const& sle) // TODO(tom): what is that "default"? json::Value -doAccountInfo(RPC::JsonContext& context) +doAccountInfo(rpc::JsonContext& context) { auto& params = context.params; @@ -92,22 +92,22 @@ doAccountInfo(RPC::JsonContext& context) if (params.isMember(jss::account)) { if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); strIdent = params[jss::account].asString(); } else if (params.isMember(jss::ident)) { if (!params[jss::ident].isString()) - return RPC::invalidFieldError(jss::ident); + return rpc::invalidFieldError(jss::ident); strIdent = params[jss::ident].asString(); } else { - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); } std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -116,7 +116,7 @@ doAccountInfo(RPC::JsonContext& context) auto id = parseBase58(strIdent); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -154,7 +154,7 @@ doAccountInfo(RPC::JsonContext& context) { // It doesn't make sense to request the queue // with any closed or validated ledger. - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } @@ -206,7 +206,7 @@ doAccountInfo(RPC::JsonContext& context) if (context.apiVersion > 1u && params.isMember(jss::signer_lists) && !params[jss::signer_lists].isBool()) { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } @@ -331,7 +331,7 @@ doAccountInfo(RPC::JsonContext& context) else { result[jss::account] = toBase58(accountID); - RPC::injectError(RpcActNotFound, result); + rpc::injectError(RpcActNotFound, result); } return result; diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp index e69f70ca5a..f134c8af92 100644 --- a/src/xrpld/rpc/handlers/account/AccountLines.cpp +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp @@ -82,24 +82,24 @@ addLine(json::Value& jsonLines, RPCTrustLine const& line) // this account's side) // } json::Value -doAccountLines(RPC::JsonContext& context) +doAccountLines(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; auto id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -116,12 +116,12 @@ doAccountLines(RPC::JsonContext& context) }(); if (!strPeer.empty() && !raPeerAccount) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountLines, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountLines, context)) return *err; // this flag allows the requester to ask incoming trustlines in default @@ -150,7 +150,7 @@ doAccountLines(RPC::JsonContext& context) if (params.isMember(jss::marker)) { if (!params[jss::marker].isString()) - return RPC::expectedFieldError(jss::marker, "string"); + 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. @@ -181,7 +181,7 @@ doAccountLines(RPC::JsonContext& context) if (!sle) return rpcError(RpcInvalidParams); - if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + if (!rpc::isRelatedToAccount(*ledger, sle, accountID)) return rpcError(RpcInvalidParams); } @@ -207,7 +207,7 @@ doAccountLines(RPC::JsonContext& context) if (++count == limit) { marker = sleCur->key(); - nextHint = RPC::getStartHint(sleCur, visitData.accountID); + nextHint = rpc::getStartHint(sleCur, visitData.accountID); } if (sleCur->getType() != ltRIPPLE_STATE) @@ -259,7 +259,7 @@ doAccountLines(RPC::JsonContext& context) for (auto const& item : visitData.items) addLine(jsonLines, item); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp index ea9bec0f45..580b93caa3 100644 --- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp @@ -35,14 +35,14 @@ namespace xrpl { * } */ json::Value -doAccountNFTs(RPC::JsonContext& context) +doAccountNFTs(rpc::JsonContext& context) { auto const& params = context.params; if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); auto id = parseBase58(params[jss::account].asString()); if (!id) @@ -51,7 +51,7 @@ doAccountNFTs(RPC::JsonContext& context) } std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (ledger == nullptr) return result; auto const accountID{id.value()}; @@ -60,7 +60,7 @@ doAccountNFTs(RPC::JsonContext& context) return rpcError(RpcActNotFound); unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountNfTokens, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountNfTokens, context)) return *err; uint256 marker; @@ -70,10 +70,10 @@ doAccountNFTs(RPC::JsonContext& context) { auto const& m = params[jss::marker]; if (!m.isString()) - return RPC::expectedFieldError(jss::marker, "string"); + return rpc::expectedFieldError(jss::marker, "string"); if (!marker.parseHex(m.asString())) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); } auto const first = keylet::nftokenPage(keylet::nftokenPageMin(accountID), marker); @@ -125,7 +125,7 @@ doAccountNFTs(RPC::JsonContext& context) } if (markerSet && !markerFound) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); pastMarker = true; @@ -160,10 +160,10 @@ doAccountNFTs(RPC::JsonContext& context) } if (markerSet && !markerFound) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index ee2595bf94..e855ed65e6 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -265,24 +265,24 @@ getAccountObjects( } json::Value -doAccountObjects(RPC::JsonContext& context) +doAccountObjects(rpc::JsonContext& context) { auto const& params = context.params; if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (ledger == nullptr) return result; auto const id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -331,10 +331,10 @@ doAccountObjects(RPC::JsonContext& context) } else { - auto [rpcStatus, type] = RPC::chooseLedgerEntryType(params); + auto [rpcStatus, type] = rpc::chooseLedgerEntryType(params); - if (!RPC::isAccountObjectsValidType(type)) - return RPC::invalidFieldError(jss::type); + if (!rpc::isAccountObjectsValidType(type)) + return rpc::invalidFieldError(jss::type); if (rpcStatus) { @@ -349,7 +349,7 @@ doAccountObjects(RPC::JsonContext& context) } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountObjects, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountObjects, context)) return *err; uint256 dirIndex; @@ -358,18 +358,18 @@ doAccountObjects(RPC::JsonContext& context) { auto const& marker = params[jss::marker]; if (!marker.isString()) - return RPC::expectedFieldError(jss::marker, "string"); + return rpc::expectedFieldError(jss::marker, "string"); auto const& markerStr = marker.asString(); auto const& idx = markerStr.find(','); if (idx == std::string::npos) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!dirIndex.parseHex(markerStr.substr(0, idx))) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!entryIndex.parseHex(markerStr.substr(idx + 1))) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); } std::optional sponsoredFilter; @@ -377,17 +377,17 @@ doAccountObjects(RPC::JsonContext& context) { auto const& sponsoredJv = params[jss::sponsored]; if (!sponsoredJv.isBool()) - return RPC::expectedFieldError(jss::sponsored, "boolean"); + return rpc::expectedFieldError(jss::sponsored, "boolean"); sponsoredFilter = sponsoredJv.asBool(); } if (!getAccountObjects( *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsoredFilter, result)) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp b/src/xrpld/rpc/handlers/account/AccountOffers.cpp index 4829ff56b1..1467b14b48 100644 --- a/src/xrpld/rpc/handlers/account/AccountOffers.cpp +++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp @@ -54,24 +54,24 @@ appendOfferJson(SLE::const_ref offer, json::Value& offers) // marker: opaque // optional, resume previous query // } json::Value -doAccountOffers(RPC::JsonContext& context) +doAccountOffers(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; auto id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; @@ -83,7 +83,7 @@ doAccountOffers(RPC::JsonContext& context) return rpcError(RpcActNotFound); unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kAccountOffers, context)) + if (auto err = readLimitField(limit, rpc::tuning::kAccountOffers, context)) return *err; json::Value& jsonOffers(result[jss::offers] = json::ValueType::Array); @@ -94,20 +94,20 @@ doAccountOffers(RPC::JsonContext& context) if (params.isMember(jss::marker)) { if (!params[jss::marker].isString()) - return RPC::expectedFieldError(jss::marker, "string"); + 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. std::stringstream marker(params[jss::marker].asString()); std::string value; if (!std::getline(marker, value, ',')) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!startAfter.parseHex(value)) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); if (!std::getline(marker, value, ',')) - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); try { @@ -115,7 +115,7 @@ doAccountOffers(RPC::JsonContext& context) } catch (boost::bad_lexical_cast&) { - return RPC::invalidFieldError(jss::marker); + return rpc::invalidFieldError(jss::marker); } // We then must check if the object pointed to by the marker is actually @@ -125,7 +125,7 @@ doAccountOffers(RPC::JsonContext& context) if (!sle) return rpcError(RpcInvalidParams); - if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + if (!rpc::isRelatedToAccount(*ledger, sle, accountID)) return rpcError(RpcInvalidParams); } @@ -150,7 +150,7 @@ doAccountOffers(RPC::JsonContext& context) if (++count == limit) { marker = sle->key(); - nextHint = RPC::getStartHint(sle, accountID); + nextHint = rpc::getStartHint(sle, accountID); } if (count <= limit && sle->getType() == ltOFFER) @@ -176,7 +176,7 @@ doAccountOffers(RPC::JsonContext& context) for (auto const& offer : offers) appendOfferJson(offer, jsonOffers); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/account/AccountTx.cpp b/src/xrpld/rpc/handlers/account/AccountTx.cpp index c43f560861..7b0c34e048 100644 --- a/src/xrpld/rpc/handlers/account/AccountTx.cpp +++ b/src/xrpld/rpc/handlers/account/AccountTx.cpp @@ -43,11 +43,11 @@ static std::expected parseDelegateFilter(json::Value const& delegateNode) { if (!delegateNode.isObject()) - return std::unexpected(RPC::invalidFieldError(jss::delegate)); + return std::unexpected(rpc::invalidFieldError(jss::delegate)); if (!delegateNode.isMember(jss::delegate_filter) || !delegateNode[jss::delegate_filter].isString()) - return std::unexpected(RPC::invalidFieldError(jss::delegate_filter)); + return std::unexpected(rpc::invalidFieldError(jss::delegate_filter)); auto const& delegateFilterStr = delegateNode[jss::delegate_filter].asString(); @@ -58,7 +58,7 @@ parseDelegateFilter(json::Value const& delegateNode) if (delegateFilterStr == "authorizer") return DelegateType::Authorizer; - return std::unexpected(RPC::invalidFieldError(jss::delegate_filter)); + return std::unexpected(rpc::invalidFieldError(jss::delegate_filter)); }(); if (!typeResult) @@ -70,7 +70,7 @@ parseDelegateFilter(json::Value const& delegateNode) if (delegateNode.isMember(jss::counter_party)) { if (!delegateNode[jss::counter_party].isString()) - return std::unexpected(RPC::invalidFieldError(jss::counter_party)); + return std::unexpected(rpc::invalidFieldError(jss::counter_party)); counterparty = parseBase58(delegateNode[jss::counter_party].asString()); @@ -90,7 +90,7 @@ using LedgerSpecifier = RelationalDatabase::LedgerSpecifier; // parses args into a ledger specifier, or returns a Json object on error std::variant, json::Value> -parseLedgerArgs(RPC::Context& context, json::Value const& params) +parseLedgerArgs(rpc::Context& context, json::Value const& params) { json::Value response; // if ledger_index_min or max is specified, then ledger_hash or ledger_index @@ -100,7 +100,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) if ((params.isMember(jss::ledger_index_min) || params.isMember(jss::ledger_index_max)) && (params.isMember(jss::ledger_hash) || params.isMember(jss::ledger_index))) { - RPC::Status const status{RpcInvalidParams, "invalidParams"}; + rpc::Status const status{RpcInvalidParams, "invalidParams"}; status.inject(response); return response; } @@ -123,7 +123,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) auto& hashValue = params[jss::ledger_hash]; if (!hashValue.isString()) { - RPC::Status const status{RpcInvalidParams, "ledgerHashNotString"}; + rpc::Status const status{RpcInvalidParams, "ledgerHashNotString"}; status.inject(response); return response; } @@ -131,7 +131,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) LedgerHash hash; if (!hash.parseHex(hashValue.asString())) { - RPC::Status const status{RpcInvalidParams, "ledgerHashMalformed"}; + rpc::Status const status{RpcInvalidParams, "ledgerHashMalformed"}; status.inject(response); return response; } @@ -162,7 +162,7 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) } else { - RPC::Status const status{RpcInvalidParams, "ledger_index string malformed"}; + rpc::Status const status{RpcInvalidParams, "ledger_index string malformed"}; status.inject(response); return response; } @@ -172,8 +172,8 @@ parseLedgerArgs(RPC::Context& context, json::Value const& params) return std::optional{}; } -std::variant -getLedgerRange(RPC::Context& context, std::optional const& ledgerSpecifier) +std::variant +getLedgerRange(rpc::Context& context, std::optional const& ledgerSpecifier) { std::uint32_t uValidatedMin = 0; std::uint32_t uValidatedMax = 0; @@ -193,7 +193,7 @@ getLedgerRange(RPC::Context& context, std::optional const& ledg if (ledgerSpecifier) { auto status = std::visit( - [&](auto const& ls) -> RPC::Status { + [&](auto const& ls) -> rpc::Status { using T = std::decay_t; if constexpr (std::is_same_v) { @@ -241,7 +241,7 @@ getLedgerRange(RPC::Context& context, std::optional const& ledg } uLedgerMin = uLedgerMax = ledgerView->header().seq; } - return RPC::Status::kOK; + return rpc::Status::kOK; }, *ledgerSpecifier); @@ -251,15 +251,15 @@ getLedgerRange(RPC::Context& context, std::optional const& ledg return LedgerRange{.min = uLedgerMin, .max = uLedgerMax}; } -std::pair -doAccountTxHelp(RPC::Context& context, AccountTxArgs const& args) +std::pair +doAccountTxHelp(rpc::Context& context, AccountTxArgs const& args) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; AccountTxResult result; auto lgrRange = getLedgerRange(context, args.ledger); - if (auto stat = std::get_if(&lgrRange)) + if (auto stat = std::get_if(&lgrRange)) { // An error occurred getting the requested ledger range return {result, *stat}; @@ -318,12 +318,12 @@ doAccountTxHelp(RPC::Context& context, AccountTxArgs const& args) json::Value populateJsonResponse( - std::pair const& res, + std::pair const& res, AccountTxArgs const& args, - RPC::JsonContext const& context) + rpc::JsonContext const& context) { json::Value response; - RPC::Status const& error = res.second; + rpc::Status const& error = res.second; if (error.toErrorCode() != RpcSuccess) { error.inject(response); @@ -374,13 +374,13 @@ populateJsonResponse( } auto const& sttx = txn->getSTransaction(); - RPC::insertDeliverMax(jvObj[jsonTx], sttx->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(jvObj[jsonTx], sttx->getTxnType(), context.apiVersion); if (txnMeta) { jvObj[jss::meta] = txnMeta->getJson(JsonOptions::Values::IncludeDate); insertDeliveredAmount(jvObj[jss::meta], context, txn, *txnMeta); - RPC::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); - RPC::insertMPTokenIssuanceID(jvObj[jss::meta], sttx, *txnMeta); + rpc::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta); + rpc::insertMPTokenIssuanceID(jvObj[jss::meta], sttx, *txnMeta); } else { @@ -445,7 +445,7 @@ populateJsonResponse( // delegate-filtered query is only valid for a follow-up request that repeats // the same `delegate` object json::Value -doAccountTx(RPC::JsonContext& context) +doAccountTx(rpc::JsonContext& context) { if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); @@ -460,24 +460,24 @@ doAccountTx(RPC::JsonContext& context) // onwards only if (context.apiVersion > 1u && params.isMember(jss::binary) && !params[jss::binary].isBool()) { - return RPC::invalidFieldError(jss::binary); + return rpc::invalidFieldError(jss::binary); } if (context.apiVersion > 1u && params.isMember(jss::forward) && !params[jss::forward].isBool()) { - return RPC::invalidFieldError(jss::forward); + return rpc::invalidFieldError(jss::forward); } - if (auto const err = RPC::readLimitField(args.limit, RPC::Tuning::kAccountTx, context)) + if (auto const err = rpc::readLimitField(args.limit, rpc::tuning::kAccountTx, context)) return *err; args.binary = params.isMember(jss::binary) && params[jss::binary].asBool(); args.forward = params.isMember(jss::forward) && params[jss::forward].asBool(); if (!params.isMember(jss::account)) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); auto const account = parseBase58(params[jss::account].asString()); if (!account) @@ -500,7 +500,7 @@ doAccountTx(RPC::JsonContext& context) !token[jss::ledger].isConvertibleTo(json::ValueType::UInt) || !token[jss::seq].isConvertibleTo(json::ValueType::UInt)) { - RPC::Status const status{ + rpc::Status const status{ RpcInvalidParams, "invalid marker. Provide ledger index via ledger field, and " "transaction sequence number via seq field"}; @@ -534,7 +534,7 @@ doAccountTx(RPC::JsonContext& context) params[jss::marker][jss::delegate].asBool(); if (markerFromDelegate != args.delegate.has_value()) { - RPC::Status const status{ + rpc::Status const status{ RpcInvalidParams, "Do not mix delegate and non-delegate pagination markers in account_tx; " "repeat the same `delegate` object when using a delegate marker."}; diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp index a6730c8e2b..ff19d1d1e5 100644 --- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp @@ -49,19 +49,19 @@ namespace xrpl { // gateway_balances [] [ [ ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; if (!(params.isMember(jss::account) || params.isMember(jss::ident))) - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); std::string const strIdent( params.isMember(jss::account) ? params[jss::account].asString() @@ -72,13 +72,13 @@ doGatewayBalances(RPC::JsonContext& context) if (!id) return rpcError(RpcActMalformed); auto const accountID{id.value()}; - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; result[jss::account] = toBase58(accountID); if (context.apiVersion > 1u && !ledger->exists(keylet::account(accountID))) { - RPC::injectError(RpcActNotFound, result); + rpc::injectError(RpcActNotFound, result); return result; } @@ -126,11 +126,11 @@ doGatewayBalances(RPC::JsonContext& context) // not have currency issued by the account from the request. if (context.apiVersion < 2u) { - RPC::injectError(RpcInvalidHotwallet, result); + rpc::injectError(RpcInvalidHotwallet, result); } else { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); } return result; } diff --git a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp index d8bb65aba9..4be6e6f1af 100644 --- a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp +++ b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp @@ -26,7 +26,7 @@ namespace xrpl { static void fillTransaction( - RPC::JsonContext& context, + rpc::JsonContext& context, json::Value& txArray, AccountID const& accountID, std::uint32_t& sequence, @@ -49,17 +49,17 @@ fillTransaction( // transactions: true // optional, recommend transactions // } json::Value -doNoRippleCheck(RPC::JsonContext& context) +doNoRippleCheck(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return RPC::missingFieldError("account"); + return rpc::missingFieldError("account"); if (!params.isMember("role")) - return RPC::missingFieldError("role"); + return rpc::missingFieldError("role"); if (!params[jss::account].isString()) - return RPC::invalidFieldError(jss::account); + return rpc::invalidFieldError(jss::account); bool roleGateway = false; { @@ -70,12 +70,12 @@ doNoRippleCheck(RPC::JsonContext& context) } else if (role != "user") { - return RPC::invalidFieldError("role"); + return rpc::invalidFieldError("role"); } } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kNoRippleCheck, context)) + if (auto err = readLimitField(limit, rpc::tuning::kNoRippleCheck, context)) return *err; bool transactions = false; @@ -89,11 +89,11 @@ doNoRippleCheck(RPC::JsonContext& context) if (context.apiVersion > 1u && params.isMember(jss::transactions) && !params[jss::transactions].isBool()) { - return RPC::invalidFieldError(jss::transactions); + return rpc::invalidFieldError(jss::transactions); } std::shared_ptr ledger; - auto result = RPC::lookupLedger(ledger, context); + auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -104,7 +104,7 @@ doNoRippleCheck(RPC::JsonContext& context) auto id = parseBase58(params[jss::account].asString()); if (!id) { - RPC::injectError(RpcActMalformed, result); + rpc::injectError(RpcActMalformed, result); return result; } auto const accountID{id.value()}; diff --git a/src/xrpld/rpc/handlers/account/OwnerInfo.cpp b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp index 7cfed3cf53..2f5f76c619 100644 --- a/src/xrpld/rpc/handlers/account/OwnerInfo.cpp +++ b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp @@ -17,11 +17,11 @@ namespace xrpl { // 'ident' : , // } json::Value -doOwnerInfo(RPC::JsonContext& context) +doOwnerInfo(rpc::JsonContext& context) { if (!context.params.isMember(jss::account) && !context.params.isMember(jss::ident)) { - return RPC::missingFieldError(jss::account); + return rpc::missingFieldError(jss::account); } std::string const strIdent = context.params.isMember(jss::account) diff --git a/src/xrpld/rpc/handlers/admin/BlackList.cpp b/src/xrpld/rpc/handlers/admin/BlackList.cpp index 5065a41ec4..7a72651373 100644 --- a/src/xrpld/rpc/handlers/admin/BlackList.cpp +++ b/src/xrpld/rpc/handlers/admin/BlackList.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doBlackList(RPC::JsonContext& context) +doBlackList(rpc::JsonContext& context) { auto& rm = context.app.getResourceManager(); if (context.params.isMember(jss::threshold)) diff --git a/src/xrpld/rpc/handlers/admin/UnlList.cpp b/src/xrpld/rpc/handlers/admin/UnlList.cpp index c3835c7ae0..61b5e4c640 100644 --- a/src/xrpld/rpc/handlers/admin/UnlList.cpp +++ b/src/xrpld/rpc/handlers/admin/UnlList.cpp @@ -12,7 +12,7 @@ namespace xrpl { json::Value -doUnlList(RPC::JsonContext& context) +doUnlList(rpc::JsonContext& context) { json::Value obj(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp index 9ec1157e66..91db16bb4f 100644 --- a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp @@ -19,10 +19,10 @@ namespace xrpl { // can_delete [||now|always|never] json::Value -doCanDelete(RPC::JsonContext& context) +doCanDelete(rpc::JsonContext& context) { if (!context.app.getSHAMapStore().advisoryDelete()) - return RPC::makeError(RpcNotEnabled); + return rpc::makeError(RpcNotEnabled); json::Value ret(json::ValueType::Object); @@ -56,20 +56,20 @@ doCanDelete(RPC::JsonContext& context) { canDeleteSeq = context.app.getSHAMapStore().getLastRotated(); if (canDeleteSeq == 0u) - return RPC::makeError(RpcNotReady); + return rpc::makeError(RpcNotReady); } else if (uint256 lh; lh.parseHex(canDeleteStr)) { auto ledger = context.ledgerMaster.getLedgerByHash(lh); if (!ledger) - return RPC::makeError(RpcLgrNotFound, "ledgerNotFound"); + return rpc::makeError(RpcLgrNotFound, "ledgerNotFound"); canDeleteSeq = ledger->header().seq; } else { - return RPC::makeError(RpcInvalidParams); + return rpc::makeError(RpcInvalidParams); } } diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp index a62ca44cca..78a2fec410 100644 --- a/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp +++ b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp @@ -9,10 +9,10 @@ namespace xrpl { json::Value -doLedgerCleaner(RPC::JsonContext& context) +doLedgerCleaner(rpc::JsonContext& context) { context.app.getLedgerCleaner().clean(context.params); - return RPC::makeObjectValue("Cleaner configured"); + return rpc::makeObjectValue("Cleaner configured"); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp index de1e4439cf..80a49aea3f 100644 --- a/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp +++ b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp @@ -13,10 +13,10 @@ namespace xrpl { // ledger_index : // } json::Value -doLedgerRequest(RPC::JsonContext& context) +doLedgerRequest(rpc::JsonContext& context) { - context.loadType = Resource::kFeeHeavyBurdenRpc; - auto res = RPC::getOrAcquireLedger(context); + context.loadType = resource::kFeeHeavyBurdenRpc; + auto res = rpc::getOrAcquireLedger(context); if (!res.has_value()) return res.error(); diff --git a/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp index 0849bad944..5ff3e6727e 100644 --- a/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp +++ b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp @@ -30,7 +30,7 @@ validationSeed(json::Value const& params) // This command requires Role::ADMIN access because it makes // no sense to ask an untrusted server for this. json::Value -doValidationCreate(RPC::JsonContext& context) +doValidationCreate(rpc::JsonContext& context) { json::Value obj(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp index 4b5f1821e3..62def76f9a 100644 --- a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp @@ -52,7 +52,7 @@ estimateEntropy(std::string const& input) // passphrase: // } json::Value -doWalletPropose(RPC::JsonContext& context) +doWalletPropose(rpc::JsonContext& context) { return walletPropose(context.params); } @@ -68,7 +68,7 @@ walletPropose(json::Value const& params) { if (!params[jss::key_type].isString()) { - return RPC::expectedFieldError(jss::key_type, "string"); + return rpc::expectedFieldError(jss::key_type, "string"); } keyType = keyTypeFromString(params[jss::key_type].asString()); @@ -83,11 +83,11 @@ walletPropose(json::Value const& params) { if (params.isMember(jss::passphrase)) { - seed = RPC::parseXrplLibSeed(params[jss::passphrase]); + seed = rpc::parseXrplLibSeed(params[jss::passphrase]); } else if (params.isMember(jss::seed)) { - seed = RPC::parseXrplLibSeed(params[jss::seed]); + seed = rpc::parseXrplLibSeed(params[jss::seed]); } if (seed) @@ -110,7 +110,7 @@ walletPropose(json::Value const& params) { json::Value err; - seed = RPC::getSeedFromRPC(params, err); + seed = rpc::getSeedFromRPC(params, err); if (!seed) return err; diff --git a/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp index 1ff5aa1a27..4aae350810 100644 --- a/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp +++ b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp @@ -16,7 +16,7 @@ namespace xrpl { json::Value -doLogLevel(RPC::JsonContext& context) +doLogLevel(rpc::JsonContext& context) { // log_level if (not context.params.isMember(jss::severity)) diff --git a/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp index 5f5c3e64df..ca935540dc 100644 --- a/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp +++ b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp @@ -9,10 +9,10 @@ namespace xrpl { json::Value -doLogRotate(RPC::JsonContext& context) +doLogRotate(rpc::JsonContext& context) { context.app.getPerfLog().rotate(); - return RPC::makeObjectValue(context.app.getLogs().rotate()); + return rpc::makeObjectValue(context.app.getLogs().rotate()); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/admin/peer/Connect.cpp b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp index 568dcdaa26..b318af06f9 100644 --- a/src/xrpld/rpc/handlers/admin/peer/Connect.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp @@ -21,15 +21,15 @@ namespace xrpl { // } // XXX Might allow domain for manual connections. json::Value -doConnect(RPC::JsonContext& context) +doConnect(rpc::JsonContext& context) { if (context.app.config().standalone()) { - return RPC::makeError(RpcNotSynced); + return rpc::makeError(RpcNotSynced); } if (!context.params.isMember(jss::ip)) - return RPC::missingFieldError(jss::ip); + return rpc::missingFieldError(jss::ip); if (context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(json::ValueType::Int)) @@ -49,12 +49,12 @@ doConnect(RPC::JsonContext& context) } auto const ipStr = context.params[jss::ip].asString(); - auto ip = beast::IP::Endpoint::fromString(ipStr); + auto ip = beast::ip::Endpoint::fromString(ipStr); if (!isUnspecified(ip)) context.app.getOverlay().connect(ip.atPort(iPort)); - return RPC::makeObjectValue( + return rpc::makeObjectValue( "attempting connection to IP:" + ipStr + " port: " + std::to_string(iPort)); } diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp index 23b0d094b4..579ad85f41 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp @@ -15,12 +15,12 @@ namespace xrpl { json::Value -doPeerReservationsAdd(RPC::JsonContext& context) +doPeerReservationsAdd(rpc::JsonContext& context) { auto const& params = context.params; if (!params.isMember(jss::public_key)) - return RPC::missingFieldError(jss::public_key); + return rpc::missingFieldError(jss::public_key); // Returning JSON from every function ruins any attempt to encapsulate // the pattern of "get field F as type T, and diagnose an error if it is @@ -36,7 +36,7 @@ doPeerReservationsAdd(RPC::JsonContext& context) // essentially an optional (the "maybe monad" in Haskell) with a non-unit // type for the failure case to capture more information. if (!params[jss::public_key].isString()) - return RPC::expectedFieldError(jss::public_key, "a string"); + return rpc::expectedFieldError(jss::public_key, "a string"); // Same for the pattern of "if field F is present, make sure it has type T // and get it". @@ -44,7 +44,7 @@ doPeerReservationsAdd(RPC::JsonContext& context) if (params.isMember(jss::description)) { if (!params[jss::description].isString()) - return RPC::expectedFieldError(jss::description, "a string"); + return rpc::expectedFieldError(jss::description, "a string"); desc = params[jss::description].asString(); } diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp index c2a8319876..e5912a5eca 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp @@ -14,15 +14,15 @@ namespace xrpl { json::Value -doPeerReservationsDel(RPC::JsonContext& context) +doPeerReservationsDel(rpc::JsonContext& context) { auto const& params = context.params; // We repeat much of the parameter parsing from `doPeerReservationsAdd`. if (!params.isMember(jss::public_key)) - return RPC::missingFieldError(jss::public_key); + return rpc::missingFieldError(jss::public_key); if (!params[jss::public_key].isString()) - return RPC::expectedFieldError(jss::public_key, "a string"); + return rpc::expectedFieldError(jss::public_key, "a string"); std::optional optPk = parseBase58(TokenType::NodePublic, params[jss::public_key].asString()); diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp index e0204159fd..30e19a5c0b 100644 --- a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doPeerReservationsList(RPC::JsonContext& context) +doPeerReservationsList(rpc::JsonContext& context) { auto const& reservations = context.app.getPeerReservations().list(); // Enumerate the reservations in context.app.getPeerReservations() diff --git a/src/xrpld/rpc/handlers/admin/peer/Peers.cpp b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp index ab14325f0e..99f069e27e 100644 --- a/src/xrpld/rpc/handlers/admin/peer/Peers.cpp +++ b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp @@ -17,7 +17,7 @@ namespace xrpl { json::Value -doPeers(RPC::JsonContext& context) +doPeers(rpc::JsonContext& context) { json::Value jvResult(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp index ce06a6e480..00a259bb52 100644 --- a/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp +++ b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp @@ -12,7 +12,7 @@ namespace xrpl { json::Value -doLedgerAccept(RPC::JsonContext& context) +doLedgerAccept(rpc::JsonContext& context) { json::Value jvResult; diff --git a/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp index 3e86bd4632..949eccf1e1 100644 --- a/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp +++ b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp @@ -6,15 +6,15 @@ namespace xrpl { -namespace RPC { +namespace rpc { struct JsonContext; -} // namespace RPC +} // namespace rpc json::Value -doStop(RPC::JsonContext& context) +doStop(rpc::JsonContext& context) { context.app.signalStop("RPC"); - return RPC::makeObjectValue(systemName() + " server stopping"); + return rpc::makeObjectValue(systemName() + " server stopping"); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp index be3ce13d45..d97ce9dac4 100644 --- a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp @@ -30,34 +30,34 @@ namespace xrpl { // drops: 64-bit uint (as string) // } json::Value -doChannelAuthorize(RPC::JsonContext& context) +doChannelAuthorize(rpc::JsonContext& context) { if (context.role != Role::ADMIN && !context.app.config().canSign()) { - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); } auto const& params(context.params); for (auto const& p : {jss::channel_id, jss::amount}) { if (!params.isMember(p)) - return RPC::missingFieldError(p); + return rpc::missingFieldError(p); } // Compatibility if a key type isn't specified. If it is, the // keypairForSignature code will validate parameters and return // the appropriate error. if (!params.isMember(jss::key_type) && !params.isMember(jss::secret)) - return RPC::missingFieldError(jss::secret); + return rpc::missingFieldError(jss::secret); json::Value result; std::optional> const keyPair = - RPC::keypairForSignature(params, result, context.apiVersion); + rpc::keypairForSignature(params, result, context.apiVersion); XRPL_ASSERT( - keyPair || RPC::containsError(result), + keyPair || rpc::containsError(result), "xrpl::doChannelAuthorize : valid keyPair or an error"); - if (!keyPair || RPC::containsError(result)) + if (!keyPair || rpc::containsError(result)) return result; PublicKey const& pk = keyPair->first; @@ -86,7 +86,7 @@ doChannelAuthorize(RPC::JsonContext& context) catch (std::exception const& ex) { // LCOV_EXCL_START - result = RPC::makeError( + result = rpc::makeError( RpcInternal, "Exception occurred during signing: " + std::string(ex.what())); // LCOV_EXCL_STOP } diff --git a/src/xrpld/rpc/handlers/admin/signing/Sign.cpp b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp index 781e160f54..6aac058a56 100644 --- a/src/xrpld/rpc/handlers/admin/signing/Sign.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp @@ -15,18 +15,18 @@ namespace xrpl { // secret: // } json::Value -doSign(RPC::JsonContext& context) +doSign(rpc::JsonContext& context) { if (context.role != Role::ADMIN && !context.app.config().canSign()) { - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); } - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; NetworkOPs::FailHard const failType = NetworkOPs::doFailHard( context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()); - auto ret = RPC::transactionSign( + auto ret = rpc::transactionSign( context.params, context.apiVersion, failType, diff --git a/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp index 35274d51f4..2b9c830647 100644 --- a/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp +++ b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp @@ -16,18 +16,18 @@ namespace xrpl { // secret: // } json::Value -doSignFor(RPC::JsonContext& context) +doSignFor(rpc::JsonContext& context) { if (context.role != Role::ADMIN && !context.app.config().canSign()) { - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); } - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; auto const failHard = context.params[jss::fail_hard].asBool(); auto const failType = NetworkOPs::doFailHard(failHard); - auto ret = RPC::transactionSignFor( + auto ret = rpc::transactionSignFor( context.params, context.apiVersion, failType, diff --git a/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp index 341980ba6d..8017e7058b 100644 --- a/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp +++ b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doConsensusInfo(RPC::JsonContext& context) +doConsensusInfo(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp index 16e4789025..ca9bff31f5 100644 --- a/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp +++ b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doFetchInfo(RPC::JsonContext& context) +doFetchInfo(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp index 789c6dcf17..421f23d237 100644 --- a/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp @@ -111,7 +111,7 @@ getCountsJson(Application& app, int minObjectCount) // min_count: // optional, defaults to 10 // } json::Value -doGetCounts(RPC::JsonContext& context) +doGetCounts(rpc::JsonContext& context) { int minCount = 10; diff --git a/src/xrpld/rpc/handlers/admin/status/Print.cpp b/src/xrpld/rpc/handlers/admin/status/Print.cpp index 99fd01c9f4..1e1f7f0662 100644 --- a/src/xrpld/rpc/handlers/admin/status/Print.cpp +++ b/src/xrpld/rpc/handlers/admin/status/Print.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doPrint(RPC::JsonContext& context) +doPrint(rpc::JsonContext& context) { JsonPropertyStream stream; if (context.params.isObject() && context.params[jss::params].isArray() && diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp index efe1529ccb..705e03c1a0 100644 --- a/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp @@ -12,12 +12,12 @@ namespace xrpl { json::Value -doValidatorInfo(RPC::JsonContext& context) +doValidatorInfo(rpc::JsonContext& context) { // return error if not configured as validator auto const validationPK = context.app.getValidationPublicKey(); if (!validationPK) - return RPC::notValidatorError(); + return rpc::notValidatorError(); json::Value ret; diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp index 7497105c50..9bc8b6bcda 100644 --- a/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doValidatorListSites(RPC::JsonContext& context) +doValidatorListSites(rpc::JsonContext& context) { return context.app.getValidatorSites().getJson(); } diff --git a/src/xrpld/rpc/handlers/admin/status/Validators.cpp b/src/xrpld/rpc/handlers/admin/status/Validators.cpp index 48e4466861..d605a38f1b 100644 --- a/src/xrpld/rpc/handlers/admin/status/Validators.cpp +++ b/src/xrpld/rpc/handlers/admin/status/Validators.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doValidators(RPC::JsonContext& context) +doValidators(rpc::JsonContext& context) { return context.app.getValidators().getJson(); } diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.cpp b/src/xrpld/rpc/handlers/ledger/Ledger.cpp index 23a97a5026..51f5bdf348 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.cpp +++ b/src/xrpld/rpc/handlers/ledger/Ledger.cpp @@ -34,7 +34,7 @@ #include namespace xrpl { -namespace RPC { +namespace rpc { LedgerHandler::LedgerHandler(JsonContext& context) : context_(context) { @@ -107,7 +107,7 @@ LedgerHandler::check() { return RpcTooBusy; } - context_.loadType = binary ? Resource::kFeeMediumBurdenRpc : Resource::kFeeHeavyBurdenRpc; + context_.loadType = binary ? resource::kFeeMediumBurdenRpc : resource::kFeeHeavyBurdenRpc; } if (*queue) @@ -162,10 +162,10 @@ LedgerHandler::writeResult(json::Value& value) value[jss::warnings] = std::move(warnings); } -} // namespace RPC +} // namespace rpc std::pair -doLedgerGrpc(RPC::GRPCContext& context) +doLedgerGrpc(rpc::GRPCContext& context) { auto begin = std::chrono::system_clock::now(); org::xrpl::rpc::v1::GetLedgerRequest const& request = context.params; @@ -173,7 +173,7 @@ doLedgerGrpc(RPC::GRPCContext& context) grpc::Status const status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = rpc::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == RpcInvalidParams) diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h b/src/xrpld/rpc/handlers/ledger/Ledger.h index 59b64832f7..07d24b497d 100644 --- a/src/xrpld/rpc/handlers/ledger/Ledger.h +++ b/src/xrpld/rpc/handlers/ledger/Ledger.h @@ -18,7 +18,7 @@ namespace json { class Object; } // namespace json -namespace xrpl::RPC { +namespace xrpl::rpc { struct JsonContext; @@ -42,9 +42,9 @@ public: // NOLINTBEGIN(readability-identifier-naming) static constexpr char name[] = "ledger"; - static constexpr unsigned minApiVer = RPC::kApiMinimumSupportedVersion; + static constexpr unsigned minApiVer = rpc::kApiMinimumSupportedVersion; - static constexpr unsigned maxApiVer = RPC::kApiMaximumValidVersion; + static constexpr unsigned maxApiVer = rpc::kApiMaximumValidVersion; static constexpr Role role = Role::USER; @@ -59,4 +59,4 @@ private: int options_ = 0; }; -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp index 7ea314292f..def0a73cd3 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp @@ -10,7 +10,7 @@ namespace xrpl { json::Value -doLedgerClosed(RPC::JsonContext& context) +doLedgerClosed(rpc::JsonContext& context) { auto ledger = context.ledgerMaster.getClosedLedger(); XRPL_ASSERT(ledger, "xrpl::doLedgerClosed : non-null closed ledger"); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp index 1d05774163..ac04084848 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doLedgerCurrent(RPC::JsonContext& context) +doLedgerCurrent(rpc::JsonContext& context) { json::Value jvResult; jvResult[jss::ledger_current_index] = context.ledgerMaster.getCurrentLedgerIndex(); diff --git a/src/xrpld/rpc/handlers/ledger/LedgerData.cpp b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp index 64ab30374b..697d8e52b8 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerData.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp @@ -36,12 +36,12 @@ namespace xrpl { // state: array of state nodes // marker: resume point, if any json::Value -doLedgerData(RPC::JsonContext& context) +doLedgerData(rpc::JsonContext& context) { std::shared_ptr lpLedger; auto const& params = context.params; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; @@ -51,14 +51,14 @@ doLedgerData(RPC::JsonContext& context) { json::Value const& jMarker = params[jss::marker]; if (!(jMarker.isString() && key.parseHex(jMarker.asString()))) - return RPC::expectedFieldError(jss::marker, "valid"); + return rpc::expectedFieldError(jss::marker, "valid"); } bool isBinary = false; if (params.isMember(jss::binary)) { if (!params[jss::binary].isBool()) - return RPC::expectedFieldError(jss::binary, "boolean"); + return rpc::expectedFieldError(jss::binary, "boolean"); isBinary = params[jss::binary].asBool(); } @@ -67,12 +67,12 @@ doLedgerData(RPC::JsonContext& context) { json::Value const& jLimit = params[jss::limit]; if (!jLimit.isIntegral()) - return RPC::expectedFieldError(jss::limit, "integer"); + return rpc::expectedFieldError(jss::limit, "integer"); limit = jLimit.asInt(); } - auto maxLimit = RPC::Tuning::pageLength(isBinary); + auto maxLimit = rpc::tuning::pageLength(isBinary); if ((limit < 0) || ((limit > maxLimit) && (!isUnlimited(context.role)))) limit = maxLimit; @@ -86,7 +86,7 @@ doLedgerData(RPC::JsonContext& context) *lpLedger, &context, isBinary ? static_cast(LedgerFill::Options::Binary) : 0)); } - auto [rpcStatus, type] = RPC::chooseLedgerEntryType(params); + auto [rpcStatus, type] = rpc::chooseLedgerEntryType(params); if (rpcStatus) { jvResult.clear(); @@ -131,14 +131,14 @@ doLedgerData(RPC::JsonContext& context) } std::pair -doLedgerDataGrpc(RPC::GRPCContext& context) +doLedgerDataGrpc(rpc::GRPCContext& context) { org::xrpl::rpc::v1::GetLedgerDataRequest const& request = context.params; org::xrpl::rpc::v1::GetLedgerDataResponse response; grpc::Status const status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = rpc::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == RpcInvalidParams) @@ -177,7 +177,7 @@ doLedgerDataGrpc(RPC::GRPCContext& con e = ledger->sles.upperBound(*key); } - int maxLimit = RPC::Tuning::pageLength(true); + int maxLimit = rpc::tuning::pageLength(true); for (auto i = ledger->sles.upperBound(startKey); i != e; ++i) { diff --git a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp index f1a9253de2..5e83bedf08 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp @@ -15,7 +15,7 @@ namespace xrpl { std::pair -doLedgerDiffGrpc(RPC::GRPCContext& context) +doLedgerDiffGrpc(rpc::GRPCContext& context) { org::xrpl::rpc::v1::GetLedgerDiffRequest const& request = context.params; org::xrpl::rpc::v1::GetLedgerDiffResponse response; @@ -24,13 +24,13 @@ doLedgerDiffGrpc(RPC::GRPCContext& con std::shared_ptr baseLedgerRv; std::shared_ptr desiredLedgerRv; - if (RPC::ledgerFromSpecifier(baseLedgerRv, request.base_ledger(), context)) + if (rpc::ledgerFromSpecifier(baseLedgerRv, request.base_ledger(), context)) { grpc::Status const errorStatus{grpc::StatusCode::NOT_FOUND, "base ledger not found"}; return {response, errorStatus}; } - if (RPC::ledgerFromSpecifier(desiredLedgerRv, request.desired_ledger(), context)) + if (rpc::ledgerFromSpecifier(desiredLedgerRv, request.desired_ledger(), context)) { grpc::Status const errorStatus{grpc::StatusCode::NOT_FOUND, "desired ledger not found"}; return {response, errorStatus}; diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 784be779bb..0dd52b6776 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -66,11 +66,11 @@ parseObjectID( json::StaticString const fieldName, std::string const& expectedType = "hex string or object") { - if (auto const uNodeIndex = LedgerEntryHelpers::parse(params)) + if (auto const uNodeIndex = ledger_entry_helpers::parse(params)) { return *uNodeIndex; } - return LedgerEntryHelpers::invalidFieldError("malformedRequest", fieldName, expectedType); + return ledger_entry_helpers::invalidFieldError("malformedRequest", fieldName, expectedType); } static std::expected @@ -101,12 +101,12 @@ parseAccountRoot( json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - if (auto const account = LedgerEntryHelpers::parse(params)) + if (auto const account = ledger_entry_helpers::parse(params)) { return keylet::account(*account).key; } - return LedgerEntryHelpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); + return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); } auto const parseAmendments = fixed(keylet::amendments()); @@ -122,17 +122,18 @@ parseAMM( return parseObjectID(params, fieldName); } - if (auto const value = LedgerEntryHelpers::hasRequired(params, {jss::asset, jss::asset2}); + if (auto const value = ledger_entry_helpers::hasRequired(params, {jss::asset, jss::asset2}); !value) { return std::unexpected(value.error()); } - auto const asset = LedgerEntryHelpers::requiredAsset(params, jss::asset, "malformedRequest"); + auto const asset = ledger_entry_helpers::requiredAsset(params, jss::asset, "malformedRequest"); if (!asset) return std::unexpected(asset.error()); - auto const asset2 = LedgerEntryHelpers::requiredAsset(params, jss::asset2, "malformedRequest"); + auto const asset2 = + ledger_entry_helpers::requiredAsset(params, jss::asset2, "malformedRequest"); if (!asset2) return std::unexpected(asset2.error()); @@ -147,7 +148,7 @@ parseBridge( { if (!params.isMember(jss::bridge)) { - return std::unexpected(LedgerEntryHelpers::missingFieldError(jss::bridge)); + return std::unexpected(ledger_entry_helpers::missingFieldError(jss::bridge)); } if (params[jss::bridge].isString()) @@ -155,11 +156,11 @@ parseBridge( return parseObjectID(params, fieldName); } - auto const bridge = LedgerEntryHelpers::parseBridgeFields(params[jss::bridge]); + auto const bridge = ledger_entry_helpers::parseBridgeFields(params[jss::bridge]); if (!bridge) return std::unexpected(bridge.error()); - auto const account = LedgerEntryHelpers::requiredAccountID( + auto const account = ledger_entry_helpers::requiredAccountID( params, jss::bridge_account, "malformedBridgeAccount"); if (!account) return std::unexpected(account.error()); @@ -167,7 +168,7 @@ parseBridge( STXChainBridge::ChainType const chainType = STXChainBridge::srcChain(account.value() == bridge->lockingChainDoor()); if (account.value() != bridge->door(chainType)) - return LedgerEntryHelpers::malformedError("malformedRequest", ""); + return ledger_entry_helpers::malformedError("malformedRequest", ""); return keylet::bridge(*bridge, chainType).key; } @@ -193,16 +194,16 @@ parseCredential( } auto const subject = - LedgerEntryHelpers::requiredAccountID(cred, jss::subject, "malformedRequest"); + ledger_entry_helpers::requiredAccountID(cred, jss::subject, "malformedRequest"); if (!subject) return std::unexpected(subject.error()); auto const issuer = - LedgerEntryHelpers::requiredAccountID(cred, jss::issuer, "malformedRequest"); + ledger_entry_helpers::requiredAccountID(cred, jss::issuer, "malformedRequest"); if (!issuer) return std::unexpected(issuer.error()); - auto const credType = LedgerEntryHelpers::requiredHexBlob( + auto const credType = ledger_entry_helpers::requiredHexBlob( cred, jss::credential_type, kMaxCredentialTypeLength, "malformedRequest"); if (!credType) return std::unexpected(credType.error()); @@ -222,12 +223,12 @@ parseDelegate( } auto const account = - LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); if (!account) return std::unexpected(account.error()); auto const authorize = - LedgerEntryHelpers::requiredAccountID(params, jss::authorize, "malformedAddress"); + ledger_entry_helpers::requiredAccountID(params, jss::authorize, "malformedAddress"); if (!authorize) return std::unexpected(authorize.error()); @@ -239,7 +240,7 @@ parseAuthorizeCredentials(json::Value const& jv) { if (!jv.isArray()) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorizedCredentials", jss::authorized_credentials, "array"); } @@ -247,7 +248,7 @@ parseAuthorizeCredentials(json::Value const& jv) if (n > kMaxCredentialsArraySize) { return std::unexpected( - LedgerEntryHelpers::malformedError( + ledger_entry_helpers::malformedError( "malformedAuthorizedCredentials", "Invalid field '" + std::string(jss::authorized_credentials) + "', array too long.")); @@ -256,7 +257,7 @@ parseAuthorizeCredentials(json::Value const& jv) if (n == 0) { return std::unexpected( - LedgerEntryHelpers::malformedError( + ledger_entry_helpers::malformedError( "malformedAuthorizedCredentials", "Invalid field '" + std::string(jss::authorized_credentials) + "', array empty.")); } @@ -266,23 +267,23 @@ parseAuthorizeCredentials(json::Value const& jv) { if (!jo.isObject()) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorizedCredentials", jss::authorized_credentials, "array of objects"); } - if (auto const value = LedgerEntryHelpers::hasRequired( + if (auto const value = ledger_entry_helpers::hasRequired( jo, {jss::issuer, jss::credential_type}, "malformedAuthorizedCredentials"); !value) { return std::unexpected(value.error()); } - auto const issuer = LedgerEntryHelpers::requiredAccountID( + auto const issuer = ledger_entry_helpers::requiredAccountID( jo, jss::issuer, "malformedAuthorizedCredentials"); if (!issuer) return std::unexpected(issuer.error()); - auto const credentialType = LedgerEntryHelpers::requiredHexBlob( + auto const credentialType = ledger_entry_helpers::requiredHexBlob( jo, jss::credential_type, kMaxCredentialTypeLength, "malformedAuthorizedCredentials"); if (!credentialType) return std::unexpected(credentialType.error()); @@ -309,13 +310,13 @@ parseDepositPreauth( if ((dp.isMember(jss::authorized) == dp.isMember(jss::authorized_credentials))) { - return LedgerEntryHelpers::malformedError( + return ledger_entry_helpers::malformedError( "malformedRequest", "Must have exactly one of `authorized` and " "`authorized_credentials`."); } - auto const owner = LedgerEntryHelpers::requiredAccountID(dp, jss::owner, "malformedOwner"); + auto const owner = ledger_entry_helpers::requiredAccountID(dp, jss::owner, "malformedOwner"); if (!owner) { return std::unexpected(owner.error()); @@ -323,11 +324,11 @@ parseDepositPreauth( if (dp.isMember(jss::authorized)) { - if (auto const authorized = LedgerEntryHelpers::parse(dp[jss::authorized])) + if (auto const authorized = ledger_entry_helpers::parse(dp[jss::authorized])) { return keylet::depositPreauth(*owner, *authorized).key; } - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorized", jss::authorized, "AccountID"); } @@ -340,7 +341,7 @@ parseDepositPreauth( if (sorted.empty()) { // TODO: this error message is bad/inaccurate - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAuthorizedCredentials", jss::authorized_credentials, "array"); } @@ -353,10 +354,10 @@ parseDID( json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - auto const account = LedgerEntryHelpers::parse(params); + auto const account = ledger_entry_helpers::parse(params); if (!account) { - return LedgerEntryHelpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); + return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID"); } return keylet::did(*account).key; @@ -377,12 +378,13 @@ parseDirectoryNode( (!params[jss::sub_index].isConvertibleTo(json::ValueType::UInt) || params[jss::sub_index].isBool())) { - return LedgerEntryHelpers::invalidFieldError("malformedRequest", jss::sub_index, "number"); + return ledger_entry_helpers::invalidFieldError( + "malformedRequest", jss::sub_index, "number"); } if (params.isMember(jss::owner) == params.isMember(jss::dir_root)) { - return LedgerEntryHelpers::malformedError( + return ledger_entry_helpers::malformedError( "malformedRequest", "Must have exactly one of `owner` and `dir_root` fields."); } @@ -390,27 +392,27 @@ parseDirectoryNode( if (params.isMember(jss::dir_root)) { - if (auto const uDirRoot = LedgerEntryHelpers::parse(params[jss::dir_root])) + if (auto const uDirRoot = ledger_entry_helpers::parse(params[jss::dir_root])) { return keylet::page(*uDirRoot, uSubIndex).key; } - return LedgerEntryHelpers::invalidFieldError("malformedDirRoot", jss::dir_root, "hash"); + return ledger_entry_helpers::invalidFieldError("malformedDirRoot", jss::dir_root, "hash"); } if (params.isMember(jss::owner)) { - auto const ownerID = LedgerEntryHelpers::parse(params[jss::owner]); + auto const ownerID = ledger_entry_helpers::parse(params[jss::owner]); if (!ownerID) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAddress", jss::owner, "AccountID"); } return keylet::page(keylet::ownerDir(*ownerID), uSubIndex).key; } - return LedgerEntryHelpers::malformedError("malformedRequest", ""); + return ledger_entry_helpers::malformedError("malformedRequest", ""); } static std::expected @@ -424,10 +426,10 @@ parseEscrow( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq"); if (!seq) return std::unexpected(seq.error()); @@ -449,7 +451,7 @@ parseFixed( } if (!params.asBool()) { - return LedgerEntryHelpers::invalidFieldError("invalidParams", fieldName, "true"); + return ledger_entry_helpers::invalidFieldError("invalidParams", fieldName, "true"); } return keylet.key; @@ -486,10 +488,10 @@ parseLoanBroker( return parseObjectID(params, fieldName, "hex string"); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq"); if (!seq) return std::unexpected(seq.error()); @@ -508,10 +510,10 @@ parseLoan( } auto const id = - LedgerEntryHelpers::requiredUInt256(params, jss::loan_broker_id, "malformedBroker"); + ledger_entry_helpers::requiredUInt256(params, jss::loan_broker_id, "malformedBroker"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::loan_seq, "malformedSeq"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::loan_seq, "malformedSeq"); if (!seq) return std::unexpected(seq.error()); @@ -529,13 +531,13 @@ parseMPToken( return parseObjectID(params, fieldName); } - auto const mptIssuanceID = - LedgerEntryHelpers::requiredUInt192(params, jss::mpt_issuance_id, "malformedMPTIssuanceID"); + auto const mptIssuanceID = ledger_entry_helpers::requiredUInt192( + params, jss::mpt_issuance_id, "malformedMPTIssuanceID"); if (!mptIssuanceID) return std::unexpected(mptIssuanceID.error()); auto const account = - LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAccount"); + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount"); if (!account) return std::unexpected(account.error()); @@ -548,10 +550,10 @@ parseMPTokenIssuance( json::StaticString const fieldName, [[maybe_unused]] unsigned const apiVersion) { - auto const mptIssuanceID = LedgerEntryHelpers::parse(params); + auto const mptIssuanceID = ledger_entry_helpers::parse(params); if (!mptIssuanceID) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedMPTokenIssuance", fieldName, "Hash192"); } @@ -589,11 +591,12 @@ parseOffer( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + auto const id = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -611,12 +614,13 @@ parseOracle( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAccount"); + auto const id = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount"); if (!id) return std::unexpected(id.error()); - auto const seq = - LedgerEntryHelpers::requiredUInt32(params, jss::oracle_document_id, "malformedDocumentID"); + auto const seq = ledger_entry_helpers::requiredUInt32( + params, jss::oracle_document_id, "malformedDocumentID"); if (!seq) return std::unexpected(seq.error()); @@ -645,16 +649,16 @@ parsePermissionedDomain( if (!pd.isObject()) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedRequest", fieldName, "hex string or object"); } auto const account = - LedgerEntryHelpers::requiredAccountID(pd, jss::account, "malformedAddress"); + ledger_entry_helpers::requiredAccountID(pd, jss::account, "malformedAddress"); if (!account) return std::unexpected(account.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(pd, jss::seq, "malformedRequest"); + auto const seq = ledger_entry_helpers::requiredUInt32(pd, jss::seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -675,7 +679,7 @@ parseRippleState( } if (auto const value = - LedgerEntryHelpers::hasRequired(jvRippleState, {jss::currency, jss::accounts}); + ledger_entry_helpers::hasRequired(jvRippleState, {jss::currency, jss::accounts}); !value) { return std::unexpected(value.error()); @@ -683,27 +687,27 @@ parseRippleState( if (!jvRippleState[jss::accounts].isArray() || jvRippleState[jss::accounts].size() != 2) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedRequest", jss::accounts, "length-2 array of Accounts"); } - auto const id1 = LedgerEntryHelpers::parse(jvRippleState[jss::accounts][0u]); - auto const id2 = LedgerEntryHelpers::parse(jvRippleState[jss::accounts][1u]); + auto const id1 = ledger_entry_helpers::parse(jvRippleState[jss::accounts][0u]); + auto const id2 = ledger_entry_helpers::parse(jvRippleState[jss::accounts][1u]); if (!id1 || !id2) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedAddress", jss::accounts, "array of Accounts"); } if (id1 == id2) { - return LedgerEntryHelpers::malformedError( + return ledger_entry_helpers::malformedError( "malformedRequest", "Cannot have a trustline to self."); } if (!jvRippleState[jss::currency].isString() || jvRippleState[jss::currency] == "" || !toCurrency(uCurrency, jvRippleState[jss::currency].asString())) { - return LedgerEntryHelpers::invalidFieldError( + return ledger_entry_helpers::invalidFieldError( "malformedCurrency", jss::currency, "Currency"); } @@ -729,12 +733,12 @@ parseSponsorship( return parseObjectID(params, fieldName); auto const sponsorID = - LedgerEntryHelpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); + ledger_entry_helpers::requiredAccountID(params, jss::sponsor, "malformedSponsor"); if (!sponsorID) return std::unexpected(sponsorID.error()); auto const sponseeID = - LedgerEntryHelpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); + ledger_entry_helpers::requiredAccountID(params, jss::sponsee, "malformedSponsee"); if (!sponseeID) return std::unexpected(sponseeID.error()); @@ -752,12 +756,13 @@ parseTicket( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::account, "malformedAddress"); + auto const id = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAddress"); if (!id) return std::unexpected(id.error()); auto const seq = - LedgerEntryHelpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest"); + ledger_entry_helpers::requiredUInt32(params, jss::ticket_seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -775,11 +780,11 @@ parseVault( return parseObjectID(params, fieldName); } - auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner"); + auto const id = ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); if (!id) return std::unexpected(id.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest"); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest"); if (!seq) return std::unexpected(seq.error()); @@ -797,11 +802,11 @@ parseXChainOwnedClaimID( return parseObjectID(claimId, fieldName); } - auto const bridgeSpec = LedgerEntryHelpers::parseBridgeFields(claimId); + auto const bridgeSpec = ledger_entry_helpers::parseBridgeFields(claimId); if (!bridgeSpec) return std::unexpected(bridgeSpec.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32( + auto const seq = ledger_entry_helpers::requiredUInt32( claimId, jss::xchain_owned_claim_id, "malformedXChainOwnedClaimID"); if (!seq) { @@ -823,11 +828,11 @@ parseXChainOwnedCreateAccountClaimID( return parseObjectID(claimId, fieldName); } - auto const bridgeSpec = LedgerEntryHelpers::parseBridgeFields(claimId); + auto const bridgeSpec = ledger_entry_helpers::parseBridgeFields(claimId); if (!bridgeSpec) return std::unexpected(bridgeSpec.error()); - auto const seq = LedgerEntryHelpers::requiredUInt32( + auto const seq = ledger_entry_helpers::requiredUInt32( claimId, jss::xchain_owned_create_account_claim_id, "malformedXChainOwnedCreateAccountClaimID"); @@ -853,7 +858,7 @@ struct LedgerEntry // ... // } json::Value -doLedgerEntry(RPC::JsonContext& context) +doLedgerEntry(rpc::JsonContext& context) { static auto kLedgerEntryParsers = std::to_array({ #pragma push_macro("LEDGER_ENTRY") @@ -892,11 +897,11 @@ doLedgerEntry(RPC::JsonContext& context) if (hasMoreThanOneMember) { - return RPC::makeParamError("Too many fields provided."); + return rpc::makeParamError("Too many fields provided."); } std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; @@ -936,7 +941,7 @@ doLedgerEntry(RPC::JsonContext& context) jvResult[jss::error] = "unknownOption"; return jvResult; } - return RPC::makeParamError("No ledger_entry params provided."); + return rpc::makeParamError("No ledger_entry params provided."); } } catch (json::Error const& e) @@ -945,7 +950,7 @@ doLedgerEntry(RPC::JsonContext& context) { // For apiVersion 2 onwards, any parsing failures that throw // this exception return an invalidParam error. - return RPC::makeError(RpcInvalidParams); + return rpc::makeError(RpcInvalidParams); } throw; @@ -956,7 +961,7 @@ doLedgerEntry(RPC::JsonContext& context) if (uNodeIndex.isZero()) { - RPC::injectError(RpcEntryNotFound, jvResult); + rpc::injectError(RpcEntryNotFound, jvResult); return jvResult; } @@ -969,13 +974,13 @@ doLedgerEntry(RPC::JsonContext& context) if (!sleNode) { // Not found. - RPC::injectError(RpcEntryNotFound, jvResult); + rpc::injectError(RpcEntryNotFound, jvResult); return jvResult; } if ((expectedType != ltANY) && (expectedType != sleNode->getType())) { - RPC::injectError(RpcUnexpectedLedgerType, jvResult); + rpc::injectError(RpcUnexpectedLedgerType, jvResult); return jvResult; } @@ -996,14 +1001,14 @@ doLedgerEntry(RPC::JsonContext& context) } std::pair -doLedgerEntryGrpc(RPC::GRPCContext& context) +doLedgerEntryGrpc(rpc::GRPCContext& context) { org::xrpl::rpc::v1::GetLedgerEntryRequest const& request = context.params; org::xrpl::rpc::v1::GetLedgerEntryResponse response; grpc::Status const status = grpc::Status::OK; std::shared_ptr ledger; - if (auto status = RPC::ledgerFromRequest(ledger, context)) + if (auto status = rpc::ledgerFromRequest(ledger, context)) { grpc::Status errorStatus; if (status.toErrorCode() == RpcInvalidParams) diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h index 57c4e58242..1b119db04e 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h @@ -22,7 +22,7 @@ #include #include -namespace xrpl::LedgerEntryHelpers { +namespace xrpl::ledger_entry_helpers { inline std::unexpected missingFieldError(json::StaticString const field, std::optional err = std::nullopt) @@ -30,7 +30,7 @@ missingFieldError(json::StaticString const field, std::optional err json::Value json = json::ValueType::Object; json[jss::error] = err.value_or("malformedRequest"); json[jss::error_code] = RpcInvalidParams; - json[jss::error_message] = RPC::missingFieldMessage(std::string(field.cStr())); + json[jss::error_message] = rpc::missingFieldMessage(std::string(field.cStr())); return std::unexpected(json); } @@ -40,7 +40,7 @@ invalidFieldError(std::string const& err, json::StaticString const field, std::s json::Value json = json::ValueType::Object; json[jss::error] = err; json[jss::error_code] = RpcInvalidParams; - json[jss::error_message] = RPC::expectedFieldMessage(field, type); + json[jss::error_message] = rpc::expectedFieldMessage(field, type); return std::unexpected(json); } @@ -291,4 +291,4 @@ parseBridgeFields(json::Value const& params) *lockingChainDoor, lockingChainIssue, *issuingChainDoor, issuingChainIssue); } -} // namespace xrpl::LedgerEntryHelpers +} // namespace xrpl::ledger_entry_helpers diff --git a/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp index e2cb80615b..ec3c9fe602 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp @@ -18,10 +18,10 @@ namespace xrpl { // ledger_index : // } json::Value -doLedgerHeader(RPC::JsonContext& context) +doLedgerHeader(rpc::JsonContext& context) { std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp index 7f54f81423..e95c51c483 100644 --- a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp @@ -61,13 +61,13 @@ toIso8601(NetClock::time_point tp) } json::Value -doAMMInfo(RPC::JsonContext& context) +doAMMInfo(rpc::JsonContext& context) { auto const& params(context.params); json::Value result; std::shared_ptr ledger; - result = RPC::lookupLedger(ledger, context); + result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -174,7 +174,7 @@ doAMMInfo(RPC::JsonContext& context) auto const r = getValuesFromContextParams(); if (!r) { - RPC::injectError(r.error(), result); + rpc::injectError(r.error(), result); return result; } diff --git a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp index abad196246..0f796df3a4 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp @@ -11,15 +11,15 @@ namespace xrpl { json::Value -doBookChanges(RPC::JsonContext& context) +doBookChanges(rpc::JsonContext& context) { std::shared_ptr ledger; - json::Value result = RPC::lookupLedger(ledger, context); + json::Value result = rpc::lookupLedger(ledger, context); if (ledger == nullptr) return result; - return RPC::computeBookChanges(ledger); + return rpc::computeBookChanges(ledger); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp index 63dee76f1b..ae539a59f3 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp @@ -32,19 +32,19 @@ validateTakerJSON(json::Value const& taker, json::StaticString const& name) { if (!taker.isMember(jss::currency) && !taker.isMember(jss::mpt_issuance_id)) { - return RPC::missingFieldError((boost::format("%s.currency") % name.cStr()).str()); + return rpc::missingFieldError((boost::format("%s.currency") % name.cStr()).str()); } if (taker.isMember(jss::mpt_issuance_id) && (taker.isMember(jss::currency) || taker.isMember(jss::issuer))) { - return RPC::invalidFieldError(name.cStr()); + return rpc::invalidFieldError(name.cStr()); } if ((taker.isMember(jss::currency) && !taker[jss::currency].isString()) || (taker.isMember(jss::mpt_issuance_id) && !taker[jss::mpt_issuance_id].isString())) { - return RPC::expectedFieldError( + return rpc::expectedFieldError( (boost::format("%s.currency") % name.cStr()).str(), "string"); } @@ -71,7 +71,7 @@ parseTakerAssetJSON( if (!toCurrency(issue.currency, taker[jss::currency].asString())) { JLOG(j.info()) << boost::format("Bad %s currency.") % name.cStr(); - return RPC::makeError( + return rpc::makeError( assetError, (boost::format("Invalid field '%s.currency', bad currency.") % name.cStr()).str()); } @@ -82,7 +82,7 @@ parseTakerAssetJSON( MPTID mptid; if (!mptid.parseHex(taker[jss::mpt_issuance_id].asString())) { - return RPC::makeError( + return rpc::makeError( assetError, (boost::format("Invalid field '%s.mpt_issuance_id'") % name.cStr()).str()); } @@ -113,20 +113,20 @@ parseTakerIssuerJSON( { if (!taker[jss::issuer].isString()) { - return RPC::expectedFieldError( + return rpc::expectedFieldError( (boost::format("%s.issuer") % name.cStr()).str(), "string"); } if (!toIssuer(issue.account, taker[jss::issuer].asString())) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format("Invalid field '%s.issuer', bad issuer.") % name.cStr()).str()); } if (issue.account == noAccount()) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format("Invalid field '%s.issuer', bad issuer account one.") % name.cStr()) @@ -140,7 +140,7 @@ parseTakerIssuerJSON( if (isXRP(issue.currency) && !isXRP(issue.account)) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format( "Unneeded field '%s.issuer' for XRP currency " @@ -151,7 +151,7 @@ parseTakerIssuerJSON( if (!isXRP(issue.currency) && isXRP(issue.account)) { - return RPC::makeError( + return rpc::makeError( issuerError, (boost::format("Invalid field '%s.issuer', expected non-XRP issuer.") % name.cStr()) .str()); @@ -162,7 +162,7 @@ parseTakerIssuerJSON( } json::Value -doBookOffers(RPC::JsonContext& context) +doBookOffers(rpc::JsonContext& context) { // VFALCO TODO Here is a terrible place for this kind of business // logic. It needs to be moved elsewhere and documented, @@ -171,25 +171,25 @@ doBookOffers(RPC::JsonContext& context) return rpcError(RpcTooBusy); std::shared_ptr lpLedger; - auto jvResult = RPC::lookupLedger(lpLedger, context); + auto jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; if (!context.params.isMember(jss::taker_pays)) - return RPC::missingFieldError(jss::taker_pays); + return rpc::missingFieldError(jss::taker_pays); if (!context.params.isMember(jss::taker_gets)) - return RPC::missingFieldError(jss::taker_gets); + return rpc::missingFieldError(jss::taker_gets); json::Value const& takerPays = context.params[jss::taker_pays]; json::Value const& takerGets = context.params[jss::taker_gets]; if (!takerPays.isObjectOrNull()) - return RPC::objectFieldError(jss::taker_pays); + return rpc::objectFieldError(jss::taker_pays); if (!takerGets.isObjectOrNull()) - return RPC::objectFieldError(jss::taker_gets); + return rpc::objectFieldError(jss::taker_gets); if (auto const err = validateTakerJSON(takerPays, jss::taker_pays)) return *err; @@ -215,11 +215,11 @@ doBookOffers(RPC::JsonContext& context) if (context.params.isMember(jss::taker)) { if (!context.params[jss::taker].isString()) - return RPC::expectedFieldError(jss::taker, "string"); + return rpc::expectedFieldError(jss::taker, "string"); takerID = parseBase58(context.params[jss::taker].asString()); if (!takerID) - return RPC::invalidFieldError(jss::taker); + return rpc::invalidFieldError(jss::taker); } std::optional domain; @@ -229,7 +229,7 @@ doBookOffers(RPC::JsonContext& context) if (!context.params[jss::domain].isString() || !num.parseHex(context.params[jss::domain].asString())) { - return RPC::makeError(RpcDomainMalformed, "Unable to parse domain."); + return rpc::makeError(RpcDomainMalformed, "Unable to parse domain."); } domain = num; @@ -238,11 +238,11 @@ doBookOffers(RPC::JsonContext& context) if (book.in == book.out) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; - return RPC::makeError(RpcBadMarket); + return rpc::makeError(RpcBadMarket); } unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kBookOffers, context)) + if (auto err = readLimitField(limit, rpc::tuning::kBookOffers, context)) return *err; bool const bProof(context.params.isMember(jss::proof)); @@ -260,7 +260,7 @@ doBookOffers(RPC::JsonContext& context) jvMarker, jvResult); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return jvResult; } diff --git a/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp index 343d539277..9d109fe16f 100644 --- a/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp +++ b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp @@ -32,17 +32,17 @@ namespace xrpl { // } json::Value -doDepositAuthorized(RPC::JsonContext& context) +doDepositAuthorized(rpc::JsonContext& context) { json::Value const& params = context.params; // Validate source_account. if (!params.isMember(jss::source_account)) - return RPC::missingFieldError(jss::source_account); + return rpc::missingFieldError(jss::source_account); if (!params[jss::source_account].isString()) { - return RPC::makeError( - RpcInvalidParams, RPC::expectedFieldMessage(jss::source_account, "a string")); + return rpc::makeError( + RpcInvalidParams, rpc::expectedFieldMessage(jss::source_account, "a string")); } auto srcID = parseBase58(params[jss::source_account].asString()); @@ -52,11 +52,11 @@ doDepositAuthorized(RPC::JsonContext& context) // Validate destination_account. if (!params.isMember(jss::destination_account)) - return RPC::missingFieldError(jss::destination_account); + return rpc::missingFieldError(jss::destination_account); if (!params[jss::destination_account].isString()) { - return RPC::makeError( - RpcInvalidParams, RPC::expectedFieldMessage(jss::destination_account, "a string")); + return rpc::makeError( + RpcInvalidParams, rpc::expectedFieldMessage(jss::destination_account, "a string")); } auto dstID = parseBase58(params[jss::destination_account].asString()); @@ -66,7 +66,7 @@ doDepositAuthorized(RPC::JsonContext& context) // Validate ledger. std::shared_ptr ledger; - json::Value result = RPC::lookupLedger(ledger, context); + json::Value result = rpc::lookupLedger(ledger, context); if (!ledger) return result; @@ -74,7 +74,7 @@ doDepositAuthorized(RPC::JsonContext& context) // If source account is not in the ledger it can't be authorized. if (!ledger->exists(keylet::account(srcAcct))) { - RPC::injectError(RpcSrcActNotFound, result); + rpc::injectError(RpcSrcActNotFound, result); return result; } @@ -82,7 +82,7 @@ doDepositAuthorized(RPC::JsonContext& context) auto const sleDest = ledger->read(keylet::account(dstAcct)); if (!sleDest) { - RPC::injectError(RpcDstActNotFound, result); + rpc::injectError(RpcDstActNotFound, result); return result; } @@ -96,15 +96,15 @@ doDepositAuthorized(RPC::JsonContext& context) auto const& creds(params[jss::credentials]); if (!creds.isArray() || !creds) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage( + rpc::expectedFieldMessage( jss::credentials, "is non-empty array of CredentialID(hash256)")); } if (creds.size() > kMaxCredentialsArraySize) { - return RPC::makeError( - RpcInvalidParams, RPC::expectedFieldMessage(jss::credentials, "array too long")); + return rpc::makeError( + RpcInvalidParams, rpc::expectedFieldMessage(jss::credentials, "array too long")); } lifeExtender.reserve(creds.size()); @@ -112,9 +112,9 @@ doDepositAuthorized(RPC::JsonContext& context) { if (!jo.isString()) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage( + rpc::expectedFieldMessage( jss::credentials, "an array of CredentialID(hash256)")); } @@ -122,34 +122,34 @@ doDepositAuthorized(RPC::JsonContext& context) auto const credS = jo.asString(); if (!credH.parseHex(credS)) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, - RPC::expectedFieldMessage( + rpc::expectedFieldMessage( jss::credentials, "an array of CredentialID(hash256)")); } SLE::const_pointer sleCred = ledger->read(keylet::credential(credH)); if (!sleCred) { - RPC::injectError(RpcBadCredentials, "credentials don't exist", result); + rpc::injectError(RpcBadCredentials, "credentials don't exist", result); return result; } if (!sleCred->isFlag(lsfAccepted)) { - RPC::injectError(RpcBadCredentials, "credentials aren't accepted", result); + rpc::injectError(RpcBadCredentials, "credentials aren't accepted", result); return result; } if (credentials::checkExpired(*sleCred, ledger->header().parentCloseTime)) { - RPC::injectError(RpcBadCredentials, "credentials are expired", result); + rpc::injectError(RpcBadCredentials, "credentials are expired", result); return result; } if ((*sleCred)[sfSubject] != srcAcct) { - RPC::injectError( + rpc::injectError( RpcBadCredentials, "credentials doesn't belong to the root account", result); return result; } @@ -157,7 +157,7 @@ doDepositAuthorized(RPC::JsonContext& context) auto [it, ins] = sorted.emplace((*sleCred)[sfIssuer], (*sleCred)[sfCredentialType]); if (!ins) { - RPC::injectError(RpcBadCredentials, "duplicates in credentials", result); + rpc::injectError(RpcBadCredentials, "duplicates in credentials", result); return result; } lifeExtender.push_back(std::move(sleCred)); diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp index 632456a3fa..f493000d0b 100644 --- a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp +++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp @@ -48,7 +48,7 @@ using Prices = bimap>, multiset_of const& f) { @@ -149,26 +149,26 @@ getStats(Prices::right_const_iterator const& begin, Prices::right_const_iterator * range - {most recent, most recent - time_threshold} [optional] */ json::Value -doGetAggregatePrice(RPC::JsonContext& context) +doGetAggregatePrice(rpc::JsonContext& context) { json::Value result; auto const& params(context.params); static constexpr std::uint16_t kMaxOracles = 200; if (!params.isMember(jss::oracles)) - return RPC::missingFieldError(jss::oracles); + return rpc::missingFieldError(jss::oracles); if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 || params[jss::oracles].size() > kMaxOracles) { - RPC::injectError(RpcOracleMalformed, result); + rpc::injectError(RpcOracleMalformed, result); return result; } if (!params.isMember(jss::base_asset)) - return RPC::missingFieldError(jss::base_asset); + return rpc::missingFieldError(jss::base_asset); if (!params.isMember(jss::quote_asset)) - return RPC::missingFieldError(jss::quote_asset); + return rpc::missingFieldError(jss::quote_asset); // Lambda to validate uint type // support positive int, uint, and a number represented as a string @@ -213,38 +213,38 @@ doGetAggregatePrice(RPC::JsonContext& context) auto const trim = getField(jss::trim); if (std::holds_alternative(trim)) { - RPC::injectError(std::get(trim), result); + rpc::injectError(std::get(trim), result); return result; } if (params.isMember(jss::trim) && (std::get(trim) == 0 || std::get(trim) > kMaxTrim)) { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } auto const timeThreshold = getField(jss::time_threshold, 0); if (std::holds_alternative(timeThreshold)) { - RPC::injectError(std::get(timeThreshold), result); + rpc::injectError(std::get(timeThreshold), result); return result; } auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset); if (std::holds_alternative(baseAsset)) { - RPC::injectError(std::get(baseAsset), result); + rpc::injectError(std::get(baseAsset), result); return result; } auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset); if (std::holds_alternative(quoteAsset)) { - RPC::injectError(std::get(quoteAsset), result); + rpc::injectError(std::get(quoteAsset), result); return result; } std::shared_ptr ledger; - result = RPC::lookupLedger(ledger, context); + result = rpc::lookupLedger(ledger, context); if (!ledger) return result; // LCOV_EXCL_LINE @@ -255,7 +255,7 @@ doGetAggregatePrice(RPC::JsonContext& context) { if (!oracle.isMember(jss::oracle_document_id) || !oracle.isMember(jss::account)) { - RPC::injectError(RpcOracleMalformed, result); + rpc::injectError(RpcOracleMalformed, result); return result; } auto const documentID = validUInt(oracle, jss::oracle_document_id) @@ -264,7 +264,7 @@ doGetAggregatePrice(RPC::JsonContext& context) auto const account = parseBase58(oracle[jss::account].asString()); if (!account || account->isZero() || !documentID) { - RPC::injectError(RpcInvalidParams, result); + rpc::injectError(RpcInvalidParams, result); return result; } @@ -298,7 +298,7 @@ doGetAggregatePrice(RPC::JsonContext& context) if (prices.empty()) { - RPC::injectError(RpcObjectNotFound, result); + rpc::injectError(RpcObjectNotFound, result); return result; } @@ -321,7 +321,7 @@ doGetAggregatePrice(RPC::JsonContext& context) if (prices.empty()) { // LCOV_EXCL_START - RPC::injectError(RpcInternal, result); + rpc::injectError(RpcInternal, result); return result; // LCOV_EXCL_STOP } diff --git a/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp index 88e4392dad..60b48ac5a6 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp @@ -10,15 +10,15 @@ namespace xrpl { json::Value -doNFTBuyOffers(RPC::JsonContext& context) +doNFTBuyOffers(rpc::JsonContext& context) { if (!context.params.isMember(jss::nft_id)) - return RPC::missingFieldError(jss::nft_id); + return rpc::missingFieldError(jss::nft_id); uint256 nftId; if (!nftId.parseHex(context.params[jss::nft_id].asString())) - return RPC::invalidFieldError(jss::nft_id); + return rpc::invalidFieldError(jss::nft_id); return enumerateNFTOffers(context, nftId, keylet::nftBuys(nftId)); } diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h index 70bc258d77..e03830ae0d 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h @@ -52,15 +52,15 @@ appendNftOfferJson(Application const& app, SLE::const_ref offer, json::Value& of // marker: opaque // optional, resume previous query // } inline json::Value -enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const& directory) +enumerateNFTOffers(rpc::JsonContext& context, uint256 const& nftId, Keylet const& directory) { unsigned int limit = 0; - if (auto err = readLimitField(limit, RPC::Tuning::kNftOffers, context)) + if (auto err = readLimitField(limit, rpc::tuning::kNftOffers, context)) return *err; std::shared_ptr ledger; - if (auto result = RPC::lookupLedger(ledger, context); !ledger) + if (auto result = rpc::lookupLedger(ledger, context); !ledger) return result; if (!ledger->exists(directory)) @@ -83,7 +83,7 @@ enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const json::Value const& marker(context.params[jss::marker]); if (!marker.isString()) - return RPC::expectedFieldError(jss::marker, "string"); + return rpc::expectedFieldError(jss::marker, "string"); if (!startAfter.parseHex(marker.asString())) return rpcError(RpcInvalidParams); @@ -127,7 +127,7 @@ enumerateNFTOffers(RPC::JsonContext& context, uint256 const& nftId, Keylet const for (auto const& offer : offers) appendNftOfferJson(context.app, offer, jsonOffers); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; return result; } diff --git a/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp index 309df93605..8b09b42a34 100644 --- a/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp @@ -10,15 +10,15 @@ namespace xrpl { json::Value -doNFTSellOffers(RPC::JsonContext& context) +doNFTSellOffers(rpc::JsonContext& context) { if (!context.params.isMember(jss::nft_id)) - return RPC::missingFieldError(jss::nft_id); + return rpc::missingFieldError(jss::nft_id); uint256 nftId; if (!nftId.parseHex(context.params[jss::nft_id].asString())) - return RPC::invalidFieldError(jss::nft_id); + return rpc::invalidFieldError(jss::nft_id); return enumerateNFTOffers(context, nftId, keylet::nftSells(nftId)); } diff --git a/src/xrpld/rpc/handlers/orderbook/PathFind.cpp b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp index 3a6b52ee98..c46238a5a5 100644 --- a/src/xrpld/rpc/handlers/orderbook/PathFind.cpp +++ b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp @@ -13,7 +13,7 @@ namespace xrpl { json::Value -doPathFind(RPC::JsonContext& context) +doPathFind(rpc::JsonContext& context) { if (context.app.config().pathSearchMax == 0) return rpcError(RpcNotSupported); @@ -34,7 +34,7 @@ doPathFind(RPC::JsonContext& context) if (sSubCommand == "create") { - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; context.infoSub->clearRequest(); return context.app.getPathRequestManager().makePathRequest( context.infoSub, lpLedger, context.params); diff --git a/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp index b7edfb6dbe..923cb0f7f5 100644 --- a/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp +++ b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp @@ -21,12 +21,12 @@ namespace xrpl { // This interface is deprecated. json::Value -doRipplePathFind(RPC::JsonContext& context) +doRipplePathFind(rpc::JsonContext& context) { if (context.app.config().pathSearchMax == 0) return rpcError(RpcNotSupported); - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; std::shared_ptr lpLedger; json::Value jvResult; @@ -37,7 +37,7 @@ doRipplePathFind(RPC::JsonContext& context) // No ledger specified, use pathfinding defaults // and dispatch to pathfinding engine if (context.app.getLedgerMaster().getValidatedLedgerAge() > - RPC::Tuning::kMaxValidatedLedgerAge) + rpc::tuning::kMaxValidatedLedgerAge) { if (context.apiVersion == 1) return rpcError(RpcNoNetwork); @@ -146,11 +146,11 @@ doRipplePathFind(RPC::JsonContext& context) } // The caller specified a ledger - jvResult = RPC::lookupLedger(lpLedger, context); + jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; - RPC::LegacyPathFind const lpf(isUnlimited(context.role), context.app); + rpc::LegacyPathFind const lpf(isUnlimited(context.role), context.app); if (!lpf.isOk()) return rpcError(RpcTooBusy); diff --git a/src/xrpld/rpc/handlers/server_info/Feature.cpp b/src/xrpld/rpc/handlers/server_info/Feature.cpp index bd7198b61c..1906658106 100644 --- a/src/xrpld/rpc/handlers/server_info/Feature.cpp +++ b/src/xrpld/rpc/handlers/server_info/Feature.cpp @@ -18,7 +18,7 @@ namespace xrpl { // vetoed : true/false // } json::Value -doFeature(RPC::JsonContext& context) +doFeature(rpc::JsonContext& context) { if (context.params.isMember(jss::feature)) { diff --git a/src/xrpld/rpc/handlers/server_info/Fee.cpp b/src/xrpld/rpc/handlers/server_info/Fee.cpp index 1fe5476d50..2e4147fc1e 100644 --- a/src/xrpld/rpc/handlers/server_info/Fee.cpp +++ b/src/xrpld/rpc/handlers/server_info/Fee.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doFee(RPC::JsonContext& context) +doFee(rpc::JsonContext& context) { auto result = context.app.getTxQ().doRPC(context.app); if (result.type() == json::ValueType::Object) @@ -16,7 +16,7 @@ doFee(RPC::JsonContext& context) // LCOV_EXCL_START UNREACHABLE("xrpl::doFee : invalid result type"); - RPC::injectError(RpcInternal, context.params); + rpc::injectError(RpcInternal, context.params); return context.params; // LCOV_EXCL_STOP } diff --git a/src/xrpld/rpc/handlers/server_info/Manifest.cpp b/src/xrpld/rpc/handlers/server_info/Manifest.cpp index cb1771750b..c0b29d8275 100644 --- a/src/xrpld/rpc/handlers/server_info/Manifest.cpp +++ b/src/xrpld/rpc/handlers/server_info/Manifest.cpp @@ -12,12 +12,12 @@ namespace xrpl { json::Value -doManifest(RPC::JsonContext& context) +doManifest(rpc::JsonContext& context) { auto& params = context.params; if (!params.isMember(jss::public_key)) - return RPC::missingFieldError(jss::public_key); + return rpc::missingFieldError(jss::public_key); auto const requested = params[jss::public_key].asString(); @@ -27,7 +27,7 @@ doManifest(RPC::JsonContext& context) auto const pk = parseBase58(TokenType::NodePublic, requested); if (!pk) { - RPC::injectError(RpcInvalidParams, ret); + rpc::injectError(RpcInvalidParams, ret); return ret; } diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index cce1b3e07f..b561ce6d38 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -382,7 +382,7 @@ getServerDefinitionsJson() } json::Value -doServerDefinitions(RPC::JsonContext& context) +doServerDefinitions(rpc::JsonContext& context) { auto& params = context.params; @@ -390,7 +390,7 @@ doServerDefinitions(RPC::JsonContext& context) if (params.isMember(jss::hash)) { if (!params[jss::hash].isString() || !hash.parseHex(params[jss::hash].asString())) - return RPC::invalidFieldError(jss::hash); + return rpc::invalidFieldError(jss::hash); } auto const& defs = detail::getDefinitions(); diff --git a/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp index aaad9d2b02..fd6e2f717f 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp @@ -9,7 +9,7 @@ namespace xrpl { json::Value -doServerInfo(RPC::JsonContext& context) +doServerInfo(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/server_info/ServerState.cpp b/src/xrpld/rpc/handlers/server_info/ServerState.cpp index acf4e9eb43..e0d43d4053 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerState.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerState.cpp @@ -8,7 +8,7 @@ namespace xrpl { json::Value -doServerState(RPC::JsonContext& context) +doServerState(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); diff --git a/src/xrpld/rpc/handlers/server_info/Version.h b/src/xrpld/rpc/handlers/server_info/Version.h index f25d8679ba..40ad4e5e71 100644 --- a/src/xrpld/rpc/handlers/server_info/Version.h +++ b/src/xrpld/rpc/handlers/server_info/Version.h @@ -9,7 +9,7 @@ #include #include -namespace xrpl::RPC { +namespace xrpl::rpc { class VersionHandler { @@ -34,9 +34,9 @@ public: // NOLINTBEGIN(readability-identifier-naming) static constexpr char const* name = "version"; - static constexpr unsigned minApiVer = RPC::kApiMinimumSupportedVersion; + static constexpr unsigned minApiVer = rpc::kApiMinimumSupportedVersion; - static constexpr unsigned maxApiVer = RPC::kApiMaximumValidVersion; + static constexpr unsigned maxApiVer = rpc::kApiMaximumValidVersion; static constexpr Role role = Role::USER; @@ -48,4 +48,4 @@ private: bool betaEnabled_; }; -} // namespace xrpl::RPC +} // namespace xrpl::rpc diff --git a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp index 93840bb6d6..7ce432c49e 100644 --- a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp @@ -27,7 +27,7 @@ namespace xrpl { json::Value -doSubscribe(RPC::JsonContext& context) +doSubscribe(rpc::JsonContext& context) { InfoSub::pointer ispSub; json::Value jvResult(json::ValueType::Object); @@ -79,7 +79,7 @@ doSubscribe(RPC::JsonContext& context) } catch (std::runtime_error const& ex) { - return RPC::makeParamError(ex.what()); + return rpc::makeParamError(ex.what()); } } else @@ -174,7 +174,7 @@ doSubscribe(RPC::JsonContext& context) if (!context.params[accountsProposed].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[accountsProposed]); + auto ids = rpc::parseAccountIds(context.params[accountsProposed]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.subAccount(ispSub, ids, true); @@ -185,7 +185,7 @@ doSubscribe(RPC::JsonContext& context) if (!context.params[jss::accounts].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[jss::accounts]); + auto ids = rpc::parseAccountIds(context.params[jss::accounts]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.subAccount(ispSub, ids, false); @@ -197,7 +197,7 @@ doSubscribe(RPC::JsonContext& context) if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; auto const& req = context.params[jss::account_history_tx_stream]; if (!req.isMember(jss::account) || !req[jss::account].isString()) return rpcError(RpcInvalidParams); @@ -230,11 +230,11 @@ doSubscribe(RPC::JsonContext& context) Book book; - if (auto const err = RPC::parseSubUnsubJson(book.in, j, jss::taker_pays, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.in, j, jss::taker_pays, context.j); err != RpcSuccess) return rpcError(err); - if (auto const err = RPC::parseSubUnsubJson(book.out, j, jss::taker_gets, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.out, j, jss::taker_gets, context.j); err != RpcSuccess) return rpcError(err); @@ -285,7 +285,7 @@ doSubscribe(RPC::JsonContext& context) if ((j.isMember(jss::snapshot) && j[jss::snapshot].asBool()) || (j.isMember(jss::state_now) && j[jss::state_now].asBool())) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; std::shared_ptr lpLedger = context.app.getLedgerMaster().getPublishedLedger(); if (lpLedger) @@ -299,7 +299,7 @@ doSubscribe(RPC::JsonContext& context) field == jss::asks ? reversed(book) : book, takerID ? *takerID : noAccount(), false, - RPC::Tuning::kBookOffers.rDefault, + rpc::tuning::kBookOffers.rDefault, jvMarker, jvOffers); diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp index af42af2a55..33c785a9bb 100644 --- a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp @@ -18,7 +18,7 @@ namespace xrpl { json::Value -doUnsubscribe(RPC::JsonContext& context) +doUnsubscribe(rpc::JsonContext& context) { InfoSub::pointer ispSub; json::Value jvResult(json::ValueType::Object); @@ -106,7 +106,7 @@ doUnsubscribe(RPC::JsonContext& context) if (!context.params[accountsProposed].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[accountsProposed]); + auto ids = rpc::parseAccountIds(context.params[accountsProposed]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.unsubAccount(ispSub, ids, true); @@ -117,7 +117,7 @@ doUnsubscribe(RPC::JsonContext& context) if (!context.params[jss::accounts].isArray()) return rpcError(RpcInvalidParams); - auto ids = RPC::parseAccountIds(context.params[jss::accounts]); + auto ids = rpc::parseAccountIds(context.params[jss::accounts]); if (ids.empty()) return rpcError(RpcActMalformed); context.netOps.unsubAccount(ispSub, ids, false); @@ -161,11 +161,11 @@ doUnsubscribe(RPC::JsonContext& context) Book book; - if (auto const err = RPC::parseSubUnsubJson(book.in, jv, jss::taker_pays, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.in, jv, jss::taker_pays, context.j); err != RpcSuccess) return rpcError(err); - if (auto const err = RPC::parseSubUnsubJson(book.out, jv, jss::taker_gets, context.j); + if (auto const err = rpc::parseSubUnsubJson(book.out, jv, jss::taker_gets, context.j); err != RpcSuccess) return rpcError(err); diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index 0f163c7356..8441add08b 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -44,7 +44,7 @@ namespace xrpl { static std::expected -getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context) +getAutofillSequence(json::Value const& txJson, rpc::JsonContext& context) { // autofill Sequence bool const hasTicketSeq = txJson.isMember(sfTicketSequence.jsonName); @@ -53,14 +53,14 @@ getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context) { // sanity check, should fail earlier // LCOV_EXCL_START - return std::unexpected(RPC::invalidFieldError("tx.Account")); + return std::unexpected(rpc::invalidFieldError("tx.Account")); // LCOV_EXCL_STOP } auto const srcAddressID = parseBase58(accountStr.asString()); if (!srcAddressID.has_value()) { return std::unexpected( - RPC::makeError(RpcSrcActMalformed, RPC::invalidFieldMessage("tx.Account"))); + rpc::makeError(RpcSrcActMalformed, rpc::invalidFieldMessage("tx.Account"))); } SLE::const_pointer const sle = context.app.getOpenLedger().current()->read(keylet::account(*srcAddressID)); @@ -88,7 +88,7 @@ autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") if (sigObject.isMember(jss::Signers)) { if (!sigObject[jss::Signers].isArray()) - return RPC::invalidFieldError(fieldPrefix + ".Signers"); + return rpc::invalidFieldError(fieldPrefix + ".Signers"); // check multisigned signers for (unsigned index = 0; index < sigObject[jss::Signers].size(); index++) { @@ -96,7 +96,7 @@ autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") if (!signer.isObject() || !signer.isMember(jss::Signer) || !signer[jss::Signer].isObject()) { - return RPC::invalidFieldError( + return rpc::invalidFieldError( fieldPrefix + ".Signers[" + std::to_string(index) + "]"); } @@ -133,7 +133,7 @@ autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") } static std::optional -autofillTx(json::Value& txJson, RPC::JsonContext& context) +autofillTx(json::Value& txJson, rpc::JsonContext& context) { if (auto error = autofillSignature(txJson)) return error; @@ -142,7 +142,7 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) { auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; if (!sponsorSignature.isObject()) - return RPC::objectFieldError(sfSponsorSignature.jsonName); + return rpc::objectFieldError(sfSponsorSignature.jsonName); if (auto const error = autofillSignature(sponsorSignature, "tx.SponsorSignature")) return error; @@ -167,7 +167,7 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) { // Autofill Fee after normalizing nested signer fields so the fee // estimator sees the full transaction shape. - auto feeOrError = RPC::getCurrentNetworkFee( + auto feeOrError = rpc::getCurrentNetworkFee( context.role, context.app.config(), context.app.getFeeTrack(), @@ -191,18 +191,18 @@ getTxJsonFromParams(json::Value const& params) { if (params.isMember(jss::tx_json)) { - return RPC::makeParamError("Can only include one of `tx_blob` and `tx_json`."); + return rpc::makeParamError("Can only include one of `tx_blob` and `tx_json`."); } auto const txBlob = params[jss::tx_blob]; if (!txBlob.isString()) { - return RPC::invalidFieldError(jss::tx_blob); + return rpc::invalidFieldError(jss::tx_blob); } auto unHexed = strUnHex(txBlob.asString()); if (!unHexed || unHexed->empty()) - return RPC::invalidFieldError(jss::tx_blob); + return rpc::invalidFieldError(jss::tx_blob); try { @@ -211,7 +211,7 @@ getTxJsonFromParams(json::Value const& params) } catch (std::runtime_error const&) { - return RPC::invalidFieldError(jss::tx_blob); + return rpc::invalidFieldError(jss::tx_blob); } } else if (params.isMember(jss::tx_json)) @@ -219,30 +219,30 @@ getTxJsonFromParams(json::Value const& params) txJson = params[jss::tx_json]; if (!txJson.isObject()) { - return RPC::objectFieldError(jss::tx_json); + return rpc::objectFieldError(jss::tx_json); } } else { - return RPC::makeParamError("Neither `tx_blob` nor `tx_json` included."); + return rpc::makeParamError("Neither `tx_blob` nor `tx_json` included."); } // basic sanity checks for transaction shape if (!txJson.isMember(jss::TransactionType)) { - return RPC::missingFieldError("tx.TransactionType"); + return rpc::missingFieldError("tx.TransactionType"); } if (!txJson.isMember(jss::Account)) { - return RPC::missingFieldError("tx.Account"); + return rpc::missingFieldError("tx.Account"); } return txJson; } static json::Value -simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) +simulateTxn(rpc::JsonContext& context, std::shared_ptr transaction) { json::Value jvResult; // Process the transaction @@ -290,11 +290,11 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::Values::None); - RPC::insertDeliveredAmount( + rpc::insertDeliveredAmount( jvResult[jss::meta], view, transaction->getSTransaction(), *result.metadata); - RPC::insertNFTSyntheticInJson( + rpc::insertNFTSyntheticInJson( jvResult, transaction->getSTransaction(), *result.metadata); - RPC::insertMPTokenIssuanceID( + rpc::insertMPTokenIssuanceID( jvResult[jss::meta], transaction->getSTransaction(), *result.metadata); } } @@ -317,23 +317,23 @@ simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) // binary: // } json::Value -doSimulate(RPC::JsonContext& context) +doSimulate(rpc::JsonContext& context) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; json::Value txJson; // the tx as a JSON // check validity of `binary` param if (context.params.isMember(jss::binary) && !context.params[jss::binary].isBool()) { - return RPC::invalidFieldError(jss::binary); + return rpc::invalidFieldError(jss::binary); } for (auto const field : {jss::secret, jss::seed, jss::seed_hex, jss::passphrase}) { if (context.params.isMember(field)) { - return RPC::invalidFieldError(field); + return rpc::invalidFieldError(field); } } @@ -365,13 +365,13 @@ doSimulate(RPC::JsonContext& context) if (stTx->getTxnType() == ttBATCH) { - return RPC::makeError(RpcNotImpl); + return rpc::makeError(RpcNotImpl); } // Reject transactions with the tfInnerBatchTxn flag. if (stTx->isFlag(tfInnerBatchTxn)) { - return RPC::makeError( + return rpc::makeError( RpcInvalidParams, "tfInnerBatchTxn flag is not allowed on top-level transactions."); } diff --git a/src/xrpld/rpc/handlers/transaction/Submit.cpp b/src/xrpld/rpc/handlers/transaction/Submit.cpp index 79f3680684..05a1552221 100644 --- a/src/xrpld/rpc/handlers/transaction/Submit.cpp +++ b/src/xrpld/rpc/handlers/transaction/Submit.cpp @@ -27,11 +27,11 @@ namespace xrpl { static std::expected -getFailHard(RPC::JsonContext const& context) +getFailHard(rpc::JsonContext const& context) { if (context.params.isMember(jss::fail_hard) && !context.params[jss::fail_hard].isBool()) { - return std::unexpected(RPC::expectedFieldError(jss::fail_hard, "boolean")); + return std::unexpected(rpc::expectedFieldError(jss::fail_hard, "boolean")); } return NetworkOPs::doFailHard( context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()); @@ -42,9 +42,9 @@ getFailHard(RPC::JsonContext const& context) // secret: // } json::Value -doSubmit(RPC::JsonContext& context) +doSubmit(rpc::JsonContext& context) { - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; if (!context.params.isMember(jss::tx_blob)) { @@ -53,16 +53,16 @@ doSubmit(RPC::JsonContext& context) return failType.error(); if (context.role != Role::ADMIN && !context.app.config().canSign()) - return RPC::makeError(RpcNotSupported, "Signing is not supported by this server."); + return rpc::makeError(RpcNotSupported, "Signing is not supported by this server."); - auto ret = RPC::transactionSubmit( + auto ret = rpc::transactionSubmit( context.params, context.apiVersion, *failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), context.app, - RPC::getProcessTxnFn(context.netOps)); + rpc::getProcessTxnFn(context.netOps)); ret[jss::deprecated] = "Signing support in the 'submit' command has been " diff --git a/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp index cc04ed073e..09301ca8a6 100644 --- a/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp +++ b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp @@ -13,20 +13,20 @@ namespace xrpl { // tx_json: , // } json::Value -doSubmitMultiSigned(RPC::JsonContext& context) +doSubmitMultiSigned(rpc::JsonContext& context) { - context.loadType = Resource::kFeeHeavyBurdenRpc; + context.loadType = resource::kFeeHeavyBurdenRpc; auto const failHard = context.params[jss::fail_hard].asBool(); auto const failType = NetworkOPs::doFailHard(failHard); - return RPC::transactionSubmitMultiSigned( + return rpc::transactionSubmitMultiSigned( context.params, context.apiVersion, failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), context.app, - RPC::getProcessTxnFn(context.netOps)); + rpc::getProcessTxnFn(context.netOps)); } } // namespace xrpl diff --git a/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp index bb68226334..2bd97e852f 100644 --- a/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp +++ b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp @@ -21,10 +21,10 @@ namespace xrpl { // XXX In this case, not specify either ledger does not mean ledger current. It // means any ledger. json::Value -doTransactionEntry(RPC::JsonContext& context) +doTransactionEntry(rpc::JsonContext& context) { std::shared_ptr lpLedger; - json::Value jvResult = RPC::lookupLedger(lpLedger, context); + json::Value jvResult = rpc::lookupLedger(lpLedger, context); if (!lpLedger) return jvResult; @@ -84,7 +84,7 @@ doTransactionEntry(RPC::JsonContext& context) jvResult[jss::tx_json] = sttx->getJson(JsonOptions::Values::None); } - RPC::insertDeliverMax(jvResult[jss::tx_json], sttx->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(jvResult[jss::tx_json], sttx->getTxnType(), context.apiVersion); auto const jsonMeta = (context.apiVersion > 1 ? jss::meta : jss::metadata); if (stobj) diff --git a/src/xrpld/rpc/handlers/transaction/Tx.cpp b/src/xrpld/rpc/handlers/transaction/Tx.cpp index c065bc268e..ee7110bf6b 100644 --- a/src/xrpld/rpc/handlers/transaction/Tx.cpp +++ b/src/xrpld/rpc/handlers/transaction/Tx.cpp @@ -68,8 +68,8 @@ struct TxArgs std::optional> ledgerRange; }; -std::pair -doTxHelp(RPC::Context& context, TxArgs args) +std::pair +doTxHelp(rpc::Context& context, TxArgs args) { TxResult result; @@ -169,7 +169,7 @@ doTxHelp(RPC::Context& context, TxArgs args) uint32_t const netID = context.app.getNetworkIDService().getNetworkID(); if (txnIdx <= 0xFFFFU && netID < 0xFFFFU && lgrSeq < 0x0FFF'FFFFUL) - result.ctid = RPC::encodeCTID(lgrSeq, txnIdx, netID); + result.ctid = rpc::encodeCTID(lgrSeq, txnIdx, netID); } } @@ -178,12 +178,12 @@ doTxHelp(RPC::Context& context, TxArgs args) json::Value populateJsonResponse( - std::pair const& res, + std::pair const& res, TxArgs const& args, - RPC::JsonContext const& context) + rpc::JsonContext const& context) { json::Value response; - RPC::Status const& error = res.second; + rpc::Status const& error = res.second; TxResult const& result = res.first; // handle errors if (error.toErrorCode() != RpcSuccess) @@ -215,7 +215,7 @@ populateJsonResponse( else { response[jss::tx_json] = result.txn->getJson(kOptionsJson); - RPC::insertDeliverMax( + rpc::insertDeliverMax( response[jss::tx_json], sttx->getTxnType(), context.apiVersion); } @@ -236,7 +236,7 @@ populateJsonResponse( { response = result.txn->getJson(JsonOptions::Values::IncludeDate, args.binary); if (!args.binary) - RPC::insertDeliverMax(response, sttx->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(response, sttx->getTxnType(), context.apiVersion); } // populate binary metadata @@ -254,8 +254,8 @@ populateJsonResponse( { response[jss::meta] = meta->getJson(JsonOptions::Values::None); insertDeliveredAmount(response[jss::meta], context, result.txn, *meta); - RPC::insertNFTSyntheticInJson(response, sttx, *meta); - RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); + rpc::insertNFTSyntheticInJson(response, sttx, *meta); + rpc::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); } } response[jss::validated] = result.validated; @@ -267,7 +267,7 @@ populateJsonResponse( } json::Value -doTxJson(RPC::JsonContext& context) +doTxJson(rpc::JsonContext& context) { if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); @@ -291,7 +291,7 @@ doTxJson(RPC::JsonContext& context) } else if (context.params.isMember(jss::ctid)) { - auto ctid = RPC::decodeCTID(context.params[jss::ctid].asString()); + auto ctid = rpc::decodeCTID(context.params[jss::ctid].asString()); if (!ctid) return rpcError(RpcInvalidParams); @@ -302,7 +302,7 @@ doTxJson(RPC::JsonContext& context) out << "Wrong network. You should submit this request to a node " "running on NetworkID: " << net_id; - return RPC::makeError(RpcWrongNetwork, out.str()); + return rpc::makeError(RpcWrongNetwork, out.str()); } args.ctid = {lgr_seq, txn_idx}; } @@ -327,7 +327,7 @@ doTxJson(RPC::JsonContext& context) } } - std::pair const res = doTxHelp(context, args); + std::pair const res = doTxHelp(context, args); return populateJsonResponse(res, args, context); } diff --git a/src/xrpld/rpc/handlers/transaction/TxHistory.cpp b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp index a45046773c..2d5ad8cbe5 100644 --- a/src/xrpld/rpc/handlers/transaction/TxHistory.cpp +++ b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp @@ -16,12 +16,12 @@ namespace xrpl { // start: // } json::Value -doTxHistory(RPC::JsonContext& context) +doTxHistory(rpc::JsonContext& context) { if (!context.app.config().useTxTables()) return rpcError(RpcNotEnabled); - context.loadType = Resource::kFeeMediumBurdenRpc; + context.loadType = resource::kFeeMediumBurdenRpc; if (!context.params.isMember(jss::start)) return rpcError(RpcInvalidParams); @@ -40,7 +40,7 @@ doTxHistory(RPC::JsonContext& context) for (auto const& t : trans) { json::Value txJson = t->getJson(JsonOptions::Values::None); - RPC::insertDeliverMax(txJson, t->getSTransaction()->getTxnType(), context.apiVersion); + rpc::insertDeliverMax(txJson, t->getSTransaction()->getTxnType(), context.apiVersion); txs.append(txJson); } diff --git a/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp index edc4eff057..8956603012 100644 --- a/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp +++ b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp @@ -7,7 +7,7 @@ namespace xrpl { json::Value -doTxReduceRelay(RPC::JsonContext& context) +doTxReduceRelay(rpc::JsonContext& context) { return context.app.getOverlay().txMetrics(); } diff --git a/src/xrpld/rpc/handlers/utility/Ping.cpp b/src/xrpld/rpc/handlers/utility/Ping.cpp index 0d34b9e0fc..68fb06456e 100644 --- a/src/xrpld/rpc/handlers/utility/Ping.cpp +++ b/src/xrpld/rpc/handlers/utility/Ping.cpp @@ -6,12 +6,12 @@ namespace xrpl { -namespace RPC { +namespace rpc { struct JsonContext; -} // namespace RPC +} // namespace rpc json::Value -doPing(RPC::JsonContext& context) +doPing(rpc::JsonContext& context) { json::Value ret(json::ValueType::Object); switch (context.role) diff --git a/src/xrpld/rpc/handlers/utility/Random.cpp b/src/xrpld/rpc/handlers/utility/Random.cpp index 56df442cf1..d3428ee6e9 100644 --- a/src/xrpld/rpc/handlers/utility/Random.cpp +++ b/src/xrpld/rpc/handlers/utility/Random.cpp @@ -10,16 +10,16 @@ namespace xrpl { -namespace RPC { +namespace rpc { struct JsonContext; -} // namespace RPC +} // namespace rpc // Result: // { // random: // } json::Value -doRandom(RPC::JsonContext& context) +doRandom(rpc::JsonContext& context) { // TODO(tom): the try/catch is almost certainly redundant, we catch at the // top level too. diff --git a/tests/conan/src/example.cpp b/tests/conan/src/example.cpp index acfb253a7d..4720af4384 100644 --- a/tests/conan/src/example.cpp +++ b/tests/conan/src/example.cpp @@ -5,6 +5,6 @@ int main(int argc, char const** argv) { - std::printf("%s\n", xrpl::BuildInfo::getVersionString().c_str()); + std::printf("%s\n", xrpl::build_info::getVersionString().c_str()); return 0; } From e0de716ee6e01f9542f24c8738de2edd9d3055b3 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:49:18 +0200 Subject: [PATCH 70/86] fix: Watch nix/*.nix files for direnv cache invalidation (#7948) --- .envrc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.envrc b/.envrc index 3550a30f2d..cecf4b4767 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ +watch_file nix/*.nix + use flake From c3ee602002e169521707246ea366d13640296e79 Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:43:59 +0200 Subject: [PATCH 71/86] test: Split Loan_test.cpp into topical suites (#7864) Co-authored-by: Ayaz Salikhov --- include/xrpl/beast/unit_test/suite.h | 14 + sanitizers/suppressions/ubsan.supp | 2 +- src/test/app/Loan_test.cpp | 9756 ----------------- .../app/{ => lending}/LendingHelpers_test.cpp | 0 .../app/{ => lending}/LoanBroker_test.cpp | 6 +- src/test/app/lending/LoanCashBasis_test.cpp | 1013 ++ .../app/lending/LoanCoverFreezeAuth_test.cpp | 722 ++ src/test/app/lending/LoanInvariants_test.cpp | 873 ++ src/test/app/lending/LoanLifecycle_test.cpp | 689 ++ src/test/app/lending/LoanMisc_test.cpp | 561 + src/test/app/lending/LoanPay_test.cpp | 760 ++ src/test/app/lending/LoanRounding_test.cpp | 993 ++ src/test/app/lending/LoanSecurity_test.cpp | 538 + src/test/app/lending/LoanSet_test.cpp | 607 + src/test/app/lending/LoanTestBase.h | 2949 +++++ src/test/app/lending/LoanValidation_test.cpp | 558 + src/test/app/lending/Loan_test.cpp | 46 + 17 files changed, 10327 insertions(+), 9760 deletions(-) delete mode 100644 src/test/app/Loan_test.cpp rename src/test/app/{ => lending}/LendingHelpers_test.cpp (100%) rename src/test/app/{ => lending}/LoanBroker_test.cpp (99%) create mode 100644 src/test/app/lending/LoanCashBasis_test.cpp create mode 100644 src/test/app/lending/LoanCoverFreezeAuth_test.cpp create mode 100644 src/test/app/lending/LoanInvariants_test.cpp create mode 100644 src/test/app/lending/LoanLifecycle_test.cpp create mode 100644 src/test/app/lending/LoanMisc_test.cpp create mode 100644 src/test/app/lending/LoanPay_test.cpp create mode 100644 src/test/app/lending/LoanRounding_test.cpp create mode 100644 src/test/app/lending/LoanSecurity_test.cpp create mode 100644 src/test/app/lending/LoanSet_test.cpp create mode 100644 src/test/app/lending/LoanTestBase.h create mode 100644 src/test/app/lending/LoanValidation_test.cpp create mode 100644 src/test/app/lending/Loan_test.cpp diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index c20fe2522c..e24904a87b 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -295,6 +295,20 @@ public: return runner_->arg(); } +protected: + /** + * Lets a suite compose other suites (e.g. an aggregator that reruns a + * group of related suites under its own name) via `SuiteInfo::run`. + * + * @return The runner this suite is executing under. + */ + Runner& + runner() const + { + return *runner_; + } + +public: /** * DEPRECATED * @return `true` if the test condition indicates success(a false value) diff --git a/sanitizers/suppressions/ubsan.supp b/sanitizers/suppressions/ubsan.supp index a67a4a0ca3..cb93a617aa 100644 --- a/sanitizers/suppressions/ubsan.supp +++ b/sanitizers/suppressions/ubsan.supp @@ -194,7 +194,7 @@ unsigned-integer-overflow:tests/libxrpl/basics/RangeSet.cpp unsigned-integer-overflow:test/app/Batch_test.cpp unsigned-integer-overflow:test/app/ConfidentialTransfer_test.cpp unsigned-integer-overflow:test/app/Invariants_test.cpp -unsigned-integer-overflow:test/app/Loan_test.cpp +unsigned-integer-overflow:test/app/lending/LoanSecurity_test.cpp unsigned-integer-overflow:test/app/NFToken_test.cpp unsigned-integer-overflow:test/app/OfferMPT_test.cpp unsigned-integer-overflow:test/app/Offer_test.cpp diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp deleted file mode 100644 index 977cdb443c..0000000000 --- a/src/test/app/Loan_test.cpp +++ /dev/null @@ -1,9756 +0,0 @@ -#include -// -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl::test { - -class Loan_test : public beast::unit_test::Suite -{ -protected: - // Ensure that all the features needed for Lending Protocol are included, - // even if they are set to unsupported. - // - // featureLendingProtocolV1_1 is excluded from the default set: it changes - // Vault/LoanBroker accounting (AssetsTotal/DebtTotal/LossUnrealized), and - // most of this file's tests assert whole-life-specific expected values - // for those fields. Tests that specifically exercise the amendment opt - // it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`). - FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1}; - std::string const iouCurrency_{"IOU"}; - - void - testDisabled() - { - testcase("Disabled"); - // Lending Protocol depends on Single Asset Vault (SAV). Test - // combinations of the two amendments. - // Single Asset Vault depends on MPTokensV1, but don't test every combo - // of that. - using namespace jtx; - auto failAll = [this](FeatureBitset features) { - Env env(*this, features); - - Account const alice{"alice"}; - Account const bob{"bob"}; - env.fund(XRP(10000), alice, bob); - - auto const keylet = keylet::loanBroker(alice, env.seq(alice)); - - using namespace std::chrono_literals; - using namespace loan; - - // counter party signature is optional on LoanSet. Confirm that by - // sending transaction without one. - auto setTx = env.jt(set(alice, keylet.key, Number(10000)), Ter(temDISABLED)); - env(setTx); - - // All loan transactions are disabled. - // 1. LoanSet - setTx = env.jt(setTx, Sig(sfCounterpartySignature, bob), Ter(temDISABLED)); - 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)); - // Other Loan transactions are disabled, too. - // 2. LoanDelete - env(del(alice, loanKeylet.key), Ter(temDISABLED)); - // 3. LoanManage - env(manage(alice, loanKeylet.key, tfLoanImpair), Ter(temDISABLED)); - // 4. LoanPay - env(pay(alice, loanKeylet.key, XRP(500)), Ter(temDISABLED)); - }; - failAll(all_ - featureMPTokensV1); - failAll(all_ - featureSingleAssetVault - featureLendingProtocol); - failAll(all_ - featureSingleAssetVault); - failAll(all_ - featureLendingProtocol); - } - - struct BrokerParameters - { - Number vaultDeposit = 1'000'000; - Number debtMax = 25'000; - TenthBips32 coverRateMin = percentageToTenthBips(10); - int coverDeposit = 1000; - TenthBips16 managementFeeRate{100}; - TenthBips32 coverRateLiquidation = percentageToTenthBips(25); - std::string data = {}; // NOLINT(readability-redundant-member-init) - std::uint32_t flags = 0; - // If set, the vault is created with this sfScale value. Useful for - // tests that need finer loanScale to exercise rounding edge cases. - std::optional vaultScale = - std::nullopt; // NOLINT(readability-redundant-member-init) - - [[nodiscard]] Number - maxCoveredLoanValue(Number const& currentDebt) const - { - NumberRoundModeGuard const mg(Number::RoundingMode::Downward); - auto debtLimit = coverDeposit * kTenthBipsPerUnity.value() / coverRateMin.value(); - - return debtLimit - currentDebt; - } - - static BrokerParameters const& - defaults() - { - static BrokerParameters const kResult{}; - return kResult; - } - - // TODO: create an operator() which returns a transaction similar to - // LoanParameters - }; - - struct BrokerInfo - { - jtx::PrettyAsset asset; - uint256 brokerID; - uint256 vaultID; - BrokerParameters params; - BrokerInfo( - jtx::PrettyAsset const& asset, - Keylet const& brokerKeylet, - Keylet const& vaultKeylet, - BrokerParameters p) - : asset(asset) - , brokerID(brokerKeylet.key) - , vaultID(vaultKeylet.key) - , params(std::move(p)) - { - } - - [[nodiscard]] Keylet - brokerKeylet() const - { - return keylet::loanBroker(brokerID); - } - [[nodiscard]] Keylet - vaultKeylet() const - { - return keylet::vault(vaultID); - } - - [[nodiscard]] int - vaultScale(jtx::Env const& env) const - { - using namespace jtx; - - auto const vaultSle = env.le(keylet::vault(vaultID)); - return getAssetsTotalScale(vaultSle); - } - }; - - struct LoanParameters - { - // The account submitting the transaction. May be borrower or broker. - jtx::Account account; - // The counterparty. Should be the other of borrower or broker. - jtx::Account counter; - // Whether the counterparty is specified in the `counterparty` field, or - // only signs. - bool counterpartyExplicit = true; - Number principalRequest; - // NOLINTBEGIN(readability-redundant-member-init) - std::optional setFee = std::nullopt; - std::optional originationFee = std::nullopt; - std::optional serviceFee = std::nullopt; - std::optional lateFee = std::nullopt; - std::optional closeFee = std::nullopt; - std::optional overFee = std::nullopt; - std::optional interest = std::nullopt; - std::optional lateInterest = std::nullopt; - std::optional closeInterest = std::nullopt; - std::optional overpaymentInterest = std::nullopt; - std::optional payTotal = std::nullopt; - std::optional payInterval = std::nullopt; - std::optional gracePd = std::nullopt; - std::optional flags = std::nullopt; - // NOLINTEND(readability-redundant-member-init) - - template - jtx::JTx - operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const - { - using namespace jtx; - using namespace jtx::loan; - - JTx jt{loan::set( - account, - broker.brokerID, - broker.asset(principalRequest).number(), - flags.value_or(0))}; - - Sig(sfCounterpartySignature, counter)(env, jt); - - Fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt); - - if (counterpartyExplicit) - kCounterparty(counter)(env, jt); - if (originationFee) - kLoanOriginationFee(broker.asset(*originationFee).number())(env, jt); - if (serviceFee) - kLoanServiceFee(broker.asset(*serviceFee).number())(env, jt); - if (lateFee) - kLatePaymentFee(broker.asset(*lateFee).number())(env, jt); - if (closeFee) - kClosePaymentFee(broker.asset(*closeFee).number())(env, jt); - if (overFee) - kOverpaymentFee (*overFee)(env, jt); - if (interest) - kInterestRate (*interest)(env, jt); - if (lateInterest) - kLateInterestRate (*lateInterest)(env, jt); - if (closeInterest) - kCloseInterestRate (*closeInterest)(env, jt); - if (overpaymentInterest) - kOverpaymentInterestRate (*overpaymentInterest)(env, jt); - if (payTotal) - kPaymentTotal (*payTotal)(env, jt); - if (payInterval) - kPaymentInterval (*payInterval)(env, jt); - if (gracePd) - kGracePeriod (*gracePd)(env, jt); - - return env.jt(jt, fN...); - } - }; - - struct PaymentParameters - { - Number overpaymentFactor = Number{1}; - std::optional overpaymentExtra = std::nullopt; - std::uint32_t flags = 0; - bool showStepBalances = false; - bool validateBalances = true; - - static PaymentParameters const& - defaults() - { - static PaymentParameters const kResult{}; - return kResult; - } - }; - - struct LoanState - { - std::uint32_t previousPaymentDate = 0; - NetClock::time_point startDate; - std::uint32_t nextPaymentDate = 0; - std::uint32_t paymentRemaining = 0; - std::int32_t const loanScale = 0; - Number totalValue = 0; - Number principalOutstanding = 0; - Number managementFeeOutstanding = 0; - Number periodicPayment = 0; - std::uint32_t flags = 0; - std::uint32_t const paymentInterval = 0; - TenthBips32 const interestRate{}; - }; - - /** - * Helper class to compare the expected state of a loan and loan broker - * against the data in the ledger. - */ - struct VerifyLoanStatus - { - public: - jtx::Env const& env; - BrokerInfo const& broker; - jtx::Account const& pseudoAccount; - Keylet const& loanKeylet; - - VerifyLoanStatus( - jtx::Env const& env, - BrokerInfo const& broker, - jtx::Account const& pseudo, - Keylet const& keylet) - : env(env), broker(broker), pseudoAccount(pseudo), loanKeylet(keylet) - { - } - - /** - * Checks the expected broker state against the ledger - */ - void - checkBroker( - Number const& principalOutstanding, - Number const& interestOwed, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - std::uint32_t ownerCount) const - { - using namespace jtx; - if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - env.test.BEAST_EXPECT(brokerSle)) - { - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const brokerDebt = brokerSle->at(sfDebtTotal); - - if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - env.test.BEAST_EXPECT(vaultSle)) - { - auto const expectedDebt = - env.current()->rules().enabled(featureLendingProtocolV1_1) && - getVaultVersion(vaultSle) == VaultVersion::CashBasis - ? principalOutstanding - : principalOutstanding + interestOwed; - env.test.BEAST_EXPECT(brokerDebt == expectedDebt); - env.test.BEAST_EXPECT( - env.balance(pseudoAccount, broker.asset).number() == - brokerSle->at(sfCoverAvailable)); - env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount); - - Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)}; - env.test.BEAST_EXPECT( - vaultSle->at(sfAssetsAvailable) == - env.balance(vaultPseudo, broker.asset).number()); - if (ownerCount == 0) - { - // The Vault must be perfectly balanced if there - // are no loans outstanding - auto const total = vaultSle->at(sfAssetsTotal); - auto const available = vaultSle->at(sfAssetsAvailable); - env.test.BEAST_EXPECT(total == available); - env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); - } - } - } - } - - void - checkPayment( - std::int32_t loanScale, - jtx::Account const& account, - jtx::PrettyAmount const& balanceBefore, - STAmount const& expectedPayment, - jtx::PrettyAmount const& adjustment) const - { - auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent()); - - STAmount const balanceChangeAmount{ - broker.asset, - roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)}; - { - auto const difference = roundToScale( - env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount), - borrowerScale); - env.test.expect( - roundToScale(difference, loanScale) >= beast::kZero, - "Balance before: " + to_string(balanceBefore.value()) + - ", expected change: " + to_string(balanceChangeAmount) + - ", difference (balance after - expected): " + to_string(difference), - __FILE__, - __LINE__); - } - } - - /** - * Checks both the loan and broker expect states against the ledger - */ - void - operator()( - std::uint32_t previousPaymentDate, - std::uint32_t nextPaymentDate, - std::uint32_t paymentRemaining, - Number const& loanScale, - Number const& totalValue, - Number const& principalOutstanding, - Number const& managementFeeOutstanding, - Number const& periodicPayment, - std::uint32_t flags) const - { - using namespace jtx; - if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) - { - env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate); - env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate); - env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); - env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue); - env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding); - env.test.BEAST_EXPECT( - loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding); - env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment); - env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); - - auto const ls = constructLoanState(loan); - - auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; - auto const paymentInterval = loan->at(sfPaymentInterval); - checkBroker( - principalOutstanding, - ls.interestDue, - interestRate, - paymentInterval, - paymentRemaining, - 1); - - if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - env.test.BEAST_EXPECT(brokerSle)) - { - if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - env.test.BEAST_EXPECT(vaultSle)) - { - if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u)) - { - env.test.BEAST_EXPECT( - vaultSle->at(sfLossUnrealized) == - (env.current()->rules().enabled(featureLendingProtocolV1_1) && - getVaultVersion(vaultSle) == VaultVersion::CashBasis - ? principalOutstanding - : totalValue - managementFeeOutstanding)); - } - else - { - env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); - } - } - } - } - } - - /** - * Checks both the loan and broker expect states against the ledger - */ - void - operator()(LoanState const& state) const - { - operator()( - state.previousPaymentDate, - state.nextPaymentDate, - state.paymentRemaining, - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - state.flags); - }; - }; - - BrokerInfo - createVaultAndBroker( - jtx::Env& env, - jtx::PrettyAsset const& asset, - jtx::Account const& lender, - BrokerParameters const& params = BrokerParameters::defaults()) - { - using namespace jtx; - - Vault const vault{env}; - - auto const deposit = asset(params.vaultDeposit); - auto const debtMaximumValue = asset(params.debtMax).value(); - auto const coverDepositValue = asset(params.coverDeposit).value(); - - auto const coverRateMinValue = params.coverRateMin; - - auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); - if (params.vaultScale) - tx[sfScale] = *params.vaultScale; - env(tx); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); - env.close(); - if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault)) - { - BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); - } - - auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender)); - - using namespace loan_broker; - env(set(lender, vaultKeylet.key, params.flags), - kData(params.data), - kManagementFeeRate(params.managementFeeRate), - kDebtMaximum(debtMaximumValue), - kCoverRateMinimum(coverRateMinValue), - kCoverRateLiquidation(TenthBips32(params.coverRateLiquidation))); - - if (coverDepositValue != beast::kZero) - env(coverDeposit(lender, keylet.key, coverDepositValue)); - - env.close(); - - return {asset, keylet, vaultKeylet, params}; - } - - /** - * Get the state without checking anything - */ - LoanState - getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet) - { - using d = NetClock::duration; - using tp = NetClock::time_point; - - // Lookup the current loan state - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - return LoanState{ - .previousPaymentDate = loan->at(sfPreviousPaymentDueDate), - .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), - .paymentRemaining = loan->at(sfPaymentRemaining), - .loanScale = loan->at(sfLoanScale), - .totalValue = loan->at(sfTotalValueOutstanding), - .principalOutstanding = loan->at(sfPrincipalOutstanding), - .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding), - .periodicPayment = loan->at(sfPeriodicPayment), - .flags = loan->at(sfFlags), - .paymentInterval = loan->at(sfPaymentInterval), - .interestRate = TenthBips32{loan->at(sfInterestRate)}, - }; - } - return LoanState{}; - } - - /** - * Get the state and check the values against the parameters used in - * `lifecycle` - */ - LoanState - getCurrentState( - jtx::Env const& env, - BrokerInfo const& broker, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) - { - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - auto const state = getCurrentState(env, broker, loanKeylet); - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); - BEAST_EXPECT( - state.loanScale >= - (broker.asset.integral() - ? 0 - : std::max(broker.vaultScale(env), state.principalOutstanding.exponent()))); - BEAST_EXPECT(state.paymentInterval == 600); - { - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - BEAST_EXPECT( - state.totalValue == - roundToAsset( - broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale)); - } - BEAST_EXPECT( - state.managementFeeOutstanding == - computeManagementFee( - broker.asset, - state.totalValue - state.principalOutstanding, - broker.params.managementFeeRate, - state.loanScale)); - - verifyLoanStatus(state); - - return state; - } - - bool - canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state) - { - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - BEAST_EXPECT(vaultSle)) - { - // log << vaultSle->getJson() << std::endl; - auto const assetsUnavailable = - vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); - auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + - (env.current()->rules().enabled(featureLendingProtocolV1_1) && - getVaultVersion(vaultSle) == VaultVersion::CashBasis - ? state.principalOutstanding - : state.totalValue - state.managementFeeOutstanding); - - if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable)) - { - return false; - } - } - } - return true; - } - - enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; - - // Specify the accounts as params to allow other accounts to be used - jtx::PrettyAsset - createAsset( - jtx::Env& env, - AssetType assetType, - BrokerParameters const& brokerParams, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - switch (assetType) - { - case AssetType::XRP: - // TODO: remove the factor, and set up loans in drops - return PrettyAsset{xrpIssue(), 1'000'000}; - - case AssetType::IOU: { - PrettyAsset const asset{issuer[iouCurrency_]}; - - auto const limit = - asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit)); - if (lender != issuer) - env(trust(lender, limit)); - if (borrower != issuer) - env(trust(borrower, limit)); - - return asset; - } - - case AssetType::MPT: { - // Enough to cover initial fees - if (!env.le(keylet::account(issuer))) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); - if (!env.le(keylet::account(lender))) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); - if (!env.le(keylet::account(borrower))) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - // Scale the MPT asset so interest is interesting - PrettyAsset const asset{mptt.issuanceID(), 10'000}; - // Need to do the authorization here because mptt isn't - // accessible outside - if (lender != issuer) - mptt.authorize({.account = lender}); - if (borrower != issuer) - mptt.authorize({.account = borrower}); - - env.close(); - - return asset; - } - - default: - throw std::runtime_error("Unknown asset type"); - } - } - - void - describeLoan( - jtx::Env& env, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - AssetType assetType, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); - auto const principal = asset(loanParams.principalRequest).number(); - auto const interest = loanParams.interest.value_or(TenthBips32{}); - auto const interval = loanParams.payInterval.value_or(LoanSet::kDefaultPaymentInterval); - auto const total = loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal); - auto const feeRate = brokerParams.managementFeeRate; - auto const props = computeLoanProperties( - env.current()->rules(), - asset, - principal, - interest, - interval, - total, - feeRate, - asset(brokerParams.vaultDeposit).number().exponent()); - log << "Loan properties:\n" - << "\tPrincipal: " << principal << std::endl - << "\tInterest rate: " << interest << std::endl - << "\tPayment interval: " << interval << std::endl - << "\tManagement Fee Rate: " << feeRate << std::endl - << "\tTotal Payments: " << total << std::endl - << "\tPeriodic Payment: " << props.periodicPayment << std::endl - << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl - << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl - << "\tLoan Scale: " << props.loanScale << std::endl - << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl; - - // checkGuards returns a TER, so success is 0 - BEAST_EXPECT(!checkLoanGuards( - asset, - asset(loanParams.principalRequest).number(), - loanParams.interest.value_or(TenthBips32{}) != beast::kZero, - loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal), - props, - env.journal)); - } - - std::optional> - createLoan( - jtx::Env& env, - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower) - { - using namespace jtx; - - // Enough to cover initial fees - env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); - if (lender != issuer) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); - if (borrower != issuer && borrower != lender) - env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); - - describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); - - // Make the asset - auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); - - env.close(); - if (asset.native() || lender != issuer) - { - env( - pay((asset.native() ? env.master : issuer), - lender, - asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); - } - // Fund the borrower later once we know the total loan - // size - - BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams); - - auto const pseudoAcctOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - if (!pseudoAcctOpt) - return std::nullopt; - Account const& pseudoAcct = *pseudoAcctOpt; - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // 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); - }(); - if (!loanKeyletOpt) - return std::nullopt; - Keylet const& loanKeylet = *loanKeyletOpt; - - env(loanParams(env, broker)); - - env.close(); - - return std::make_tuple(broker, loanKeylet, pseudoAcct); - } - - static void - topUpBorrower( - jtx::Env& env, - BrokerInfo const& broker, - jtx::Account const& issuer, - jtx::Account const& borrower, - LoanState const& state, - std::optional const& servFee) - { - using namespace jtx; - - STAmount const serviceFee = broker.asset(servFee.value_or(0)); - - // Ensure the borrower has enough funds to make the payments - // (including tx fees, if necessary) - auto const borrowerBalance = env.balance(borrower, broker.asset); - - auto const baseFee = env.current()->fees().base; - - // Add extra for transaction fees and reserves, if appropriate, or a - // tiny amount for the extra paid in each transaction - auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + - (broker.asset.native() ? Number( - baseFee * state.paymentRemaining + - accountReserve(*env.current(), borrower.id(), env.journal)) - : broker.asset(15).number()); - - auto const shortage = totalNeeded - borrowerBalance.number(); - - if (shortage > beast::kZero && (broker.asset.native() || issuer != borrower)) - { - env( - pay((broker.asset.native() ? env.master : issuer), - borrower, - STAmount{broker.asset, shortage})); - } - } - - void - makeLoanPayments( - jtx::Env& env, - BrokerInfo const& broker, - LoanParameters const& loanParams, - Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus, - jtx::Account const& issuer, - jtx::Account const& lender, - jtx::Account const& borrower, - PaymentParameters const& paymentParams = PaymentParameters::defaults()) - { - // Make all the individual payments - using namespace jtx; - using namespace jtx::loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - - bool const showStepBalances = paymentParams.showStepBalances; - - auto const currencyLabel = getCurrencyLabel(broker.asset); - - auto const baseFee = env.current()->fees().base; - - env.close(); - auto state = getCurrentState(env, broker, loanKeylet); - - verifyLoanStatus(state); - - STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0)); - - topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; - - if (!showStepBalances) - { - log << currencyLabel << " Payment components: " - << "Payments remaining, " - << "rawInterest, rawPrincipal, " - "rawMFee, " - << "trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special" - << std::endl; - } - - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward); - - auto currentRoundedState = constructLoanState( - state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); - { - auto const raw = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - if (showStepBalances) - { - log << currencyLabel << " Starting loan balances: " - << "\n\tTotal value: " << currentRoundedState.valueOutstanding - << "\n\tPrincipal: " << currentRoundedState.principalOutstanding - << "\n\tInterest: " << currentRoundedState.interestDue - << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue - << "\n\tPayments remaining " << state.paymentRemaining << std::endl; - } - else - { - log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", " - << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", " - << currentRoundedState.principalOutstanding << ", " - << currentRoundedState.interestDue << ", " - << currentRoundedState.managementFeeDue << std::endl; - } - } - - // Try to pay a little extra to show that it's _not_ - // taken - auto const extraAmount = paymentParams.overpaymentExtra - ? broker.asset(*paymentParams.overpaymentExtra).value() - : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20}); - - STAmount const transactionAmount = - STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount; - - auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number(); - auto const initialState = state; - xrpl::detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - Number totalFeesPaid = 0; - std::size_t totalPaymentsMade = 0; - - xrpl::LoanState currentTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - auto validateBorrowerBalance = [&]() { - if (borrower == issuer || !paymentParams.validateBalances) - return; - auto const totalSpent = - (totalPaid.trackedValueDelta + totalFeesPaid + - (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : kNumZero)); - BEAST_EXPECT( - env.balance(borrower, broker.asset).number() == - borrowerInitialBalance - totalSpent); - }; - - auto const defaultRound = broker.asset.integral() ? 3 : 0; - auto truncate = [defaultRound](Number const& n, std::optional places = std::nullopt) { - auto const p = places.value_or(defaultRound); - if (p == 0) - return n; - auto const factor = Number{1, p}; - return (n * factor).truncate() / factor; - }; - while (state.paymentRemaining > 0) - { - validateBorrowerBalance(); - // Compute the expected principal amount - auto const paymentComponents = xrpl::detail::computePaymentComponents( - env.current()->rules(), - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta <= roundedPeriodicPayment || - (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final && - paymentComponents.trackedValueDelta >= roundedPeriodicPayment)); - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - - xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; - BEAST_EXPECT( - deltas.total() == deltas.principal + deltas.interest + deltas.managementFee); - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - deltas.total() == state.periodicPayment || - (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14); - - if (!showStepBalances) - { - log << currencyLabel << " Payment components: " << state.paymentRemaining << ", " - - << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee - << ", " << paymentComponents.trackedValueDelta << ", " - << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* { - if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Final) - return "final"; - if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Extra) - return "extra"; - return "none"; - }() << std::endl; - } - - auto const totalDueAmount = - STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee}; - - if (paymentParams.validateBalances) - { - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be dust. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - diff == beast::kZero || - (diff > beast::kZero && - ((broker.asset.integral() && (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::kZero && - paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedPrincipalDelta == state.principalOutstanding); - } - - auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags)); - - env.close(d{state.paymentInterval / 2}); - - if (paymentParams.validateBalances) - { - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - } - - if (showStepBalances) - { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - { - // No reason for this not to exist - return; - } - auto const current = constructLoanState(loanSle); - auto const errors = nextTrueState - current; - log << currencyLabel << " Loan balances: " - << "\n\tAmount taken: " << paymentComponents.trackedValueDelta - << "\n\tTotal value: " << current.valueOutstanding - << " (true: " << truncate(nextTrueState.valueOutstanding) - << ", error: " << truncate(errors.total()) - << ")\n\tPrincipal: " << current.principalOutstanding - << " (true: " << truncate(nextTrueState.principalOutstanding) - << ", error: " << truncate(errors.principal) - << ")\n\tInterest: " << current.interestDue - << " (true: " << truncate(nextTrueState.interestDue) - << ", error: " << truncate(errors.interest) - << ")\n\tMgmt fee: " << current.managementFeeDue - << " (true: " << truncate(nextTrueState.managementFeeDue) - << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining " - << loanSle->at(sfPaymentRemaining) << std::endl; - - currentRoundedState = current; - } - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) - { - state.paymentRemaining = 0; - state.nextPaymentDate = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - if (paymentParams.validateBalances) - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += paymentComponents.trackedInterestPart(); - totalFeesPaid += serviceFee; - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - validateBorrowerBalance(); - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - auto const initialInterestDue = initialState.totalValue - - (initialState.principalOutstanding + initialState.managementFeeOutstanding); - if (paymentParams.validateBalances) - { - // Make sure all the payments add up - BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT(totalInterestPaid == initialInterestDue); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); - } - - if (showStepBalances) - { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - { - // No reason for this not to exist - return; - } - log << currencyLabel << " Total amounts paid: " - << "\n\tTotal value: " << totalPaid.trackedValueDelta - << " (initial: " << truncate(initialState.totalValue) - << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta) - << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta - << " (initial: " << truncate(initialState.principalOutstanding) << ", error: " - << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta) - << ")\n\tInterest: " << totalInterestPaid - << " (initial: " << truncate(initialInterestDue) - << ", error: " << truncate(initialInterestDue - totalInterestPaid) - << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta - << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: " - << truncate( - initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta) - << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl; - } - } - - void - runLoan( - AssetType assetType, - BrokerParameters const& brokerParams, - LoanParameters const& loanParams, - FeatureBitset features) - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - Env env(*this, features); - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - } - - /** - * Runs through the complete lifecycle of a loan - * - * 1. Create a loan. - * 2. Test a bunch of transaction failure conditions. - * 3. Use the `toEndOfLife` callback to take the loan to 0. How that is done - * depends on the callback. e.g. Default, Early payoff, make all the - * normal payments, etc. - * 4. Delete the loan. The loan will alternate between being deleted by the - * lender and the borrower. - */ - void - lifecycle( - std::string const& caseLabel, - char const* label, - jtx::Env& env, - Number const& loanAmount, - int interestExponent, - jtx::Account const& lender, - jtx::Account const& borrower, - jtx::Account const& evan, - BrokerInfo const& broker, - jtx::Account const& pseudoAcct, - std::uint32_t flags, - // The end of life callback is expected to take the loan to 0 payments - // remaining, one way or another - std::function - toEndOfLife) - { - auto const [keylet, loanSequence] = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - { - // will be invalid - return std::make_pair(keylet::loan(broker.brokerID), std::uint32_t(0)); - } - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // 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); - }(); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); - - // No loans yet - verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); - - if (!BEAST_EXPECT(loanSequence != 0)) - return; - - testcase << caseLabel << " " << label; - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - auto applyExponent = [interestExponent, this](TenthBips32 value) mutable { - BEAST_EXPECT(value > TenthBips32(0)); - while (interestExponent > 0) - { - auto const oldValue = value; - value *= 10; - --interestExponent; - BEAST_EXPECT(value / 10 == oldValue); - } - while (interestExponent < 0) - { - auto const oldValue = value; - value /= 10; - ++interestExponent; - BEAST_EXPECT(value * 10 == oldValue); - } - return value; - }; - - auto const borrowerOwnerCount = env.ownerCount(borrower); - - auto const loanSetFee = env.current()->fees().base * 2; - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .counterpartyExplicit = false, - .principalRequest = loanAmount, - .setFee = loanSetFee, - .originationFee = 1, - .serviceFee = 2, - .lateFee = 3, - .closeFee = 4, - .overFee = applyExponent(percentageToTenthBips(5) / 10), - .interest = applyExponent(percentageToTenthBips(12)), - // 2.4% - .lateInterest = applyExponent(percentageToTenthBips(24) / 10), - .closeInterest = applyExponent(percentageToTenthBips(36) / 10), - .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10), - .payTotal = 12, - .payInterval = 600, - .gracePd = 60, - .flags = flags, - }; - Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value(); - auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value(); - auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value(); - auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value(); - auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value(); - - auto const borrowerStartbalance = env.balance(borrower, broker.asset); - - auto createJtx = loanParams(env, broker); - // Successfully create a Loan - env(createJtx); - - env.close(); - - auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count(); - - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); - } - - { - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = 2 * env.current()->fees().base; - } - - BEAST_EXPECT( - env.balance(borrower, broker.asset).value() == - borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount - - adjustment.value()); - } - - auto const loanFlags = - createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0); - - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - // log << "loan after create: " << to_string(loan->getJson()) - // << std::endl; - BEAST_EXPECT( - loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment)); - BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); - BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); - BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); - BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount); - BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount); - BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount); - BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount); - BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee); - BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest); - BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest); - BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest); - BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest); - BEAST_EXPECT(loan->at(sfStartDate) == startDate); - BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval); - BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); - BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0); - BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval); - BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); - BEAST_EXPECT( - loan->at(sfLoanScale) >= - (broker.asset.integral() - ? 0 - : std::max(broker.vaultScale(env), principalRequestAmount.exponent()))); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount); - } - - auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); - - auto const loanProperties = computeLoanProperties( - env.current()->rules(), - broker.asset.raw(), - state.principalOutstanding, - state.interestRate, - state.paymentInterval, - state.paymentRemaining, - broker.params.managementFeeRate, - state.loanScale); - - verifyLoanStatus( - 0, - startDate + *loanParams.payInterval, - *loanParams.payTotal, - state.loanScale, - loanProperties.loanState.valueOutstanding, - principalRequestAmount, - loanProperties.loanState.managementFeeDue, - loanProperties.periodicPayment, - loanFlags | 0); - - // Manage the loan - // no-op - env(manage(lender, keylet.key, 0)); - { - // no flags - auto jt = manage(lender, keylet.key, 0); - jt.removeMember(sfFlags.getName()); - env(jt); - } - // Only the lender can manage - env(manage(evan, keylet.key, 0), Ter(tecNO_PERMISSION)); - // unknown flags - env(manage(lender, keylet.key, tfLoanManageMask), Ter(temINVALID_FLAG)); - // combinations of flags are not allowed - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), Ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), Ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), Ter(temINVALID_FLAG)); - env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault), - Ter(temINVALID_FLAG)); - // invalid loan ID - env(manage(lender, broker.brokerID, tfLoanImpair), Ter(tecNO_ENTRY)); - // Loan is unimpaired, can't unimpair it again - env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); - // Loan is unimpaired, it can go into default, but only after it's past - // due - env(manage(lender, keylet.key, tfLoanDefault), Ter(tecTOO_SOON)); - - // Check the vault - bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, keylet.key, tfLoanImpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); - // Unimpair the loan - env(manage(lender, keylet.key, tfLoanUnimpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION)); - - auto const nextDueDate = startDate + *loanParams.payInterval; - - env.close(); - - verifyLoanStatus( - 0, - nextDueDate, - *loanParams.payTotal, - loanProperties.loanScale, - loanProperties.loanState.valueOutstanding, - principalRequestAmount, - loanProperties.loanState.managementFeeDue, - loanProperties.periodicPayment, - loanFlags | 0); - - // Can't delete the loan yet. It has payments remaining. - env(del(lender, keylet.key), Ter(tecHAS_OBLIGATIONS)); - - if (BEAST_EXPECT(toEndOfLife)) - toEndOfLife(keylet, verifyLoanStatus); - env.close(); - - // Verify the loan is at EOL - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); - } - auto const borrowerStartingBalance = env.balance(borrower, broker.asset); - - // Try to delete the loan broker with an active loan - env(loan_broker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS)); - // Ensure the above tx doesn't get ordered after the LoanDelete and - // delete our broker! - env.close(); - - // Test failure cases - env(del(lender, keylet.key, tfLoanOverpayment), Ter(temINVALID_FLAG)); - env(del(evan, keylet.key), Ter(tecNO_PERMISSION)); - env(del(lender, broker.brokerID), Ter(tecNO_ENTRY)); - - // Delete the loan - // Either the borrower or the lender can delete the loan. Alternate - // between who does it across tests. - static unsigned kDeleteCounter = 0; - auto const deleter = ((++kDeleteCounter % 2) != 0u) ? lender : borrower; - env(del(deleter, keylet.key)); - env.close(); - - PrettyAmount adjustment = broker.asset(0); - if (deleter == borrower) - { - // Need to account for fees if the loan is in XRP - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - } - - // No loans left - verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0); - - BEAST_EXPECT( - env.balance(borrower, broker.asset).value() == - borrowerStartingBalance.value() - adjustment); - BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); - - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - } - } - - static std::string - getCurrencyLabel(Asset const& asset) - { - if (asset.native()) - return "XRP"; - if (asset.holds()) - return "IOU"; - if (asset.holds()) - return "MPT"; - return "Unknown"; - } - - /** - * Wrapper to run a series of lifecycle tests for a given asset and loan - * amount - * - * Will be used in the future to vary the loan parameters. For now, it is - * only called once. - * - * Tests a bunch of LoanSet failure conditions before lifecycle. - */ - template - void - testCaseWrapper( - jtx::Env& env, - jtx::MPTTester& mptt, - std::array const& assets, - BrokerInfo const& broker, - Number const& loanAmount, - int interestExponent) - { - using namespace jtx; - using namespace lending; - - auto const& asset = broker.asset.raw(); - auto const currencyLabel = getCurrencyLabel(asset); - auto const caseLabel = [&]() { - std::stringstream ss; - ss << "Lifecycle: " << loanAmount << " " << currencyLabel - << " Scale interest to: " << interestExponent << " "; - return ss.str(); - }(); - testcase << caseLabel; - - using namespace loan; - using namespace std::chrono_literals; - using d = NetClock::duration; - using tp = NetClock::time_point; - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - // Evan will attempt to be naughty - Account const evan{"evan"}; - // Do not fund alice - Account const alice{"alice"}; - - Number const principalRequest = broker.asset(loanAmount).value(); - Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0); - BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10); - Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value(); - Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value(); - Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value(); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - auto const pseudoAcct = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return Account{lender}; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - auto const baseFee = env.current()->fees().base; - - auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); - // Try some failure cases - // flags are checked first - env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(temINVALID_FLAG)); - - // field length validation - // sfData: good length, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kData(std::string(kMaxDataPayloadLength, 'X')), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfData: too long - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kData(std::string(kMaxDataPayloadLength + 1, 'Y')), - loanSetFee, - Ter(temINVALID)); - - // field range validation - // sfOverpaymentFee: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kOverpaymentFee(kMaxOverpaymentFee), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfOverpaymentFee: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kOverpaymentFee(kMaxOverpaymentFee + 1), - loanSetFee, - Ter(temINVALID)); - - // sfInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kInterestRate(kMaxInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kInterestRate(kMaxInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - // sfInterestRate: too small - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfLateInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kLateInterestRate(kMaxLateInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kLateInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfLateInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kLateInterestRate(kMaxLateInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - // sfLateInterestRate: too small - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kLateInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfCloseInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kCloseInterestRate(kMaxCloseInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kCloseInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfCloseInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kCloseInterestRate(kMaxCloseInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kCloseInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfOverpaymentInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kOverpaymentInterestRate(kMaxOverpaymentInterestRate), - loanSetFee, - Ter(tefBAD_AUTH)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kOverpaymentInterestRate(TenthBips32(0)), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfOverpaymentInterestRate: too big - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kOverpaymentInterestRate(kMaxOverpaymentInterestRate + 1), - loanSetFee, - Ter(temINVALID)); - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kOverpaymentInterestRate(TenthBips32(-1)), - loanSetFee, - Ter(temINVALID)); - - // sfPaymentTotal: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kPaymentTotal(LoanSet::kMinPaymentTotal), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfPaymentTotal: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kPaymentTotal(LoanSet::kMinPaymentTotal - 1), - loanSetFee, - Ter(temINVALID)); - - // sfPaymentInterval: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kPaymentInterval(LoanSet::kMinPaymentInterval), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfPaymentInterval: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kPaymentInterval(LoanSet::kMinPaymentInterval - 1), - loanSetFee, - Ter(temINVALID)); - - // sfGracePeriod: good value, bad account - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, borrower), - kPaymentInterval(LoanSet::kMinPaymentInterval * 2), - kGracePeriod(LoanSet::kMinPaymentInterval * 2), - loanSetFee, - Ter(tefBAD_AUTH)); - // sfGracePeriod: larger than paymentInterval - env(set(evan, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - kPaymentInterval(LoanSet::kMinPaymentInterval * 2), - kGracePeriod(LoanSet::kMinPaymentInterval * 3), - loanSetFee, - Ter(temINVALID)); - - // insufficient fee - single sign - env(set(borrower, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - Ter(telINSUF_FEE_P)); - // insufficient fee - multisign - env(signers(lender, 2, {{evan, 1}, {borrower, 1}})); - env(signers(borrower, 2, {{evan, 1}, {lender, 1}})); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(evan, lender), - Msig(sfCounterpartySignature, evan, borrower), - Fee(env.current()->fees().base * 5 - 1), - Ter(telINSUF_FEE_P)); - // Bad multisign signatures for borrower (Account) - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(alice, issuer), - Msig(sfCounterpartySignature, evan, borrower), - Fee(env.current()->fees().base * 5), - Ter(tefBAD_SIGNATURE)); - // Bad multisign signatures for issuer (Counterparty) - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(evan, lender), - Msig(sfCounterpartySignature, alice, issuer), - Fee(env.current()->fees().base * 5 - 1), - Ter(tefBAD_SIGNATURE)); - env(signers(lender, kNone)); - env(signers(borrower, kNone)); - // multisign sufficient fee, but no signers set up - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(evan, lender), - Msig(sfCounterpartySignature, evan, borrower), - Fee(env.current()->fees().base * 5), - Ter(tefNOT_MULTI_SIGNING)); - // not the broker owner, no counterparty, not signed by broker - // owner - env(set(borrower, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, evan), - loanSetFee, - Ter(tefBAD_AUTH)); - // not the broker owner, counterparty is borrower - env(set(evan, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - loanSetFee, - Ter(tecNO_PERMISSION)); - // not a LoanBroker object, no counterparty - env(set(lender, badKeylet.key, principalRequest), - Sig(sfCounterpartySignature, evan), - loanSetFee, - Ter(temBAD_SIGNER)); - // not a LoanBroker object, counterparty is valid - env(set(lender, badKeylet.key, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - loanSetFee, - Ter(tecNO_ENTRY)); - // borrower doesn't exist - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(alice), - Sig(sfCounterpartySignature, alice), - loanSetFee, - Ter(terNO_ACCOUNT)); - - // Request more funds than the vault has available - env(set(evan, broker.brokerID, totalVaultRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecINSUFFICIENT_FUNDS)); - - // Request more funds than the broker's first-loss capital can - // cover. - env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecINSUFFICIENT_FUNDS)); - - // Frozen trust line / locked MPT issuance - // XRP can not be frozen, but run through the loop anyway to test - // the tecLIMIT_EXCEEDED case - { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return; - - auto const vaultPseudo = [&]() { - auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); - if (!BEAST_EXPECT(vaultSle)) - { - // This will be wrong, but the test has failed anyway. - return Account{lender}; - } - auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount)); - return vaultPseudo; - }(); - - auto const [freeze, deepfreeze, unfreeze, expectedResult] = - [&]() -> std::tuple< - std::function, - std::function, - std::function, - TER> { - // Freeze / lock the asset - std::function const empty; - if (broker.asset.native()) - { - // XRP can't be frozen - return std::make_tuple(empty, empty, empty, tesSUCCESS); - } - if (broker.asset.holds()) - { - auto freeze = [&](Account const& holder) { - env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze)); - }; - auto deepfreeze = [&](Account const& holder) { - env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze)); - }; - auto unfreeze = [&](Account const& holder) { - env(trust( - issuer, holder[iouCurrency_](0), tfClearFreeze | tfClearDeepFreeze)); - }; - return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN); - } - - auto freeze = [&](Account const& holder) { - mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock}); - }; - auto unfreeze = [&](Account const& holder) { - mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock}); - }; - return std::make_tuple(freeze, empty, unfreeze, tecLOCKED); - }(); - - // Try freezing the accounts that can't be frozen - if (freeze) - { - for (auto const& account : {vaultPseudo, evan}) - { - // Freeze the account - freeze(account); - - // Try to create a loan with a frozen line - env(set(evan, broker.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(expectedResult)); - - // Unfreeze the account - BEAST_EXPECT(unfreeze); - unfreeze(account); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, broker.brokerID, debtMaximumRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecLIMIT_EXCEEDED)); - } - } - - // Deep freeze the borrower, which prevents them from receiving - // funds - if (deepfreeze) - { - // Make sure evan has a trust line that so the issuer can - // freeze it. (Don't need to do this for the borrower, - // because LoanSet will create a line to the borrower - // automatically.) - env(trust(evan, issuer[iouCurrency_](100'000))); - - for (auto const& account : {// these accounts can't be frozen, which deep freeze - // implies - vaultPseudo, - evan, - // these accounts can't be deep frozen - lender}) - { - // Freeze evan - deepfreeze(account); - - // Try to create a loan with a deep frozen line - env(set(evan, broker.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(expectedResult)); - - // Unfreeze evan - BEAST_EXPECT(unfreeze); - unfreeze(account); - - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, broker.brokerID, debtMaximumRequest + 1), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecLIMIT_EXCEEDED)); - } - } - } - - // Finally! Create a loan - - auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) { - if (auto const brokerSle = env.le(keylet::loanBroker(brokerID)); - BEAST_EXPECT(brokerSle)) - { - auto const available = brokerSle->at(sfCoverAvailable); - BEAST_EXPECT(available == expected); - return available; - } - return Number{}; - }; - auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) { - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT( - state.loanScale >= - (broker.asset.integral() - ? 0 - : std::max( - broker.vaultScale(env), state.principalOutstanding.exponent()))); - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - auto const defaultAmount = roundToAsset( - broker.asset, - std::min( - tenthBipsOfValue( - tenthBipsOfValue( - brokerSle->at(sfDebtTotal), broker.params.coverRateMin), - broker.params.coverRateLiquidation), - state.totalValue - state.managementFeeOutstanding), - state.loanScale); - return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); - } - return std::make_pair(Number{}, AccountID{}); - }; - auto replenishCover = [&env, &coverAvailable]( - BrokerInfo const& broker, - AccountID const& brokerAcct, - Number const& startingCoverAvailable, - Number const& amountToBeCovered) { - coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered); - env(loan_broker::coverDeposit( - brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered})); - coverAvailable(broker.brokerID, startingCoverAvailable); - env.close(); - }; - - auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) { - return [&, impair, baseFlag]( - Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Default the loan - - // Initialize values with the current state - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == baseFlag); - - auto const& broker = verifyLoanStatus.broker; - auto const startingCoverAvailable = coverAvailable( - broker.brokerID, broker.asset(broker.params.coverDeposit).number()); - - if (impair) - { - // Check the vault - bool const canImpair = canImpairLoan(env, broker, state); - // Impair the loan, if possible - env(manage(lender, loanKeylet.key, tfLoanImpair), - canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); - - if (canImpair) - { - state.flags |= tfLoanImpair; - state.nextPaymentDate = env.now().time_since_epoch().count(); - - // Once the loan is impaired, it can't be impaired again - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - } - verifyLoanStatus(state); - } - - auto const nextDueDate = tp{d{state.nextPaymentDate}}; - - // Can't default the loan yet. The grace period hasn't - // expired - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON)); - - // Let some time pass so that the loan can be - // defaulted - env.close(nextDueDate + 60s); - - auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker); - - // Default the loan - env(manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // The LoanBroker just lost some of it's first-loss capital. - // Replenish it. - replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered); - - state.flags |= tfLoanDefault; - state.paymentRemaining = 0; - state.totalValue = 0; - state.principalOutstanding = 0; - state.managementFeeOutstanding = 0; - state.nextPaymentDate = 0; - verifyLoanStatus(state); - - // Once a loan is defaulted, it can't be managed - env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - // Can't make a payment on it either - env(pay(borrower, loanKeylet.key, broker.asset(300)), Ter(tecKILLED)); - }; - }; - - auto singlePayment = [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus, - LoanState& state, - STAmount const& payoffAmount, - std::uint32_t numPayments, - std::uint32_t baseFlag, - std::uint32_t txFlags) { - // toEndOfLife - // - verifyLoanStatus(state); - - // Send some bogus pay transactions - env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags), - Ter(temINVALID)); - // broker.asset(80) is less than a single payment, but all these - // checks fail before that matters - env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), Ter(temBAD_AMOUNT)); - env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), Ter(tecNO_ENTRY)); - env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), Ter(tecNO_PERMISSION)); - - // TODO: Write a general "isFlag" function? See STObject::isFlag. - // Maybe add a static overloaded member? - if (!(state.flags & lsfLoanOverpayment)) - { - // If the loan does not allow overpayments, send a payment that - // tries to make an overpayment. Do not include `txFlags`, so we - // don't end up duplicating the next test transaction. - // - // fixCleanup3_1_3 gates tfLoanOverpayment as a valid flag: - // with fix on → preflight passes, apply returns tecNO_PERMISSION; - // with fix off → preflight rejects the flag, returns temINVALID_FLAG. - bool const hasFix313 = env.current()->rules().enabled(fixCleanup3_1_3); - STAmount const overpayAmount{broker.asset, state.periodicPayment * Number{15, -1}}; - XRPAmount const overpayFee{ - baseFee * (Number{15, -1} / kLoanPaymentsPerFeeIncrement + 1)}; - env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), - Fee(overpayFee), - Ter(hasFix313 ? TER{tecNO_PERMISSION} : TER{temINVALID_FLAG})); - - if (hasFix313) - { - env.disableFeature(fixCleanup3_1_3); - env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), - Fee(overpayFee), - Ter(temINVALID_FLAG)); - env.enableFeature(fixCleanup3_1_3); - } - } - // Try to send a payment marked as multiple mutually exclusive - // payment types. Do not include `txFlags`, so we don't duplicate - // the prior test transaction. - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanFullPayment), - Ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanOverpayment), - Ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanOverpayment | tfLoanFullPayment), - Ter(temINVALID_FLAG)); - env(pay(borrower, - loanKeylet.key, - broker.asset(state.periodicPayment * 2), - tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment), - Ter(temINVALID_FLAG)); - - { - auto const otherAsset = - broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0]; - env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), Ter(tecWRONG_ASSET)); - } - - // Amount doesn't cover a single payment - env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags), - Ter(tecINSUFFICIENT_PAYMENT)); - - // Get the balance after these failed transactions take - // fees - auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - // Try to pay a little extra to show that it's _not_ - // taken - auto const transactionAmount = payoffAmount + broker.asset(10); - - // Send a transaction that tries to pay more than the borrowers's - // balance - XRPAmount const badFee{ - baseFee * - (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment / - kLoanPaymentsPerFeeIncrement + - 1)}; - env(pay(borrower, - loanKeylet.key, - STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2}, - txFlags), - Fee(badFee), - Ter(tecINSUFFICIENT_FUNDS)); - - XRPAmount const goodFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; - env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), Fee(goodFee)); - - env.close(); - - // log << env.meta()->getJson() << std::endl; - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = badFee + goodFee; - } - - state.paymentRemaining = 0; - state.principalOutstanding = 0; - state.totalValue = 0; - state.managementFeeOutstanding = 0; - state.previousPaymentDate = - state.nextPaymentDate + (state.paymentInterval * (numPayments - 1)); - state.nextPaymentDate = 0; - verifyLoanStatus(state); - - verifyLoanStatus.checkPayment( - state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); - }; - - auto fullPayment = [&](std::uint32_t baseFlag) { - return [&, baseFlag]( - Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - // Full payoff amount will consist of - // 1. principal outstanding (1000) - // 2. accrued interest (at 12%) - // 3. prepayment penalty (closeInterest at 3.6%) - // 4. close payment fee (4) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; - BEAST_EXPECT( - periodicRate == Number(2283105022831050228ULL, -24, Number::Normalized{})); - STAmount const principalOutstanding{broker.asset, state.principalOutstanding}; - STAmount const accruedInterest{ - broker.asset, state.principalOutstanding * periodicRate * loanAge / interval}; - BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19))); - STAmount const prepaymentPenalty{ - broker.asset, state.principalOutstanding * Number(36, -3)}; - BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); - STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = roundToScale( - principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee, - state.loanScale); - BEAST_EXPECT( - payoffAmount == - roundToAsset( - broker.asset, - broker.asset(Number(1040000114155251, -12)).number(), - state.loanScale)); - - // The terms of this loan actually make the early payoff - // more expensive than just making payments - BEAST_EXPECT( - payoffAmount > - state.paymentRemaining * (state.periodicPayment + broker.asset(2).value())); - - singlePayment( - loanKeylet, - verifyLoanStatus, - state, - payoffAmount, - 1, - baseFlag, - tfLoanFullPayment); - }; - }; - - auto combineAllPayments = [&](std::uint32_t baseFlag) { - return - [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - env.close(); - - BEAST_EXPECT( - STAmount(broker.asset, state.periodicPayment) == - broker.asset(Number(8333457002039338267, -17))); - - // Make all the payments in one transaction - // service fee is 2 - auto const startingPayments = state.paymentRemaining; - STAmount const payoffAmount = [&]() { - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - auto const rawPayoff = - startingPayments * (state.periodicPayment + broker.asset(2).value()); - STAmount payoffAmount{broker.asset, rawPayoff}; - BEAST_EXPECTS( - payoffAmount == broker.asset(Number(1024014840244721, -12)), - to_string(payoffAmount)); - BEAST_EXPECT(payoffAmount > state.principalOutstanding); - - payoffAmount = roundToScale(payoffAmount, state.loanScale); - - return payoffAmount; - }(); - - auto const totalPayoffValue = - state.totalValue + startingPayments * broker.asset(2).value(); - STAmount const totalPayoffAmount{broker.asset, totalPayoffValue}; - - BEAST_EXPECTS( - totalPayoffAmount == payoffAmount, - "Payoff amount: " + to_string(payoffAmount) + - ". Total Value: " + to_string(totalPayoffAmount)); - - singlePayment( - loanKeylet, - verifyLoanStatus, - state, - payoffAmount, - state.paymentRemaining, - baseFlag, - 0); - }; - }; - - // There are a lot of fields that can be set on a loan, but most - // of them only affect the "math" when a payment is made. The - // only one that really affects behavior is the - // `tfLoanOverpayment` flag. - lifecycle( - caseLabel, - "Loan overpayment allowed - Impair and Default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultImmediately(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Impair and Default", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultImmediately(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Default without Impair", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - defaultImmediately(lsfLoanOverpayment, false)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Default without Impair", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - defaultImmediately(0, false)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Pay off immediately", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - fullPayment(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Pay off immediately", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - fullPayment(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Combine all payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - combineAllPayments(0)); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Combine all payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - combineAllPayments(lsfLoanOverpayment)); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Make payments", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - 0, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // toEndOfLife - // - // Draw and make multiple payments - auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); - BEAST_EXPECT(state.flags == 0); - env.close(); - - verifyLoanStatus(state); - - env.close(state.startDate + 20s); - auto const loanAge = (env.now() - state.startDate).count(); - BEAST_EXPECT(loanAge == 30); - - // Periodic payment amount will consist of - // 1. principal outstanding (1000) - // 2. interest interest rate (at 12%) - // 3. payment interval (600s) - // 4. loan service fee (2) - // Calculate these values without the helper functions - // to verify they're working correctly The numbers in - // the below BEAST_EXPECTs may not hold across assets. - Number const interval = state.paymentInterval; - auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; - BEAST_EXPECT( - periodicRate == Number(2283105022831050228, -24, Number::Normalized{})); - STAmount const roundedPeriodicPayment{ - broker.asset, - roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; - - testcase << currencyLabel << " Payment components: " - << "Payments remaining, rawInterest, rawPrincipal, " - "rawMFee, trackedValueDelta, trackedPrincipalDelta, " - "trackedInterestDelta, trackedMgmtFeeDelta, special"; - - auto const serviceFee = broker.asset(2); - - BEAST_EXPECT( - roundedPeriodicPayment == - roundToScale( - broker.asset( - Number(8333457002039338267, -17), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - // 83334570.01162141 - // Include the service fee - STAmount const totalDue = roundToScale( - roundedPeriodicPayment + serviceFee, - state.loanScale, - Number::RoundingMode::Upward); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - totalDue == - roundToScale( - broker.asset( - Number(8533457002039338267, -17), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - - { - auto const raw = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - auto const rounded = constructLoanState( - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding); - testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining - << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " - << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", " - << rounded.principalOutstanding << ", " << rounded.interestDue << ", " - << rounded.managementFeeDue; - } - - // Try to pay a little extra to show that it's _not_ - // taken - STAmount const transactionAmount = - STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding - // may drift as payments are made - BEAST_EXPECT( - transactionAmount == - roundToScale( - broker.asset(Number(9533457002039400, -14), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - - auto const initialState = state; - xrpl::detail::PaymentComponents totalPaid{ - .trackedValueDelta = 0, - .trackedPrincipalDelta = 0, - .trackedManagementFeeDelta = 0}; - Number totalInterestPaid = 0; - std::size_t totalPaymentsMade = 0; - - xrpl::LoanState currentTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - while (state.paymentRemaining > 0) - { - // Compute the expected principal amount - auto const paymentComponents = xrpl::detail::computePaymentComponents( - env.current()->rules(), - broker.asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - broker.params.managementFeeRate); - - BEAST_EXPECTS( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedValueDelta <= roundedPeriodicPayment, - "Delta: " + to_string(paymentComponents.trackedValueDelta) + - ", periodic payment: " + to_string(roundedPeriodicPayment)); - - xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining - 1, - broker.params.managementFeeRate); - xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; - - testcase << currencyLabel << " Payment components: " << state.paymentRemaining - << ", " << deltas.interest << ", " << deltas.principal << ", " - << deltas.managementFee << ", " << paymentComponents.trackedValueDelta - << ", " << paymentComponents.trackedPrincipalDelta << ", " - << paymentComponents.trackedInterestPart() << ", " - << paymentComponents.trackedManagementFeeDelta << ", " - << [&]() -> char const* { - if (paymentComponents.specialCase == - ::xrpl::detail::PaymentSpecialCase::Final) - return "final"; - if (paymentComponents.specialCase == - ::xrpl::detail::PaymentSpecialCase::Extra) - return "extra"; - return "none"; - }(); - - auto const totalDueAmount = STAmount{ - broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()}; - - // Due to the rounding algorithms to keep the interest and - // principal in sync with "true" values, the computed amount - // may be a little less than the rounded fixed payment - // amount. For integral types, the difference should be < 3 - // (1 unit for each of the interest and management fee). For - // IOUs, the difference should be after the 8th digit. - Number const diff = totalDue - totalDueAmount; - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - diff == beast::kZero || - (diff > beast::kZero && - ((broker.asset.integral() && (static_cast(diff) < 3)) || - (state.loanScale - diff.exponent() > 13)))); - - BEAST_EXPECT( - paymentComponents.trackedValueDelta == - paymentComponents.trackedPrincipalDelta + - paymentComponents.trackedInterestPart() + - paymentComponents.trackedManagementFeeDelta); - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedValueDelta <= roundedPeriodicPayment); - - BEAST_EXPECT( - state.paymentRemaining < 12 || - roundToAsset( - broker.asset, - deltas.principal, - state.loanScale, - Number::RoundingMode::Upward) == - roundToScale( - broker.asset( - Number(8333228691531218890, -17), Number::RoundingMode::Upward), - state.loanScale, - Number::RoundingMode::Upward)); - BEAST_EXPECT( - paymentComponents.trackedPrincipalDelta >= beast::kZero && - paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || - paymentComponents.trackedPrincipalDelta == state.principalOutstanding); - BEAST_EXPECT( - paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || - (state.periodicPayment.exponent() - - (deltas.principal + deltas.interest + deltas.managementFee - - state.periodicPayment) - .exponent()) > 14); - - auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); - - if (canImpairLoan(env, broker, state)) - { - // Making a payment will unimpair the loan - env(manage(lender, loanKeylet.key, tfLoanImpair)); - } - - env.close(); - - // Make the payment - env(pay(borrower, loanKeylet.key, transactionAmount)); - - env.close(); - - // Need to account for fees if the loan is in XRP - PrettyAmount adjustment = broker.asset(0); - if (broker.asset.native()) - { - adjustment = env.current()->fees().base; - } - - // Check the result - verifyLoanStatus.checkPayment( - state.loanScale, - borrower, - borrowerBalanceBeforePayment, - totalDueAmount, - adjustment); - - --state.paymentRemaining; - state.previousPaymentDate = state.nextPaymentDate; - if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) - { - state.paymentRemaining = 0; - state.nextPaymentDate = 0; - } - else - { - state.nextPaymentDate += state.paymentInterval; - } - state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; - state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; - state.totalValue -= paymentComponents.trackedValueDelta; - - verifyLoanStatus(state); - - totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; - totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; - totalPaid.trackedManagementFeeDelta += - paymentComponents.trackedManagementFeeDelta; - totalInterestPaid += paymentComponents.trackedInterestPart(); - ++totalPaymentsMade; - - currentTrueState = nextTrueState; - } - - // Loan is paid off - BEAST_EXPECT(state.paymentRemaining == 0); - BEAST_EXPECT(state.principalOutstanding == 0); - - // Make sure all the payments add up - BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); - BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); - BEAST_EXPECT( - totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); - // This is almost a tautology given the previous checks, but - // check it anyway for completeness. - BEAST_EXPECT( - totalInterestPaid == - initialState.totalValue - - (initialState.principalOutstanding + - initialState.managementFeeOutstanding)); - BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); - - // Can't impair or default a paid off loan - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); - }); - -#if LOAN_TODO - // TODO - - /* - LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also - borrower) tries to do the payment. Here's the scenario: Create a XRP - loan with loan broker as borrower, loan origination fee and loan service - fee. Loan broker makes the first payment with periodic payment and loan - service fee. - */ - - auto time = [&](std::string label, std::function timed) { - if (!BEAST_EXPECT(timed)) - return; - - using clock_type = std::chrono::steady_clock; - using duration_type = std::chrono::milliseconds; - - auto const start = clock_type::now(); - timed(); - auto const duration = - std::chrono::duration_cast(clock_type::now() - start); - - log << label << " took " << duration.count() << "ms" << std::endl; - - return duration; - }; - - lifecycle( - caseLabel, - "timing", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { - // Estimate optimal values for kLoanPaymentsPerFeeIncrement and - // kLoanMaximumPaymentsPerTransaction. - using namespace loan; - - auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet); - auto const serviceFee = broker.asset(2).value(); - - STAmount const totalDue{ - broker.asset, - roundPeriodicPayment( - broker.asset, state.periodicPayment + serviceFee, state.loanScale)}; - - // Make a single payment - time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); }); - env.close(); - - // Make all but the final payment - auto const numPayments = (state.paymentRemaining - 2); - STAmount const bigPayment{broker.asset, totalDue * numPayments}; - XRPAmount const bigFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; - time("ten payments", [&]() { - env(pay(borrower, loanKeylet.key, bigPayment), Fee(bigFee)); - }); - env.close(); - - time("final payment", [&]() { - // Make the final payment - env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1})); - }); - env.close(); - }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Explicit overpayment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment prohibited - Late payment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Late payment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - - lifecycle( - caseLabel, - "Loan overpayment allowed - Late payment and overpayment", - env, - loanAmount, - interestExponent, - lender, - borrower, - evan, - broker, - pseudoAcct, - tfLoanOverpayment, - [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); - -#endif - } - - void - testLoanSet(FeatureBitset features) - { - using namespace jtx; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - struct CaseArgs - { - bool requireAuth = false; - bool authorizeBorrower = false; - int initialXRP = 1'000'000; - }; - - auto const testCase = [&, this]( - std::function mptTest, - std::function iouTest, - CaseArgs args = {}) { - Env env(*this, features); - env.fund(XRP(args.initialXRP), issuer, lender, borrower); - env.close(); - if (args.requireAuth) - { - env(fset(issuer, asfRequireAuth)); - env.close(); - } - - // We need two different asset types, MPT and IOU. Prepare MPT - // first - MPTTester mptt{env, issuer, kMptInitNoFund}; - - auto const kNone = LedgerSpecificFlags(0); - mptt.create( - {.flags = tfMPTCanTransfer | tfMPTCanLock | - (args.requireAuth ? tfMPTRequireAuth : kNone)}); - env.close(); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - env.close(); - if (args.requireAuth) - { - mptt.authorize({.account = issuer, .holder = lender}); - if (args.authorizeBorrower) - mptt.authorize({.account = issuer, .holder = borrower}); - env.close(); - } - - env(pay(issuer, lender, mptAsset(10'000'000))); - env.close(); - - // Prepare IOU - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env.close(); - if (args.requireAuth) - { - env(trust(issuer, iouAsset(0), lender, tfSetfAuth)); - env(pay(issuer, lender, iouAsset(10'000'000))); - if (args.authorizeBorrower) - { - env(trust(issuer, iouAsset(0), borrower, tfSetfAuth)); - env(pay(issuer, borrower, iouAsset(10'000))); - } - } - else - { - env(pay(issuer, lender, iouAsset(10'000'000))); - env(pay(issuer, borrower, iouAsset(10'000))); - } - env.close(); - - // Create vaults and loan brokers - std::array const assets{mptAsset, iouAsset}; - std::vector brokers; - brokers.reserve(assets.size()); - for (auto const& asset : assets) - { - brokers.emplace_back(createVaultAndBroker(env, asset, lender)); - } - - if (mptTest) - mptTest(env, brokers[0], mptt); - if (iouTest) - iouTest(env, brokers[1]); - }; - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT issuer is borrower, issuer submits"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - - testcase("MPT issuer is borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(issuer), - Sig(sfCounterpartySignature, issuer), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU issuer is borrower, issuer submits"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - - testcase("IOU issuer is borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(issuer), - Sig(sfCounterpartySignature, issuer), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT unauthorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - - testcase("MPT unauthorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU unauthorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - - testcase("IOU unauthorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - }, - CaseArgs{.requireAuth = true}); - - auto const [acctReserve, incReserve] = [this]() -> std::pair { - Env const env{*this, testableAmendments()}; - return { - env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), - env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; - }(); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, borrower has " - "no reserve"); - mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize}); - env.close(); - - auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 == nullptr); - - // Burn some XRP - env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); - env.close(); - - // Cannot create loan, not enough reserve to create MPToken - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecINSUFFICIENT_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create MPToken - env(pay(issuer, borrower, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 != nullptr); - }, - {}, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - {}, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, borrower has " - "no reserve"); - // Remove trust line from borrower to issuer - env.trust(broker.asset(0), borrower); - env.close(); - - env(pay(borrower, issuer, broker.asset(10'000))); - env.close(); - auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get()); - auto const sleLine1 = env.le(trustline); - BEAST_EXPECT(sleLine1 == nullptr); - - // Burn some XRP - env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); - env.close(); - - // Cannot create loan, not enough reserve to create trust line - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_LINE_INSUF_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create trust line - env(pay(issuer, borrower, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleLine2 = env.le(trustline); - BEAST_EXPECT(sleLine2 != nullptr); - }, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, lender has " - "no reserve"); - auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 != nullptr); - - env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); - env.close(); - - mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 == nullptr); - - // Burn some XRP - env(noop(lender), Fee(XRP(incReserve))); - env.close(); - - // Cannot create loan, not enough reserve to create MPToken - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecINSUFFICIENT_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create MPToken - env(pay(issuer, lender, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleMPT3 = env.le(mptoken); - BEAST_EXPECT(sleMPT3 != nullptr); - }, - {}, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - {}, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, lender has no " - "reserve"); - // Remove trust line from lender to issuer - env.trust(broker.asset(0), lender); - env.close(); - - auto const trustline = keylet::trustLine(lender, broker.asset.raw().get()); - auto const sleLine1 = env.le(trustline); - BEAST_EXPECT(sleLine1 != nullptr); - - env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value())))); - env.close(); - auto const sleLine2 = env.le(trustline); - BEAST_EXPECT(sleLine2 == nullptr); - - // Burn some XRP - env(noop(lender), Fee(XRP(incReserve))); - env.close(); - - // Cannot create loan, not enough reserve to create trust line - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_LINE_INSUF_RESERVE}); - env.close(); - - // Can create loan now, will implicitly create trust line - env(pay(issuer, lender, XRP(incReserve))); - env.close(); - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - env.close(); - - auto const sleLine3 = env.le(trustline); - BEAST_EXPECT(sleLine3 != nullptr); - }, - CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, unauthorized lender"); - auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); - auto const sleMPT1 = env.le(mptoken); - BEAST_EXPECT(sleMPT1 != nullptr); - - env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); - env.close(); - - mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); - env.close(); - - auto const sleMPT2 = env.le(mptoken); - BEAST_EXPECT(sleMPT2 == nullptr); - - // Cannot create loan, lender not authorized to receive fee - env(set(borrower, broker.brokerID, principalRequest), - kLoanOriginationFee(broker.asset(1).value()), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - env.close(); - - // Cannot create loan, even without an origination fee - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter{tecNO_AUTH}); - env.close(); - - // No MPToken for lender - no authorization and no payment - auto const sleMPT3 = env.le(mptoken); - BEAST_EXPECT(sleMPT3 == nullptr); - }, - {}, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU authorized borrower, borrower submits"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("MPT authorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - - testcase("IOU authorized borrower, lender submits"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - jtx::Account const alice{"alice"}; - jtx::Account const bella{"bella"}; - auto const msigSetup = [&](Env& env, Account const& account) { - json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}}); - env(tx1); - env.close(); - }; - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - msigSetup(env, lender); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, borrower submits, lender " - "multisign"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - msigSetup(env, lender); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, borrower submits, lender " - "multisign"); - env(set(borrower, broker.brokerID, principalRequest), - kCounterparty(lender), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - msigSetup(env, borrower); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "MPT authorized borrower, lender submits, borrower " - "multisign"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - [&, this](Env& env, BrokerInfo const& broker) { - using namespace loan; - msigSetup(env, borrower); - Number const principalRequest = broker.asset(1'000).value(); - - testcase( - "IOU authorized borrower, lender submits, borrower " - "multisign"); - env(set(lender, broker.brokerID, principalRequest), - kCounterparty(borrower), - Msig(sfCounterpartySignature, alice, bella), - Fee(env.current()->fees().base * 5)); - }, - CaseArgs{.requireAuth = true, .authorizeBorrower = true}); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - Vault const vault{env}; - auto tx = vault.set({.owner = lender, .id = broker.vaultID}); - tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit; - env(tx); - env.close(); - - testcase("Vault at maximum value"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - kInterestRate(TenthBips32(10'000)), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - Ter(tecLIMIT_EXCEEDED)); - }, - nullptr); - - testCase( - [&, this](Env& env, BrokerInfo const& broker, auto&) { - using namespace loan; - Number const principalRequest = broker.asset(1'000).value(); - Vault const vault{env}; - auto tx = vault.set({.owner = lender, .id = broker.vaultID}); - tx[sfAssetsMaximum] = - BrokerParameters::defaults().vaultDeposit + broker.asset(1).number(); - env(tx); - env.close(); - - testcase("Vault maximum value exceeded"); - env(set(issuer, broker.brokerID, principalRequest), - kCounterparty(lender), - kInterestRate(TenthBips32(100'000)), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 5), - kPaymentTotal(2), - kPaymentInterval(3600 * 24), - Ter(tecLIMIT_EXCEEDED)); - }, - nullptr); - } - - void - testLifecycle(FeatureBitset features) - { - testcase("Lifecycle"); - using namespace jtx; - - // Create 3 loan brokers: one for XRP, one for an IOU, and one for - // an MPT. That'll require three corresponding SAVs. - Env env(*this, features); - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - // Borrower only wants to borrow - Account const borrower{"borrower"}; - // Evan will attempt to be naughty - Account const evan{"evan"}; - // Do not fund alice - Account const alice{"alice"}; - - // Fund the accounts and trust lines with the same amount so that - // tests can use the same values regardless of the asset. - env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); - env.close(); - - // Create assets - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env(trust(evan, iouAsset(10'000'000))); - env(pay(issuer, evan, iouAsset(1'000'000))); - env(pay(issuer, lender, iouAsset(10'000'000))); - // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, iouAsset(10'000))); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - // Scale the MPT asset a little bit so we can get some interest - PrettyAsset const mptAsset{mptt.issuanceID(), 100}; - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - mptt.authorize({.account = evan}); - env(pay(issuer, lender, mptAsset(10'000'000))); - env(pay(issuer, evan, mptAsset(1'000'000))); - // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, mptAsset(10'000))); - env.close(); - - std::array const assets{iouAsset, xrpAsset, mptAsset}; - - // Create vaults and loan brokers - std::vector brokers; - brokers.reserve(assets.size()); - for (auto const& asset : assets) - { - brokers.emplace_back(createVaultAndBroker( - env, asset, lender, BrokerParameters{.data = "spam spam spam spam"})); - } - - // Create and update Loans - for (auto const& broker : brokers) - { - for (int amountExponent = 3; amountExponent >= 3; --amountExponent) - { - Number const loanAmount{1, amountExponent}; - for (int interestExponent = 0; interestExponent >= 0; --interestExponent) - { - testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent); - } - } - - if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); - - auto const coverAvailable = brokerSle->at(sfCoverAvailable); - env(loan_broker::coverWithdraw( - lender, broker.brokerID, STAmount(broker.asset, coverAvailable))); - env.close(); - - brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); - } - // Verify we can delete the loan broker - env(loan_broker::del(lender, broker.brokerID)); - env.close(); - } - } - - void - testSelfLoan(FeatureBitset features) - { - testcase << "Self Loan"; - - using namespace jtx; - using namespace std::chrono_literals; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for - // an MPT. That'll require three corresponding SAVs. - Env env(*this, features); - - Account const issuer{"issuer"}; - // For simplicity, lender will be the sole actor for the vault & - // brokers. - Account const lender{"lender"}; - - // Fund the accounts and trust lines with the same amount so that - // tests can use the same values regardless of the asset. - env.fund(XRP(100'000'000), issuer, noripple(lender)); - env.close(); - - // Use an XRP asset for simplicity - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - // Create vaults and loan brokers - BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - // The LoanSet json can be created without a counterparty signature, - // but it will not pass preflight - auto createJson = env.json( - set(lender, broker.brokerID, broker.asset(principalRequest).value()), Fee(loanSetFee)); - env(createJson, Ter(temBAD_SIGNER)); - - // Adding an empty counterparty signature object also fails, but - // at the RPC level. - createJson = env.json(createJson, Json(sfCounterpartySignature, json::ValueType::Object)); - env(createJson, Ter(telENV_RPC_FAILED)); - - if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx)) - { - Serializer s; - jt.stx->add(s); - auto const jr = env.rpc("submit", strHex(s.slice())); - - BEAST_EXPECT(jr.isMember(jss::result)); - auto const jResult = jr[jss::result]; - BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); - BEAST_EXPECT( - jResult[jss::error_exception] == - "fails local checks: Transaction has bad signature."); - } - - // Copy the transaction signature into the counterparty signature. - json::Value counterpartyJson{json::ValueType::Object}; - counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; - counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; - if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) - counterpartyJson[sfSigners] = createJson[sfSigners]; - - // The duplicated signature works - createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); - env(createJson); - - env.close(); - - auto const startDate = env.current()->header().parentCloseTime; - - // Loan is successfully created - { - auto const res = env.rpc("account_objects", lender.human()); - auto const objects = res[jss::result][jss::account_objects]; - - std::map types; - BEAST_EXPECT(objects.size() == 4); - for (auto const& object : objects) - { - ++types[object[sfLedgerEntryType].asString()]; - } - BEAST_EXPECT(types.size() == 4); - for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"}) - { - BEAST_EXPECT(types[type] == 1); - } - } - auto const loanID = [&]() { - json::Value params(json::ValueType::Object); - params[jss::account] = lender.human(); - params[jss::type] = "Loan"; - auto const res = env.rpc("json", "account_objects", to_string(params)); - auto const objects = res[jss::result][jss::account_objects]; - - BEAST_EXPECT(objects.size() == 1); - - auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfBorrower] == lender.human()); - // soeDEFAULT fields are not returned if they're in the default - // state - BEAST_EXPECT(!loan.isMember(sfCloseInterestRate)); - BEAST_EXPECT(!loan.isMember(sfClosePaymentFee)); - BEAST_EXPECT(loan[sfFlags] == 0); - BEAST_EXPECT(loan[sfGracePeriod] == 60); - BEAST_EXPECT(!loan.isMember(sfInterestRate)); - BEAST_EXPECT(!loan.isMember(sfLateInterestRate)); - BEAST_EXPECT(!loan.isMember(sfLatePaymentFee)); - BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); - BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee)); - BEAST_EXPECT(loan[sfLoanSequence] == 1); - BEAST_EXPECT(!loan.isMember(sfLoanServiceFee)); - BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); - BEAST_EXPECT(!loan.isMember(sfOverpaymentFee)); - BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate)); - BEAST_EXPECT(loan[sfPaymentInterval] == 60); - BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); - BEAST_EXPECT(loan[sfPaymentRemaining] == 1); - BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate)); - BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); - BEAST_EXPECT(!loan.isMember(sfLoanScale)); - BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); - - return loan["index"].asString(); - }(); - auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})}; - - env.close(startDate); - - // Make a payment - env(pay(lender, loanKeylet.key, broker.asset(1000))); - } - - void - testBatchBypassCounterparty(FeatureBitset features) - { - // From FIND-001 - testcase << "Batch Bypass Counterparty"; - - bool const lendingBatchEnabled = !std::ranges::any_of( - Batch::kDisabledTxTypes, - [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }); - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - BrokerParameters const brokerParams; - env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0); - - json::Value randomData{json::ValueType::Object}; - randomData[jss::SigningPubKey] = json::StaticString{"2600"}; - json::Value sigObject{json::ValueType::Object}; - sigObject[jss::SigningPubKey] = strHex(lender.pk().slice()); - Serializer ss; - ss.add32(HashPrefix::TxSign); - parse(randomData).addWithoutSigningFields(ss); - auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice()); - sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); - - forgedLoanSet[json::StaticString{"CounterpartySignature"}] = sigObject; - - // ? Fails because the lender hasn't signed the tx - env(env.json(forgedLoanSet, Fee(loanSetFee)), Ter(telENV_RPC_FAILED)); - - auto const seq = env.seq(borrower); - auto const batchFee = batch::calcBatchFee(env, 1, 2); - // ! Should fail because the lender hasn't signed the tx - env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), - batch::Inner(forgedLoanSet, seq + 1), - batch::Inner(pay(borrower, lender, XRP(1)), seq + 2), - Ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH)); - env.close(); - - // ? Check that the loan was NOT created - { - json::Value params(json::ValueType::Object); - params[jss::account] = borrower.human(); - params[jss::type] = "Loan"; - auto const res = env.rpc("json", "account_objects", to_string(params)); - auto const objects = res[jss::result][jss::account_objects]; - BEAST_EXPECT(objects.size() == 0); - } - } - - void - testWrongMaxDebtBehavior(FeatureBitset features) - { - // From FIND-003 - testcase << "Wrong Max Debt Behavior"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - - BrokerParameters const brokerParams{.debtMax = 0}; - env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender)); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0); - } - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee)); - - json::Value counterpartyJson{json::ValueType::Object}; - counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; - counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; - if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) - counterpartyJson[sfSigners] = createJson[sfSigners]; - - createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); - env(createJson); - - env.close(); - } - - void - testLoanPayComputePeriodicPaymentValidRateInvariant(FeatureBitset features) - { - // From FIND-012 - testcase << "LoanPay xrpl::detail::computePeriodicPayment : " - "valid rate"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - BrokerParameters const brokerParams; - env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower); - env.close(); - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{640562, -5}; - - Number const serviceFee{2462611968}; - std::uint32_t const numPayments{4294967295 / 800}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - kLoanServiceFee(serviceFee), - kPaymentTotal(numPayments), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["CloseInterestRate"] = 55374; - createJson["ClosePaymentFee"] = "3825205248"; - createJson["LatePaymentFee"] = "237"; - createJson["LoanOriginationFee"] = "0"; - createJson["OverpaymentFee"] = 35167; - createJson["OverpaymentInterestRate"] = 1360; - createJson["PaymentInterval"] = 727; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - // Fails in preclaim because principal requested can't be - // represented as XRP - env(createJson, Ter(tecPRECISION_LOSS)); - env.close(); - - BEAST_EXPECT(!env.le(keylet)); - - Number const actualPrincipal{6}; - - createJson[sfPrincipalRequested] = actualPrincipal; - createJson.removeMember(sfSequence.jsonName); - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - // Fails in doApply because the payment is too small to be - // represented as XRP. - env(createJson, Ter(tecPRECISION_LOSS)); - env.close(); - } - - void - testRPC(FeatureBitset features) - { - // This will expand as more test cases are added. Some functionality - // is tested in other test functions. - testcase("RPC"); - - using namespace jtx; - - Env env(*this, features); - - auto lowerFee = [&]() { - // Run the local fee back down. - while (env.app().getFeeTrack().lowerLocalFee()) - ; - }; - - auto const baseFee = env.current()->fees().base; - - Account const alice{"alice"}; - std::string const borrowerPass = "borrower"; - Account const borrower{borrowerPass, KeyType::Ed25519}; - auto const lenderPass = "lender"; - Account const lender{lenderPass, KeyType::Ed25519}; - - env.fund(XRP(1'000'000), alice, lender, borrower); - env.close(); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env(noop(lender)); - env.close(); - - { - testcase("RPC AccountSet"); - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "AccountSet"; - txJson[sfAccount] = borrower.human(); - - auto const signParams = [&]() { - json::Value signParams{json::ValueType::Object}; - signParams[jss::passphrase] = borrowerPass; - signParams[jss::key_type] = "ed25519"; - signParams[jss::tx_json] = txJson; - return signParams; - }(); - auto const jSign = env.rpc("json", "sign", to_string(signParams)); - BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json)); - auto txSignResult = jSign[jss::result][jss::tx_json]; - auto txSignBlob = jSign[jss::result][jss::tx_blob].asString(); - txSignResult.removeMember(jss::hash); - - auto const jtx = env.jt(txJson, Sig(borrower)); - BEAST_EXPECT(txSignResult == jtx.jv); - - lowerFee(); - auto const jSubmit = env.rpc("submit", txSignBlob); - BEAST_EXPECT( - jSubmit.isMember(jss::result) && - jSubmit[jss::result].isMember(jss::engine_result) && - jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS"); - - lowerFee(); - env(jtx.jv, Sig(kNone), Seq(kNone), Fee(kNone), Ter(tefPAST_SEQ)); - } - - { - testcase("RPC LoanSet - illegal signature_target"); - - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "AccountSet"; - txJson[sfAccount] = borrower.human(); - - auto const borrowerSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "Destination"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::error) && - jSignBorrower[jss::result][jss::error] == "invalidParams" && - jSignBorrower[jss::result].isMember(jss::error_message) && - jSignBorrower[jss::result][jss::error_message] == "Destination"); - } - { - testcase("RPC LoanSet - sign and submit borrower initiated"); - // 1. Borrower creates the transaction - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "LoanSet"; - txJson[sfAccount] = borrower.human(); - txJson[sfCounterparty] = lender.human(); - txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" - "F83F" - "5C"; - txJson[sfPrincipalRequested] = "100000000"; - txJson[sfPaymentTotal] = 10000; - txJson[sfPaymentInterval] = 3600; - txJson[sfGracePeriod] = 300; - txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = to_string(24 * baseFee / 10); - - // 2. Borrower signs the transaction - auto const borrowerSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECTS( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json), - to_string(jSignBorrower)); - auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; - auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); - - // 2a. Borrower attempts to submit the transaction. It doesn't - // work - { - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - // Transaction fails because the CounterpartySignature is - // missing - BEAST_EXPECT( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); - } - - // 3. Borrower sends the signed transaction to the lender - // 4. Lender signs the transaction - auto const lenderSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = lenderPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "CounterpartySignature"; - params[jss::tx_json] = txBorrowerSignResult; - return params; - }(); - auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); - BEAST_EXPECT( - jSignLender.isMember(jss::result) && - jSignLender[jss::result].isMember(jss::tx_json)); - auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; - auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); - - // 5. Lender submits the signed transaction blob - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the - // signatures all validated. Of course the transaction won't - // succeed because no Vault or Broker were created. - BEAST_EXPECTS( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", - to_string(jSubmitBlobResult)); - - BEAST_EXPECT( - !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - - // 4-alt. Lender submits the transaction json originally - // received from the Borrower. It gets signed, but is now a - // duplicate, so fails. Borrower could done this instead of - // steps 4 and 5. - lowerFee(); - auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams)); - BEAST_EXPECT(jSubmitJson.isMember(jss::result)); - auto const jSubmitJsonResult = jSubmitJson[jss::result]; - BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); - auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not - // going anywhere - BEAST_EXPECTS( - jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", - to_string(jSubmitJsonResult)); - - BEAST_EXPECT( - !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); - - BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); - } - - { - testcase("RPC LoanSet - sign and submit lender initiated"); - // 1. Lender creates the transaction - json::Value txJson{json::ValueType::Object}; - txJson[sfTransactionType] = "LoanSet"; - txJson[sfAccount] = lender.human(); - txJson[sfCounterparty] = borrower.human(); - txJson[sfLoanBrokerID] = - "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" - "F83F" - "5C"; - txJson[sfPrincipalRequested] = "100000000"; - txJson[sfPaymentTotal] = 10000; - txJson[sfPaymentInterval] = 3600; - txJson[sfGracePeriod] = 300; - txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = to_string(24 * baseFee / 10); - - // 2. Lender signs the transaction - auto const lenderSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = lenderPass; - params[jss::key_type] = "ed25519"; - params[jss::tx_json] = txJson; - return params; - }(); - auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); - BEAST_EXPECT( - jSignLender.isMember(jss::result) && - jSignLender[jss::result].isMember(jss::tx_json)); - auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; - auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); - - // 2a. Lender attempts to submit the transaction. It doesn't - // work - { - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - // Transaction fails because the CounterpartySignature is - // missing - BEAST_EXPECT( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); - } - - // 3. Lender sends the signed transaction to the Borrower - // 4. Borrower signs the transaction - auto const borrowerSignParams = [&]() { - json::Value params{json::ValueType::Object}; - params[jss::passphrase] = borrowerPass; - params[jss::key_type] = "ed25519"; - params[jss::signature_target] = "CounterpartySignature"; - params[jss::tx_json] = txLenderSignResult; - return params; - }(); - auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( - jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json)); - auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; - auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); - - // 5. Borrower submits the signed transaction blob - lowerFee(); - auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); - BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); - auto const jSubmitBlobResult = jSubmitBlob[jss::result]; - BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); - auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the - // signatures all validated. Of course the transaction won't - // succeed because no Vault or Broker were created. - BEAST_EXPECTS( - jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", - to_string(jSubmitBlobResult)); - - BEAST_EXPECT( - !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - - // 4-alt. Borrower submits the transaction json originally - // received from the Lender. It gets signed, but is now a - // duplicate, so fails. Lender could done this instead of steps - // 4 and 5. - lowerFee(); - auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams)); - BEAST_EXPECT(jSubmitJson.isMember(jss::result)); - auto const jSubmitJsonResult = jSubmitJson[jss::result]; - BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); - auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not - // going anywhere - BEAST_EXPECTS( - jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", - to_string(jSubmitJsonResult)); - - BEAST_EXPECT( - !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); - - BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); - } - } - - void - testServiceFeeOnBrokerDeepFreeze() - { - testcase << "Service Fee On Broker Deep Freeze"; - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - auto const iou = issuer["IOU"]; - - for (bool const deepFreeze : {true, false}) - { - Env env(*this); - - auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) { - if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(le)) - { - auto const account = le->at(accountField); - if (auto const sleLine = env.le(keylet::trustLine(account, iou)); - BEAST_EXPECT(sleLine)) - { - STAmount balance = sleLine->at(sfBalance); - if (account > issuer.id()) - balance.negate(); - return balance; - } - } - return STAmount{iou}; - }; - - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - env(trust(broker, iou(20'000'000))); - env(pay(issuer, broker, iou(10'000'000))); - env.close(); - - auto const brokerInfo = createVaultAndBroker(env, iou, broker); - - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); - - auto const keylet = keylet::loan(brokerInfo.brokerID, 1); - - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(iou(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - - env(trust(borrower, iou(20'000'000))); - // The borrower increases their limit and acquires some IOU so - // they can pay interest - env(pay(issuer, borrower, iou(500))); - env.close(); - - if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le)) - { - if (deepFreeze) - { - env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze)); - env.close(); - } - - env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100))); - env.close(); - - if (deepFreeze) - { - // The fee goes to the broker pseudo-account - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100)); - BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000)); - } - else - { - // The fee goes to the broker account - BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100)); - BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); - } - } - }; - } - - void - testIssuerLoan() - { - testcase << "Issuer Loan"; - - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower = issuer; - Account const lender("lender"); - Env env(*this); - - env.fund(XRP(1'000), issuer, lender); - - static constexpr std::int64_t kIssuerBalance = 10'000'000; - MPTTester const asset( - {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance}); - - BrokerParameters const brokerParams{ - .debtMax = 200, - }; - auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); - auto const loanSetFee = Fee(env.current()->fees().base * 2); - // Create Loan - env(set(borrower, broker.brokerID, 200), Sig(sfCounterpartySignature, lender), loanSetFee); - env.close(); - // Issuer should not create MPToken - BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer))); - // 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); - env(pay(borrower, loanKeylet.key, asset(200))); - env.close(); - // Issuer "re-payed" 200, OutstandingAmount increased by 200 - BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance)); - } - - void - testInvalidLoanDelete() - { - testcase("Invalid LoanDelete"); - using namespace jtx; - using namespace loan; - - // preflight: temINVALID, LoanID == zero - { - Account const alice{"alice"}; - Env env(*this); - env.fund(XRP(1'000), alice); - env.close(); - env(del(alice, beast::kZero), Ter(temINVALID)); - } - } - - void - testInvalidLoanManage() - { - testcase("Invalid LoanManage"); - using namespace jtx; - using namespace loan; - - // preflight: temINVALID, LoanID == zero - { - Account const alice{"alice"}; - Env env(*this); - env.fund(XRP(1'000), alice); - env.close(); - env(manage(alice, beast::kZero, tfLoanDefault), Ter(temINVALID)); - } - } - - void - testInvalidLoanPay() - { - testcase("Invalid LoanPay"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - auto const iou = issuer["IOU"]; - - // preclaim - Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower); - env(trust(lender, iou(10'000'000))); - env(pay(issuer, lender, iou(5'000'000))); - BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); - - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee); - - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); - - env(fset(issuer, asfGlobalFreeze)); - env.close(); - - // preclaim: tecFROZEN - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); - env.close(); - - env(fclear(issuer, asfGlobalFreeze)); - env.close(); - - auto const pseudoBroker = [&]() -> std::optional { - if (auto brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - return Account{"pseudo", brokerSle->at(sfAccount)}; - } - - return std::nullopt; - }(); - if (!pseudoBroker) - return; - - // Lender and pseudoaccount must both be frozen - env(trust(issuer, lender["IOU"](1'000), lender, tfSetFreeze | tfSetDeepFreeze)); - env(trust( - issuer, (*pseudoBroker)["IOU"](1'000), *pseudoBroker, tfSetFreeze | tfSetDeepFreeze)); - env.close(); - - // preclaim: tecFROZEN due to deep frozen - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); - env.close(); - - // Only one needs to be unfrozen - env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); - env.close(); - - // The payment is late by this point - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecEXPIRED)); - env.close(); - env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment)); - env.close(); - - // preclaim: tecKILLED - // note that tecKILLED in loanMakePayment() - // doesn't happen because of the preclaim check. - env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecKILLED)); - } - - void - testInvalidLoanSet() - { - testcase("Invalid LoanSet"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const sponsor{"sponsor"}; - auto const iou = issuer["IOU"]; - - auto testWrapper = [&](auto&& test) { - Env env(*this); - env.fund(XRP(1'000), lender, issuer, borrower, sponsor); - env(trust(lender, iou(10'000'000))); - env(pay(issuer, lender, iou(5'000'000))); - BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); - test(env, brokerInfo, loanSetFee, debtMaximumRequest); - }; - - // preflight: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::Fee const& loanSetFee, - Number const& debtMaximumRequest) { - for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) - { - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - sponsor::As(sponsor, sponsorFlags), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(temINVALID_FLAG)); - } - - // first temBAD_SIGNER: TODO - // invalid grace period - { - // zero grace period - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kGracePeriod(0), - loanSetFee, - Ter(temINVALID)); - - // grace period less than default minimum - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kGracePeriod(LoanSet::kDefaultGracePeriod - 1), - loanSetFee, - Ter(temINVALID)); - - // grace period greater than payment interval - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kPaymentInterval(120), - kGracePeriod(121), - loanSetFee, - Ter(temINVALID)); - } - // empty/zero broker ID - { - auto jv = set(borrower, uint256{}, debtMaximumRequest); - - auto testZeroBrokerID = [&](std::string const& id, std::uint32_t flags = 0) { - // empty broker ID - jv[sfLoanBrokerID] = id; - env(jv, - Sig(sfCounterpartySignature, lender), - loanSetFee, - Txflags(flags), - Ter(temINVALID)); - }; - // empty broker ID - testZeroBrokerID(std::string("")); - // zero broker ID - // needs a flag to distinguish the parsed STTx from the prior - // test - testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig); - } - - // preflightCheckSigningKey() failure: - // can it happen? the signature is checked before transactor - // executes - - JTx const tx = env.jt( - set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee); - STTx local = *(tx.stx); - auto counterpartySig = local.getFieldObject(sfCounterpartySignature); - auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey); - badPubKey[20] ^= 0xAA; - counterpartySig.setFieldVL(sfSigningPubKey, badPubKey); - local.setFieldObject(sfCounterpartySignature, counterpartySig); - json::Value jvResult; - jvResult[jss::tx_blob] = strHex(local.getSerializer().slice()); - auto res = env.rpc("json", "submit", to_string(jvResult))["result"]; - BEAST_EXPECT( - res[jss::error] == "invalidTransaction" && - res[jss::error_exception] == - "fails local checks: Counterparty: Invalid signature."); - }); - - // preclaim: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::Fee const& loanSetFee, - Number const& debtMaximumRequest) { - // canAddHoldingFailure (IOU only, if MPT doesn't have - // MPTCanTransfer set, then can't create Vault/LoanBroker, - // and LoanSet will fail with different error - env(fclear(issuer, asfDefaultRipple)); - env.close(); - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(terNO_RIPPLE)); - }); - - // doApply: - testWrapper([&](Env& env, - BrokerInfo const& brokerInfo, - jtx::Fee const& loanSetFee, - Number const& debtMaximumRequest) { - auto const amt = - env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); - env(pay(borrower, issuer, amt)); - - // tecINSUFFICIENT_RESERVE - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecINSUFFICIENT_RESERVE)); - - // addEmptyHolding failure - env(pay(issuer, borrower, amt)); - env(fset(issuer, asfGlobalFreeze)); - env.close(); - - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - Ter(tecFROZEN)); - }); - } - - void - testAccountSendMptMinAmountInvariant(FeatureBitset features) - { - // (From FIND-006) - testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount " - "and MPT"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - env(pay(issuer, lender, mptAsset(2'000'000))); - env(pay(issuer, borrower, mptAsset(1'000))); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["CloseInterestRate"] = 76671; - createJson["ClosePaymentFee"] = "2061925410"; - createJson["GracePeriod"] = 434; - createJson["InterestRate"] = 50302; - createJson["LateInterestRate"] = 30322; - createJson["LatePaymentFee"] = "294427911"; - createJson["LoanOriginationFee"] = "3250635102"; - createJson["LoanServiceFee"] = "9557386"; - createJson["OverpaymentFee"] = 51249; - createJson["OverpaymentInterestRate"] = 14304; - createJson["PaymentInterval"] = 434; - createJson["PaymentTotal"] = "2891743748"; - createJson["PrincipalRequested"] = "8516.98"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(temINVALID)); - env.close(); - } - - void - testLoanPayDebtDecreaseInvariant(FeatureBitset features) - { - // From FIND-007 - testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease " - "rounding good"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const baseFee = env.current()->fees().base; - auto const loanSetFee = Fee(baseFee * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["ClosePaymentFee"] = "0"; - createJson["GracePeriod"] = 60; - createJson["InterestRate"] = 24346; - createJson["LateInterestRate"] = 65535; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const pseudoAcct = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return Account{lender}; - auto const brokerPseudo = brokerSle->at(sfAccount); - return Account("Broker pseudo-account", brokerPseudo); - }(); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); - auto const originalState = getCurrentState(env, broker, keylet); - verifyLoanStatus(originalState); - - Number const payment{3'269'349'176'470'588, -12}; - XRPAmount const payFee{ - baseFee * - ((payment / originalState.periodicPayment) / kLoanPaymentsPerFeeIncrement + 1)}; - auto loanPayTx = - env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), Fee(payFee)); - BEAST_EXPECT(to_string(payment) == "3269.349176470588"); - env(loanPayTx, Ter(tesSUCCESS)); - env.close(); - - auto const newState = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale)); - BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding); - BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale)); - BEAST_EXPECT( - isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale)); - } - - void - testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(FeatureBitset features) - { - // From FIND-010 - testcase << "xrpl::loanComputePaymentParts : valid total interest"; - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["CloseInterestRate"] = 47299; - createJson["ClosePaymentFee"] = "3985819770"; - createJson["InterestRate"] = 92; - createJson["LatePaymentFee"] = "3866894865"; - createJson["LoanOriginationFee"] = "0"; - createJson["LoanServiceFee"] = "2348810240"; - createJson["OverpaymentFee"] = 58545; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 1; - createJson["PrincipalRequested"] = "0.000763058"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson); - env.close(); - - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - loanPayTx["Amount"]["value"] = "0.000281284125490196"; - env(loanPayTx, Ter(tecINSUFFICIENT_PAYMENT)); - env.close(); - } - - void - testDosLoanPay(FeatureBitset features) - { - bool const feeCapped = features[fixCleanup3_1_3]; - - // From FIND-005 - testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped"); - - using namespace jtx; - using namespace std::chrono_literals; - using namespace lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3)); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(100'000'000))); - env(trust(borrower, iouAsset(100'000'000))); - env(pay(issuer, lender, iouAsset(10'000'000))); - env(pay(issuer, borrower, iouAsset(1'000))); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{3959'37, -2}; - auto const baseFee = env.current()->fees().base; - - auto const createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object), - kClosePaymentFee(0), - kGracePeriod(60), - kInterestRate(TenthBips32(20930)), - kLateInterestRate(TenthBips32(77049)), - kLatePaymentFee(0), - kLoanServiceFee(0), - kOverpaymentFee(TenthBips32(7)), - kOverpaymentInterestRate(TenthBips32(66653)), - kPaymentInterval(60), - kPaymentTotal(3239184)); - - // There are enough payments due on this loan that it only needs to be - // created once, and can be paid on multiple times. Just don't create a - // gazillion test cases. - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - env(createJson, Sig(sfCounterpartySignature, lender)); - env.close(); - - auto const roundedPayment = [&]() { - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining == 3239184); - BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); - - return roundToAsset( - iouAsset, - stateBefore.periodicPayment, - stateBefore.loanScale, - Number::RoundingMode::Upward); - }(); - - auto test = [&](int const payFactor, - int const feeFactor, - TER const expectedTer = tesSUCCESS) { - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184); - BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); - - Number const amount = roundedPayment * payFactor; - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount})); - XRPAmount const payFee{baseFee * feeFactor}; - env(loanPayTx, Ter(expectedTer), Fee(payFee)); - env.close(); - auto const expectedChange = isTesSuccess(expectedTer) - ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor) - : 0; - - auto const stateAfter = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange); - }; - - static constexpr std::int64_t kMaxFeeIncrements = - kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement; - - TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P; - - // * Amount well above threshold -> capped fee - // The original test case - way over the limit - more fee is always ok - test(1819878, 363976); - // The capped fee is only sufficient if the amendment is enabled. - test(1819878, kMaxFeeIncrements, failWithoutFix); - - // * Amount exactly at threshold -> capped fee - test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements); - // More fee is always ok - test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10); - - // * Amount below threshold -> normal calculation - test(1, 1); - test(kLoanPaymentsPerFeeIncrement * 2, 2); - test(0, 0, temBAD_AMOUNT); - test(0, 1, temBAD_AMOUNT); - // Fee difference rounds evenly - test( - kLoanMaximumPaymentsPerTransaction - 10, - ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1, - telINSUF_FEE_P); - test( - kLoanMaximumPaymentsPerTransaction - 10, - ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement)); - // More fee is always ok - test( - kLoanMaximumPaymentsPerTransaction - 10, - ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3); - // Fee rounds up - for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under) - { - test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P); - test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements); - } - // Only when you get one less fee increment can you pay less - test( - kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, - kMaxFeeIncrements - 1); - // And again, more fee is always ok. - test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements); - } - - void - testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(FeatureBitset features) - { - // From FIND-009 - testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid " - "rounded"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["ClosePaymentFee"] = "0"; - createJson["InterestRate"] = 24346; - createJson["LateInterestRate"] = 65535; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 60; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const baseFee = env.current()->fees().base; - - auto const stateBefore = getCurrentState(env, broker, keylet); - - { - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{3074'745'058'823'529, -12}; - BEAST_EXPECT(to_string(amount) == "3074.745058823529"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); - env.close(); - } - - { - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{6732'118'170'944'051, -12}; - BEAST_EXPECT(to_string(amount) == "6732.118170944051"); - XRPAmount const payFee{ - baseFee * - (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); - env.close(); - } - - auto const stateAfter = getCurrentState(env, broker, keylet); - // Total interest outstanding is non-negative - BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding); - // Principal paid is non-negative - BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding); - // Total value change is non-negative - BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue); - // Value delta is larger or same as principal delta (meaning - // non-negative interest paid) - BEAST_EXPECT( - (stateBefore.totalValue - stateAfter.totalValue) >= - (stateBefore.principalOutstanding - stateAfter.principalOutstanding)); - } - - void - testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(FeatureBitset features) - { - // From FIND-008 - testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace lending; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); - env(payIssuerTx); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; - { - auto const coverDepositValue = broker.asset(broker.params.coverDeposit * 10).value(); - env(loan_broker::coverDeposit(lender, broker.brokerID, coverDepositValue)); - env.close(); - } - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 3}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["ClosePaymentFee"] = "0"; - createJson["InterestRate"] = 12833; - createJson["LateInterestRate"] = 77048; - createJson["LatePaymentFee"] = "0"; - createJson["LoanOriginationFee"] = "218"; - createJson["LoanServiceFee"] = "0"; - createJson["PaymentInterval"] = 752; - createJson["PaymentTotal"] = 5678; - createJson["PrincipalRequested"] = "9924.81"; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const baseFee = env.current()->fees().base; - - auto const stateBefore = getCurrentState(env, broker, keylet); - BEAST_EXPECT(stateBefore.paymentRemaining == 5678); - BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); - - auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); - Number const amount{9924'81, -2}; - BEAST_EXPECT(to_string(amount) == "9924.81"); - XRPAmount const payFee{ - baseFee * (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; - loanPayTx["Amount"]["value"] = to_string(amount); - env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); - env.close(); - - auto const stateAfter = getCurrentState(env, broker, keylet); - BEAST_EXPECT( - stateAfter.paymentRemaining == - stateBefore.paymentRemaining - kLoanMaximumPaymentsPerTransaction); - } - - void - testLoanNextPaymentDueDateOverflow(FeatureBitset features) - { - // For FIND-013 - testcase << "Prevent nextPaymentDueDate overflow"; - - using namespace jtx; - using namespace std::chrono_literals; - using namespace lending; - Env env{*this, features}; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const iouAsset = issuer[iouCurrency_]; - auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); - env(trustLenderTx); - auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); - env(trustBorrowerTx); - auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000)); - env(payLenderTx); - auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000)); - env(payIssuerTx); - env.close(); - - BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}}; - BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - using timeType = decltype(sfNextPaymentDueDate)::type::value_type; - static_assert(std::is_same_v); - constexpr timeType kMaxTime = std::numeric_limits::max(); - static_assert(kMaxTime == 4'294'967'295); - - auto const baseJson = [&]() { - auto createJson = env.json( - set(borrower, broker.brokerID, Number{55524'81, -2}), - Fee(loanSetFee), - kClosePaymentFee(0), - kGracePeriod(LoanSet::kDefaultGracePeriod), - kInterestRate(TenthBips32(12833)), - kLateInterestRate(TenthBips32(77048)), - kLatePaymentFee(0), - kLoanOriginationFee(218), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson.removeMember(sfSequence.getJsonName()); - - return createJson; - }(); - - auto const baseFee = env.current()->fees().base; - - auto parentCloseTime = [&]() { - return env.current()->parentCloseTime().time_since_epoch().count(); - }; - auto maxLoanTime = [&]() { - auto const startDate = parentCloseTime(); - - BEAST_EXPECT(startDate >= 50); - - return kMaxTime - startDate; - }; - - { - // straight-up overflow: interval - auto const interval = maxLoanTime() + 1; - auto const total = 1; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // straight-up overflow: total - // min interval is 60 - auto const interval = 60; - auto const total = maxLoanTime() + 1; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // straight-up overflow: grace period - // min interval is 60 - auto const interval = maxLoanTime() + 1; - auto const total = 1; - auto const grace = interval; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - // The grace period can't be larger than the interval. - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Overflow with multiplication of a few large intervals - auto const interval = 1'000'000'000; - auto const total = 10; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Overflow with multiplication of many small payments - // min interval is 60 - auto const interval = 60; - auto const total = 1'000'000'000; - auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Overflow with an absurdly large grace period - // min interval is 60 - auto const total = 60; - auto const interval = (maxLoanTime() - total) / total; - auto const grace = interval; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); - env.close(); - } - { - // Start date when the ledger is closed will be larger - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const grace = 100; - auto const interval = maxLoanTime() - grace; - auto const total = 1; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); - env.close(); - - // The transaction is killed in the closed ledger - auto const meta = env.meta(); - if (BEAST_EXPECT(meta)) - { - BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED); - } - - // If the transaction had succeeded, the loan would exist - auto const loanSle = env.le(keylet); - // but it doesn't - BEAST_EXPECT(!loanSle); - } - { - // Start date when the ledger is closed will be larger - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; - auto const grace = 5'000; - auto const interval = kMaxTime - closeStartDate - grace; - auto const total = 1; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); - env.close(); - - // The transaction succeeds in the closed ledger - auto const meta = env.meta(); - if (BEAST_EXPECT(meta)) - { - BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); - } - - // This loan exists - auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); - BEAST_EXPECT(afterState.previousPaymentDate == 0); - BEAST_EXPECT(afterState.paymentRemaining == 1); - } - - { - // Ensure the borrower has funds to pay back the loan - env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2}))); - - // Start date when the ledger is closed will be larger - auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; - auto const grace = 5'000; - auto const maxLoanTime = kMaxTime - closeStartDate - grace; - auto const total = [&]() { - if (maxLoanTime % 5 == 0) - return 5; - if (maxLoanTime % 3 == 0) - return 3; - if (maxLoanTime % 2 == 0) - return 2; - return 0; - }(); - if (!BEAST_EXPECT(total != 0)) - return; - - auto const brokerState = env.le(keylet::loanBroker(broker.brokerID)); - // Intentionally shadow the outer values - auto const loanSequence = brokerState->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - auto const interval = maxLoanTime / total; - auto createJson = env.json( - baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); - - env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); - env.close(); - - // This loan exists - auto const beforeState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval); - BEAST_EXPECT(beforeState.previousPaymentDate == 0); - BEAST_EXPECT(beforeState.paymentRemaining == total); - BEAST_EXPECT(beforeState.periodicPayment > 0); - - // pay all but the last payment - { - NumberRoundModeGuard const mg{Number::RoundingMode::Upward}; - Number const payment = beforeState.periodicPayment * (total - 1); - XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)}; - STAmount const paymentAmount = - roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale); - auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee)); - env(loanPayTx, Ter(tesSUCCESS)); - env.close(); - } - - // The loan is on the last payment - auto const afterState = getCurrentState(env, broker, keylet); - BEAST_EXPECT(afterState.paymentRemaining == 1); - BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); - BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval); - } - } - - void - testRequireAuth() - { - testcase("Require Auth - Implicit Pseudo-account authorization"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Env env(*this); - - env.fund(XRP(100'000), issuer, lender, borrower); - env.close(); - - auto asset = MPTTester({ - .env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = kMptDexFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock, - .authHolder = true, - }); - - env(pay(issuer, lender, asset(5'000'000))); - BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); - - auto forUnauthAuth = [&](auto&& doTx) { - for (auto const flag : {tfMPTUnauthorize, 0u}) - { - asset.authorize({.account = issuer, .holder = borrower, .flags = flag}); - env.close(); - doTx(flag == 0); - env.close(); - } - }; - - // Can't create a loan if the borrower is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); - env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - err); - }); - - static constexpr std::uint32_t kLoanSequence = 1; - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, kLoanSequence); - - // Can't loan pay if the borrower is not authorized - forUnauthAuth([&](bool authorized) { - auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); - env(pay(borrower, loanKeylet.key, debtMaximumRequest), err); - }); - } - - void - testLendingCanTradeDisabledNoImpact() - { - testcase("Lending: CanTrade disabled has no impact"); - using namespace jtx; - using namespace loan; - using namespace loan_broker; - - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - MPTTester mpt( - {.env = env, - .issuer = issuer, - .holders = {lender, borrower}, - .flags = tfMPTCanTransfer | tfMPTCanLock, - .mutableFlags = tmfMPTCanEnableCanTrade}); - PrettyAsset const asset = mpt.issuanceID(); - env(pay(issuer, lender, asset(10'000'000))); - env(pay(issuer, borrower, asset(100'000))); - env.close(); - - auto const broker = createVaultAndBroker(env, asset, lender); - - // CanTrade is not set - env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); - env.close(); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - // New cover deposits still work. - env(coverDeposit(lender, broker.brokerID, asset(100))); - env.close(); - - // New loan issuance still works. - env(loan::set(borrower, broker.brokerID, 1'000), - Sig(sfCounterpartySignature, lender), - loanSetFee); - env.close(); - auto const loanKeylet = keylet::loan(broker.brokerID, 1); - BEAST_EXPECT(env.le(loanKeylet)); - - // Repayment still works. - env(pay(borrower, loanKeylet.key, asset(1'000))); - env.close(); - - // Cover withdrawal still works. - env(coverWithdraw(lender, broker.brokerID, asset(100))); - env.close(); - - // Enable CanTrade and verify the DEX path is restored. - mpt.set({.mutableFlags = tmfMPTSetCanTrade}); - env.close(); - - env(offer(lender, XRP(1), asset(10))); - env.close(); - } - -#if LOAN_TODO - void - testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features) - { - testcase("LoanPay full payment skips late penalties"); - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(100'000'000))); - env(trust(borrower, asset(100'000'000))); - env(pay(issuer, lender, asset(50'000'000))); - env(pay(issuer, borrower, asset(5'000'000))); - env.close(); - - BrokerInfo broker{createVaultAndBroker(env, asset, lender)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID)); - if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value()) - return; - - auto const loanSequence = brokerPreLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - Number const principal = asset(1'000).value(); - Number const serviceFee = asset(2).value(); - Number const lateFee = asset(5).value(); - Number const closeFee = asset(4).value(); - - env(set(borrower, broker.brokerID, principal), - Sig(sfCounterpartySignature, lender), - kLoanServiceFee(serviceFee), - kLatePaymentFee(lateFee), - kClosePaymentFee(closeFee), - kInterestRate(percentageToTenthBips(12)), - kLateInterestRate(percentageToTenthBips(24) / 10), - kCloseInterestRate(percentageToTenthBips(5)), - kPaymentTotal(12), - kPaymentInterval(600), - kGracePeriod(0), - Fee(loanSetFee)); - env.close(); - - auto state1 = getCurrentState(env, broker, loanKeylet); - if (!BEAST_EXPECT(state1.paymentRemaining > 1)) - return; - - using d = NetClock::duration; - using tp = NetClock::time_point; - auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}}; - env.close(overdueClose); - - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(brokerSle && loanSle)) - return; - - auto state = getCurrentState(env, broker, loanKeylet); - - TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)}; - TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)}; - TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)}; - - Number const closePaymentFeeRounded = - roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale); - Number const latePaymentFeeRounded = - roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); - - auto const roundedLoanState = constructLoanState( - state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); - Number const totalInterestOutstanding = roundedLoanState.interestDue; - - auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval); - auto const rawLoanState = computeTheoreticalLoanState( - env.current()->rules(), - state.periodicPayment, - periodicRate, - state.paymentRemaining, - managementFeeRate); - - auto const parentCloseTime = env.current()->parentCloseTime(); - auto const startDateSeconds = - static_cast(state.startDate.time_since_epoch().count()); - - Number const fullPaymentInterest = computeFullPaymentInterest( - rawLoanState.principalOutstanding, - periodicRate, - parentCloseTime, - state.paymentInterval, - state.previousPaymentDate, - startDateSeconds, - closeInterestRateValue); - - Number const roundedFullInterestAmount = - roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); - Number const roundedFullManagementFee = computeManagementFee( - broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale); - Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee; - - Number const trackedValueDelta = - state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding; - Number const untrackedManagementFee = - closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding; - Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding; - - Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee; - BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale)); - - auto const overdueSeconds = - parentCloseTime.time_since_epoch().count() - state.nextPaymentDate; - if (!BEAST_EXPECT(overdueSeconds > 0)) - return; - - Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds); - Number const lateInterestRaw = state.principalOutstanding * overdueRate; - Number const lateInterestRounded = - roundToAsset(broker.asset, lateInterestRaw, state.loanScale); - Number const lateManagementFeeRounded = computeManagementFee( - broker.asset, lateInterestRounded, managementFeeRate, state.loanScale); - Number const penaltyDue = - lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded; - BEAST_EXPECT(penaltyDue > Number{}); - - auto const balanceBefore = env.balance(borrower, broker.asset).number(); - - STAmount const paymentAmount{broker.asset.raw(), baseFullDue}; - env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment)); - env.close(); - - if (auto const meta = env.meta(); BEAST_EXPECT(meta)) - BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); - - auto const balanceAfter = env.balance(borrower, broker.asset).number(); - Number const actualPaid = balanceBefore - balanceAfter; - BEAST_EXPECT(actualPaid == baseFullDue); - - Number const expectedWithPenalty = baseFullDue + penaltyDue; - BEAST_EXPECT(expectedWithPenalty > actualPaid); - BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); - } - - void - testLoanCoverMinimumRoundingExploit(FeatureBitset features) - { - auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) { - testcase << "LoanBrokerCoverClawback drains cover via rounding" - << " principalRequested=" << to_string(principalRequest); - - using namespace jtx; - using namespace loan; - using namespace loan_broker; - - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000'000), issuer, lender, borrower); - env.close(); - - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - PrettyAsset const asset = issuer[iouCurrency]; - env(trust(lender, asset(2'000'0000))); - env(trust(borrower, asset(2'000'0000))); - env.close(); - - env(pay(issuer, lender, asset(2'000'0000))); - env.close(); - - BrokerParameters brokerParams{.debtMax = 0, .coverRateMin = TenthBips32{10'000}}; - BrokerInfo broker{createVaultAndBroker(env, asset, lender, brokerParams)}; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - auto createTx = env.jt( - set(borrower, broker.brokerID, principalRequest), - Sig(sfCounterpartySignature, lender), - loanSetFee, - kPaymentInterval(600), - kPaymentTotal(1), - kGracePeriod(60)); - env(createTx); - env.close(); - - auto const brokerBefore = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerBefore); - if (!brokerBefore) - return; - - Number const debtOutstanding = brokerBefore->at(sfDebtTotal); - Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); - - BEAST_EXPECT(debtOutstanding > Number{}); - BEAST_EXPECT(coverAvailableBefore > Number{}); - - log << "debt=" << to_string(debtOutstanding) - << " cover_available=" << to_string(coverAvailableBefore); - - env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); - env.close(); - - auto const brokerAfter = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerAfter); - if (!brokerAfter) - return; - - Number const debtAfter = brokerAfter->at(sfDebtTotal); - // the debt has not changed - BEAST_EXPECT(debtAfter == debtOutstanding); - - Number const coverAvailableAfter = brokerAfter->at(sfCoverAvailable); - - // since the cover rate min != 0, the cover available should not - // be zero - BEAST_EXPECT(coverAvailableAfter != Number{}); - }; - - // Call the lambda with different principal values - testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units - testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units - testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units - testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units - } -#endif - - void - testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features) - { - // --- PoC Summary ---------------------------------------------------- - // Scenario: Borrower makes one periodic payment early (before next due) - // so doPayment sets sfPreviousPaymentDueDate to the (future) - // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one - // interval. Borrower then immediately performs a full-payment - // (tfLoanFullPayment). Why it matters: Full-payment interest accrual - // uses - // delta = now - max(prevPaymentDate, startDate) - // with an unsigned clock representation (uint32). If prevPaymentDate is - // in the future, the subtraction underflows to a very large positive - // number. This inflates roundedFullInterest and total full-close due, - // and LoanPay applies the inflated valueChange to the vault - // (sfAssetsTotal), increasing NAV. - // -------------------------------------------------------------------- - testcase("PoC: Unsigned-underflow full-pay accrual after early periodic"); - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - Env env{*this, features}; - - Account const lender{"poc_lender4"}; - Account const borrower{"poc_borrower4"}; - env.fund(XRP(3'000'000), lender, borrower); - env.close(); - - PrettyAsset const asset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{}; - auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); - - // Create a 3-payment loan so full-payment path is enabled after 1 - // periodic payment. - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest = asset(1000).value(); - auto const originationFee = asset(0).value(); - auto const serviceFee = asset(1).value(); - auto const serviceFeePA = asset(1); - auto const lateFee = asset(0).value(); - auto const closeFee = asset(0).value(); - auto const interest = percentageToTenthBips(12); - auto const lateInterest = percentageToTenthBips(12) / 10; - auto const closeInterest = percentageToTenthBips(12) / 10; - auto const overpaymentInterest = percentageToTenthBips(12) / 10; - auto const total = 3u; - auto const interval = 600u; - auto const grace = 60u; - - auto createJtx = env.jt( - set(borrower, broker.brokerID, principalRequest, 0), - Sig(sfCounterpartySignature, lender), - kLoanOriginationFee(originationFee), - kLoanServiceFee(serviceFee), - kLatePaymentFee(lateFee), - kClosePaymentFee(closeFee), - kOverpaymentFee(percentageToTenthBips(5) / 10), - kInterestRate(interest), - kLateInterestRate(lateInterest), - kCloseInterestRate(closeInterest), - kOverpaymentInterestRate(overpaymentInterest), - kPaymentTotal(total), - kPaymentInterval(interval), - kGracePeriod(grace), - Fee(loanSetFee)); - - 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); - - env(createJtx); - env.close(); - - // Compute a regular periodic due and pay it early (before next due). - auto state = getCurrentState(env, broker, loanKeylet); - Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); - auto const components = xrpl::detail::computePaymentComponents( - env.current()->rules(), - asset.raw(), - state.loanScale, - state.totalValue, - state.principalOutstanding, - state.managementFeeOutstanding, - state.periodicPayment, - periodicRate, - state.paymentRemaining, - brokerParams.managementFeeRate); - STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()}; - // now < nextDue immediately after creation, so this is an early pay. - env(pay(borrower, loanKeylet.key, regularDue)); - env.close(); - - // Immediately attempt a full payoff. Compute the exact full-payment - // due to ensure the tx applies. - auto after = getCurrentState(env, broker, loanKeylet); - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID)); - BEAST_EXPECT(brokerSle2); - - auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{}; - auto const closeInterestRate = - loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{}; - auto const managementFeeRate = - brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{}; - - Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval); - // Accrued + prepayment-penalty interest based on current periodic - // schedule - auto const fullPaymentInterest = computeFullPaymentInterest( - xrpl::detail::loanPrincipalFromPeriodicPayment( - env.current()->rules(), - after.periodicPayment, - periodicRate2, - after.paymentRemaining), - periodicRate2, - env.current()->parentCloseTime(), - after.paymentInterval, - after.previousPaymentDate, - static_cast(after.startDate.time_since_epoch().count()), - closeInterestRate); - - // Round to asset scale and split interest/fee parts - auto const roundedInterest = - roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); - Number const roundedFullMgmtFee = - computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale); - Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; - - // Show both signed and unsigned deltas to highlight the underflow. - auto const nowSecs = - static_cast(env.current()->parentCloseTime().time_since_epoch().count()); - auto const startSecs = - static_cast(after.startDate.time_since_epoch().count()); - auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs); - auto const signedDelta = - static_cast(nowSecs) - static_cast(lastPaymentDate); - auto const unsignedDelta = static_cast(nowSecs - lastPaymentDate); - log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs - << " now=" << nowSecs << " signedDelta=" << signedDelta - << " unsignedDelta=" << unsignedDelta << std::endl; - - // Reference (clamped) computation: emulate a non-negative accrual - // window by clamping prevPaymentDate to 'now' for the full-pay path. - auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); - auto const fullPaymentInterestClamped = computeFullPaymentInterest( - xrpl::detail::loanPrincipalFromPeriodicPayment( - env.current()->rules(), - after.periodicPayment, - periodicRate2, - after.paymentRemaining), - periodicRate2, - env.current()->parentCloseTime(), - after.paymentInterval, - prevClamped, - startSecs, - closeInterestRate); - auto const roundedInterestClamped = - roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale); - Number const roundedFullMgmtFeeClamped = computeManagementFee( - asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale); - Number const roundedFullInterestClamped = - roundedInterestClamped - roundedFullMgmtFeeClamped; - STAmount const fullDueClamped{ - asset, - after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped + - closePaymentFee}; - - // Collect vault NAV before closing payment - auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{}; - auto const vaultKey2 = keylet::vault(vaultId2); - auto const vaultBefore = env.le(vaultKey2); - BEAST_EXPECT(vaultBefore); - Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{}; - - STAmount const fullDue{ - asset, - after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee + - closePaymentFee}; - - log << "PoC payoff: principalOutstanding=" << after.principalOutstanding - << " roundedFullInterest=" << roundedFullInterest - << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee - << " fullDue=" << to_string(fullDue.getJson()) << std::endl; - log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped - << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped - << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl; - - env(pay(borrower, loanKeylet.key, fullDue), Txflags(tfLoanFullPayment)); - env.close(); - - // Sanity: underflow present (unsigned delta very large relative to - // interval) - BEAST_EXPECT(unsignedDelta > after.paymentInterval); - - // Compare vault NAV before/after the full close - auto const vaultAfter = env.le(vaultKey2); - BEAST_EXPECT(vaultAfter); - if (vaultAfter) - { - auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); - log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore - << " assetsTotalAfter=" << assetsTotalAfter - << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl; - - // Value-based proof: underflowed window yields a payoff larger than - // the clamped (non-underflow) reference. - BEAST_EXPECT(fullDue == fullDueClamped); - if (fullDue > fullDueClamped) - log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; - } - - // Loan should be paid off - auto const finalLoan = env.le(loanKeylet); - BEAST_EXPECT(finalLoan); - if (finalLoan) - { - BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0); - } - } - - void - testDustManipulation(FeatureBitset features) - { - testcase("Dust manipulation"); - - using namespace jtx; - using namespace std::chrono_literals; - Env env{*this, features}; - - // Setup: Create accounts - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - Account const victim{"victim"}; - - env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim); - env.close(); - - // Step 1: Create vault with IOU asset - auto asset = issuer["USD"]; - env(trust(lender, asset(100000))); - env(trust(borrower, asset(100000))); - env(trust(victim, asset(100000))); - env(pay(issuer, lender, asset(50000))); - env(pay(issuer, borrower, asset(50000))); - env(pay(issuer, victim, asset(50000))); - env.close(); - - BrokerParameters const brokerParams{ - .vaultDeposit = 10000, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{1000}, - .coverRateLiquidation = TenthBips32{2500}}; - - auto broker = createVaultAndBroker(env, asset, lender, brokerParams); - - auto const loanKeyletOpt = [&]() -> std::optional { - auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::nullopt; - - // Broker has no loans - BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); - - // 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); - }(); - if (!loanKeyletOpt) - return; - - auto const& vaultKeylet = broker.vaultKeylet(); - - { - auto const vaultSle = env.le(vaultKeylet); - Number const assetsTotal = vaultSle->at(sfAssetsTotal); - Number const assetsAvail = vaultSle->at(sfAssetsAvailable); - - log << "Before loan creation:" << std::endl; - log << " AssetsTotal: " << assetsTotal << std::endl; - log << " AssetsAvailable: " << assetsAvail << std::endl; - log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; - - // before the loan the assets total and available should be equal - BEAST_EXPECT(assetsAvail == assetsTotal); - BEAST_EXPECT(assetsAvail == broker.asset(brokerParams.vaultDeposit).number()); - } - - Keylet const& loanKeylet = *loanKeyletOpt; - - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{100}, - .interest = TenthBips32{1922}, - .payTotal = 5816, - .payInterval = 86400 * 6, - .gracePd = 86400 * 5, - }; - - env(loanParams(env, broker)); - env.close(); - - // Wait for loan to be late enough to default - env.close(std::chrono::seconds(86400 * 40)); // 40 days - - { - auto const vaultSle = env.le(vaultKeylet); - Number const assetsTotal = vaultSle->at(sfAssetsTotal); - Number const assetsAvail = vaultSle->at(sfAssetsAvailable); - - log << "After loan creation:" << std::endl; - log << " AssetsTotal: " << assetsTotal << std::endl; - log << " AssetsAvailable: " << assetsAvail << std::endl; - log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; - - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - return; - auto const state = constructLoanState(loanSle); - - log << "Loan state:" << std::endl; - log << " ValueOutstanding: " << state.valueOutstanding << std::endl; - log << " PrincipalOutstanding: " << state.principalOutstanding << std::endl; - log << " InterestOutstanding: " << state.interestOutstanding() << std::endl; - log << " InterestDue: " << state.interestDue << std::endl; - log << " FeeDue: " << state.managementFeeDue << std::endl; - - // after loan creation the assets total and available should - // reflect the value of the loan - BEAST_EXPECT(assetsAvail < assetsTotal); - BEAST_EXPECT( - assetsAvail == - broker.asset(brokerParams.vaultDeposit - loanParams.principalRequest).number()); - BEAST_EXPECT( - assetsTotal == - broker.asset(brokerParams.vaultDeposit + state.interestDue).number()); - } - - // Step 7: Trigger default (dust adjustment will occur) - env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault)); - env.close(); - - // Step 8: Verify phantom assets created - { - auto const vaultSle2 = env.le(vaultKeylet); - Number const assetsTotal2 = vaultSle2->at(sfAssetsTotal); - Number const assetsAvail2 = vaultSle2->at(sfAssetsAvailable); - - log << "After default:" << std::endl; - log << " AssetsTotal: " << assetsTotal2 << std::endl; - log << " AssetsAvailable: " << assetsAvail2 << std::endl; - log << " Difference: " << (assetsTotal2 - assetsAvail2) << std::endl; - - // after a default the assets total and available should be equal - BEAST_EXPECT(assetsAvail2 == assetsTotal2); - } - } - - void - testRIPD3831(FeatureBitset features) - { - using namespace jtx; - - testcase("RIPD-3831"); - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - // .managementFeeRate = TenthBips16{5919}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{200'000, -6}, - .lateFee = Number{200, -6}, - .interest = TenthBips32{50'000}, - .payTotal = 10, - .payInterval = 150}; - - auto const assetType = AssetType::XRP; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - - using tp = NetClock::time_point; - using d = NetClock::duration; - - auto state = getCurrentState(env, broker, loanKeylet); - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); - } - - topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); - - using namespace jtx::loan; - - auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue))); - - { - auto const submitParam = to_string(jv); - auto const jr = env.rpc("submit", borrower.name(), submitParam); - - BEAST_EXPECT(jr.isMember(jss::result)); - auto const jResult = jr[jss::result]; - } - - env.close(); - - // Make sure the system keeps responding - env(noop(borrower)); - env.close(); - env(noop(issuer)); - env.close(); - env(noop(lender)); - env.close(); - } - - void - testRIPD3459(FeatureBitset features) - { - testcase("RIPD-3459 - LoanBroker incorrect debt total"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 200'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{500}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{100'000, -4}, - .interest = TenthBips32{100'000}, - .payTotal = 10}; - - auto const assetType = AssetType::MPT; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) - { - if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); - } - } - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - - if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) - { - if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) - { - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::kZero); - } - } - } - - void - testRIPD3901() - { - testcase("Crash with tfLoanOverpayment"); - using namespace jtx; - using namespace loan; - Account const lender{"lender"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const depositor{"depositor"}; - auto const txFee = Fee(XRP(100)); - - Env env(*this); - Vault const vault(env); - - env.fund(XRP(10'000), lender, issuer, borrower, depositor); - env.close(); - - auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()}); - env(tx, txFee); - env.close(); - - env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}), - txFee); - env.close(); - - auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender)); - - env(loan_broker::set(lender, vaultKeyLet.key), txFee); - env.close(); - - // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}}; - - STAmount const debtMaximumRequest = XRPAmount(200'000); - - env(set(borrower, brokerKeyLet.key, debtMaximumRequest), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(50'000)), - kPaymentTotal(2), - kPaymentInterval(150), - Txflags(tfLoanOverpayment), - txFee); - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); - - if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) - { - env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), - Txflags(tfLoanOverpayment), - txFee); - env.close(); - } - } - - void - testRoundingAllowsUndercoverage(FeatureBitset features) - { - testcase("Minimum cover rounding allows undercoverage (XRP)"); - - using namespace jtx; - using namespace loan_broker; - - Env env{*this, features}; - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(200'000), lender, borrower); - env.close(); - - // Vault with XRP asset - Vault const vault{env}; - auto [vaultCreate, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()}); - env(vaultCreate); - env.close(); - BEAST_EXPECT(env.le(vaultKeylet)); - - // Seed the vault with XRP so it can fund the loan principal - PrettyAsset const xrpAsset{xrpIssue(), 1}; - - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{10'000}, - .coverDeposit = 82, - }; - - auto const brokerInfo = createVaultAndBroker(env, xrpAsset, lender, brokerParams); - // Create a loan with principal 804 XRP and 0% interest (so - // DebtTotal increases by exactly 804) - env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()), - loan::kInterestRate(TenthBips32(0)), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 2)); - BEAST_EXPECT(env.ter() == tesSUCCESS); - env.close(); - - // Verify DebtTotal is exactly 804 - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - log << *brokerSle << std::endl; - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); - } - - // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable. - // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails. - env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()), - Ter(tecINSUFFICIENT_FUNDS)); - BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS); - env.close(); - - // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable. - // because that leaves sufficient cover, this succeeds - env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value())); - BEAST_EXPECT(env.ter() == tesSUCCESS); - env.close(); - - // Validate CoverAvailable == 80 XRP and DebtTotal remains 804 - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - log << *brokerSle << std::endl; - BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); - BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); - - // Also demonstrate that the true minimum (804 * 10%) exceeds 80 - auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000)); - log << "Theoretical min cover: " << theoreticalMin << std::endl; - BEAST_EXPECT(Number(804, -1) == theoreticalMin); - } - } - - void - testRIPD3902(FeatureBitset features) - { - testcase("RIPD-3902 - 1 IOU loan payments"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 10, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{1, 0}, - .interest = TenthBips32{100'000}, - .payTotal = 5, - .payInterval = 150, - .gracePd = 60}; - - auto const assetType = AssetType::IOU; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - PaymentParameters{.showStepBalances = true}); - } - - void - testBorrowerIsBroker() - { - testcase("Test Borrower is Broker"); - using namespace jtx; - using namespace loan; - Account const broker{"broker"}; - Account const issuer{"issuer"}; - Account const borrower{"borrower"}; - Account const depositor{"depositor"}; - - auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) { - Env env(*this); - Vault const vault(env); - - if (borrower == broker) - { - env.fund(XRP(10'000), broker, issuer, depositor); - } - else - { - env.fund(XRP(10'000), broker, borrower, issuer, depositor); - } - env.close(); - - auto const xrpFee = XRP(100); - auto const txFee = Fee(xrpFee); - - STAmount const debtMaximumRequest = getMaxDebt(env); - - auto const& asset = debtMaximumRequest.asset(); - auto const initialVault = asset(debtMaximumRequest * 100); - - auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset}); - env(tx, txFee); - env.close(); - - env(vault.deposit( - {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}), - txFee); - env.close(); - - auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker)); - - env(loan_broker::set(broker, vaultKeylet.key), txFee); - env.close(); - - auto const serviceFee = 101; - - env(set(broker, brokerKeylet.key, debtMaximumRequest), - kCounterparty(borrower), - Sig(sfCounterpartySignature, borrower), - kLoanServiceFee(serviceFee), - kPaymentTotal(10), - txFee); - env.close(); - - std::uint32_t const loanSequence = 1; - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); - - auto const brokerBalanceBefore = env.balance(broker, asset); - - if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle)) - { - auto const payment = loanSle->at(sfPeriodicPayment); - auto const totalPayment = payment + serviceFee; - env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee); - env.close(); - if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle)) - { - auto const expected = [&]() { - // The service fee is transferred to the broker if - // a borrower is not the broker - if (borrower != broker) - return brokerBalanceBefore.number() + serviceFee; - // Since a borrower is the broker, the payment is - // transferred to the Vault from the broker but not - // the service fee. - // If the asset is XRP then the broker pays the txFee. - if (asset.native()) - return brokerBalanceBefore.number() - payment - xrpFee.number(); - return brokerBalanceBefore.number() - payment; - }(); - BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value()); - } - } - }; - // Test when a borrower is the broker and is not to verify correct - // service fee transfer in both cases. - for (auto const& borrowerAcct : {broker, borrower}) - { - testLoanAsset( - [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct); - testLoanAsset( - [&](Env& env) -> STAmount { - auto const iou = issuer["USD"]; - env(trust(broker, iou(1'000'000'000))); - env(trust(depositor, iou(1'000'000'000))); - env(pay(issuer, broker, iou(100'000'000))); - env(pay(issuer, depositor, iou(100'000'000))); - env.close(); - return iou(200'000); - }, - borrowerAcct); - testLoanAsset( - [&](Env& env) -> STAmount { - MPTTester const mpt( - {.env = env, - .issuer = issuer, - .holders = {broker, depositor}, - .pay = 100'000'000}); - return mpt(200'000); - }, - borrowerAcct); - } - } - - void - testIssuerIsBorrower(FeatureBitset features) - { - testcase("RIPD-4096 - Issuer as borrower"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, .counter = issuer, .principalRequest = Number{10000}}; - - auto const assetType = AssetType::IOU; - - Env env{*this, features}; - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - issuer, - PaymentParameters{.showStepBalances = true}); - } - - void - testLimitExceeded() - { - testcase("RIPD-4125 - overpayment"); - - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = Number{200000, -6}, - .interest = TenthBips32{50000}, - .payTotal = 3, - .payInterval = 200, - .gracePd = 60, - .flags = tfLoanOverpayment, - }; - - auto const assetType = AssetType::XRP; - - Env env(*this, makeConfig(), all_, nullptr, beast::Severity::Warning); - - auto loanResult = - createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); - - if (BEAST_EXPECT(loanResult); !loanResult.has_value()) - return; - - auto broker = std::get(*loanResult); - auto loanKeylet = std::get(*loanResult); - auto pseudoAcct = std::get(*loanResult); - - VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); - - auto const state = getCurrentState(env, broker, loanKeylet); - - env(loan::pay( - borrower, - loanKeylet.key, - STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1}, - tfLoanOverpayment)); - env.close(); - - PaymentParameters const paymentParams{ - .showStepBalances = false, - .validateBalances = true, - }; - - makeLoanPayments( - env, - broker, - loanParams, - loanKeylet, - verifyLoanStatus, - issuer, - lender, - borrower, - paymentParams); - } - - void - testOverpaymentManagementFee(FeatureBitset features) - { - testcase("testOverpaymentManagementFee"); - - using namespace jtx; - using namespace loan; - - Env env{*this, features}; - - Account const lender{"lender"}, borrower{"borrower"}; - - env.fund(XRP(10'000'000), lender, borrower); - env.close(); - - PrettyAsset const asset{xrpIssue(), 1000}; - - auto const result = createVaultAndBroker( - env, - asset, - lender, - { - .vaultDeposit = asset(100'000).value(), - .managementFeeRate = TenthBips16(10'000), - }); - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - - auto const loanKeylet = keylet::loan( - result.brokerKeylet().key, (env.le(result.brokerKeylet()))->at(sfLoanSequence)); - env(loan::set( - borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment), - Sig(sfCounterpartySignature, lender), - loan::kPaymentInterval(86400 * 30), - loan::kPaymentTotal(3), - loan::kOverpaymentInterestRate(TenthBips32(percentageToTenthBips(20))), - loanSetFee); - - // From calculator - auto const expectedOverpaymentManagementFee = Number{33333, 0}; - auto const loanBrokerBalanceBefore = env.balance(lender); - - auto const loanPayFee = Fee(env.current()->fees().base * 2); - env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee); - env.close(); - - BEAST_EXPECTS( - env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee, - "overpayment management fee missmatch; expected:" + - to_string(expectedOverpaymentManagementFee) + - " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore)); - } - - void - testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features) - { - testcase << "LoanPay Broker Owner Missing Trustline (PoC)"; - using namespace jtx; - using namespace loan; - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - auto const iou = issuer["IOU"]; - Env env(*this, features); - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - // Set up trustlines and fund accounts - env(trust(broker, iou(20'000'000))); - env(trust(borrower, iou(20'000'000))); - env(pay(issuer, broker, iou(10'000'000))); - env(pay(issuer, borrower, iou(1'000))); - env.close(); - // 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); - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(iou(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - // Ensure broker has sufficient cover so brokerPayee == brokerOwner - // We need coverAvailable >= (debtTotal * coverRateMinimum) - // Deposit enough cover to ensure the fee goes to broker owner - // The default coverRateMinimum is 10%, so for a 10,000 loan we need - // at least 1,000 cover. Default cover is 1,000, so we add more to be - // safe. - auto const additionalCover = iou(50'000).value(); - env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover})); - env.close(); - // Verify broker owner has a trustline - auto const brokerTrustline = keylet::trustLine(broker, iou); - BEAST_EXPECT(env.le(brokerTrustline) != nullptr); - // Broker owner deletes their trustline - // First, pay any positive balance to issuer to zero it out - auto const brokerBalance = env.balance(broker, iou); - env(pay(broker, issuer, brokerBalance)); - env.close(); - // Remove the trustline by setting limit to 0 - env(trust(broker, iou(0))); - env.close(); - // Verify trustline is deleted - BEAST_EXPECT(env.le(brokerTrustline) == nullptr); - // Now borrower tries to make a payment - // We should get a tesSUCCESS instead of a tecNO_LINE. - env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); - env.close(); - // Verify trustline is still deleted - BEAST_EXPECT(env.le(brokerTrustline) == nullptr); - // Verify the service fee went to the broker pseudo-account - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); - auto const balance = env.balance(pseudo, iou); - // 1,000 default + 50,000 extra + 100 service fee from LoanPay - BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance))); - } - } - - void - testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features) - { - testcase << "LoanPay Broker Owner MPT unauthorized"; - using namespace jtx; - using namespace loan; - - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - - Env env{*this, features}; - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - - PrettyAsset const mpt{mptt.issuanceID()}; - - // Authorize broker and borrower - mptt.authorize({.account = broker}); - mptt.authorize({.account = borrower}); - - env.close(); - - // Fund accounts - env(pay(issuer, broker, mpt(10'000'000))); - env(pay(issuer, borrower, mpt(1'000))); - env.close(); - - // 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); - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(mpt(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - // Ensure broker has sufficient cover so brokerPayee == brokerOwner - // We need coverAvailable >= (debtTotal * coverRateMinimum) - // Deposit enough cover to ensure the fee goes to broker owner - // The default coverRateMinimum is 10%, so for a 10,000 loan we need - // at least 1,000 cover. Default cover is 1,000, so we add more to be - // safe. - auto const additionalCover = mpt(50'000).value(); - env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); - env.close(); - // Verify broker owner is authorized - auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); - BEAST_EXPECT(env.le(brokerMpt) != nullptr); - // Broker owner unauthorizes. - // First, pay any positive balance to issuer to zero it out - auto const brokerBalance = env.balance(broker, mpt); - env(pay(broker, issuer, brokerBalance)); - env.close(); - // Then, unauthorize the MPT. - mptt.authorize({.account = broker, .flags = tfMPTUnauthorize}); - env.close(); - // Verify the MPT is unauthorized. - BEAST_EXPECT(env.le(brokerMpt) == nullptr); - // Now borrower tries to make a payment - // We should get a tesSUCCESS instead of a tecNO_AUTH. - auto const borrowerBalance = env.balance(borrower, mpt); - env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); - env.close(); - // Verify the MPT is still unauthorized. - BEAST_EXPECT(env.le(brokerMpt) == nullptr); - // Verify the service fee went to the broker pseudo-account - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); - auto const balance = env.balance(pseudo, mpt); - // 1,000 default + 50,000 extra + 100 service fee from LoanPay - BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); - } - } - - void - testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) - { - testcase << "LoanPay Broker Owner without permissioned domain of the MPT"; - using namespace jtx; - using namespace loan; - - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - - Env env{*this, features}; - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - auto credType = "credential1"; - - pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}}; - env(pdomain::setTx(issuer, credentials1)); - env.close(); - - auto domainID = pdomain::getNewDomain(env.meta()); - - env(credentials::create(broker, issuer, credType)); - env(credentials::accept(broker, issuer, credType)); - env.close(); - - env(credentials::create(borrower, issuer, credType)); - env(credentials::accept(borrower, issuer, credType)); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({ - .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, - .domainID = domainID, - }); - - PrettyAsset const mpt{mptt.issuanceID()}; - - // Authorize broker and borrower - mptt.authorize({.account = broker}); - mptt.authorize({.account = borrower}); - - env.close(); - - // Fund accounts - env(pay(issuer, broker, mpt(10'000'000))); - env(pay(issuer, borrower, mpt(1'000))); - env.close(); - - // 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); - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(mpt(100).value()), - kPaymentInterval(100), - Fee(XRP(100))); - env.close(); - // Ensure broker has sufficient cover so brokerPayee == brokerOwner - // We need coverAvailable >= (debtTotal * coverRateMinimum) - // Deposit enough cover to ensure the fee goes to broker owner - // The default coverRateMinimum is 10%, so for a 10,000 loan we need - // at least 1,000 cover. Default cover is 1,000, so we add more to be - // safe. - auto const additionalCover = mpt(50'000).value(); - env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); - env.close(); - // Verify broker owner is authorized - auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); - BEAST_EXPECT(env.le(brokerMpt) != nullptr); - // Remove the credentials for the Broker owner. - // First, pay any positive balance to issuer to zero it out - auto const brokerBalance = env.balance(broker, mpt); - env(pay(broker, issuer, brokerBalance)); - env.close(); - - env(credentials::deleteCred(broker, broker, issuer, credType)); - env.close(); - - // Make sure the broker is not authorized to hold the MPT after we - // deleted the credentials - env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); - - // Now borrower tries to make a payment - // We should get a tesSUCCESS instead of a tecNO_AUTH. - auto const borrowerBalance = env.balance(borrower, mpt); - env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); - env.close(); - // Verify broker is still not authorized - env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); - // Verify the service fee went to the broker pseudo-account - if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - BEAST_EXPECT(brokerSle)) - { - Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); - auto const balance = env.balance(pseudo, mpt); - // 1,000 default + 50,000 extra + 100 service fee from LoanPay - BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); - } - } - - void - testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) - { - testcase << "LoanSet Broker Owner without permissioned domain of the MPT"; - using namespace jtx; - using namespace loan; - - Account const issuer("issuer"); - Account const borrower("borrower"); - Account const broker("broker"); - - Env env{*this, features}; - env.fund(XRP(20'000), issuer, broker, borrower); - env.close(); - - auto credType = "credential1"; - - pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}}; - env(pdomain::setTx(issuer, credentials1)); - env.close(); - - auto domainID = pdomain::getNewDomain(env.meta()); - - // Add credentials for the broker and borrower - env(credentials::create(broker, issuer, credType)); - env(credentials::accept(broker, issuer, credType)); - env.close(); - - env(credentials::create(borrower, issuer, credType)); - env(credentials::accept(borrower, issuer, credType)); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({ - .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, - .domainID = domainID, - }); - - PrettyAsset const mpt{mptt.issuanceID()}; - - // Authorize broker and borrower - mptt.authorize({.account = broker}); - mptt.authorize({.account = borrower}); - env.close(); - - // Fund accounts - env(pay(issuer, broker, mpt(10'000'000))); - env(pay(issuer, borrower, mpt(1'000))); - env.close(); - - // Create vault and broker - auto const brokerInfo = createVaultAndBroker(env, mpt, broker); - - // Remove the credentials for the Broker owner. - // Clear the balance first. - auto const brokerBalance = env.balance(broker, mpt); - env(pay(broker, issuer, brokerBalance)); - env.close(); - // Delete the credentials - env(credentials::deleteCred(broker, broker, issuer, credType)); - env.close(); - - // Create a loan, this should fail for tecNO_AUTH - env(set(borrower, brokerInfo.brokerID, 10'000), - Sig(sfCounterpartySignature, broker), - kLoanServiceFee(mpt(100).value()), - kPaymentInterval(100), - Fee(XRP(100)), - Ter(tecNO_AUTH)); - env.close(); - } - - void - testSequentialFLCDepletion(FeatureBitset features) - { - testcase << "First-Loss Capital Depletion on Sequential Defaults"; - - using namespace jtx; - using namespace loan; - using namespace loan_broker; - - Env env{*this, features}; - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrowerA{"borrowerA"}; - Account const borrowerB{"borrowerB"}; - - env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB); - env.close(); - - PrettyAsset const asset = xrpIssue(); - auto const vaultDepositAmount = - asset(200'000); // Enough for 2 x 50k loans plus interest/fees - - auto const brokerInfo = createVaultAndBroker( - env, - asset, - lender, - { - .vaultDeposit = vaultDepositAmount.value(), - .debtMax = 0, - .coverRateMin = TenthBips32(20000), // 20% - .coverDeposit = 21'000, - .managementFeeRate = TenthBips16(100), // 0.1% - .coverRateLiquidation = TenthBips32(100000), - }); - auto const brokerKeylet = brokerInfo.brokerKeylet(); - - // Create two identical loans: each 50,000 XRP principal (scaled down to - // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal - // + interest) Formula will calculate cover as: 100% × (20% × 100,000) = - // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first - // default - auto const principalAmount = Number(50'000); - auto const loanPaymentInterval = 2592000; // 30 days - auto const loanGracePeriod = 604800; // 7 days - - // Create Loan A - auto loanATx = env.jt( - set(borrowerA, brokerKeylet.key, principalAmount), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(500)), // 5% - kPaymentTotal(12), - loan::kPaymentInterval(loanPaymentInterval), - loan::kGracePeriod(loanGracePeriod), - Fee(XRP(10))); // Sufficient fee for multi-sig transaction - env(loanATx); - env.close(); - - auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1); - - // Create Loan B - auto loanBTx = env.jt( - set(borrowerB, brokerKeylet.key, principalAmount), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32(500)), // 5% - kPaymentTotal(12), - loan::kPaymentInterval(loanPaymentInterval), - loan::kGracePeriod(loanGracePeriod), - Fee(XRP(10))); // Sufficient fee for multi-sig transaction - env(loanBTx); - env.close(); - - auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2); - - auto loanASle = env.le(loanAKeylet); - if (!BEAST_EXPECT(loanASle)) - return; - - // Advance time past grace period for both loans to be defaultable - auto const loanANextDue = loanASle->at(sfNextPaymentDueDate); - auto const loanAGrace = loanASle->at(sfGracePeriod); - env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60}); - - env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - env.close(); - - // Verify Loan A is defaulted - loanASle = env.le(loanAKeylet); - if (!BEAST_EXPECT(loanASle)) - return; - BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault)); - BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0); - - // Check broker state after first default (from committed ledger) - auto brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal); - auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable); - - // DebtTotal should have decreased by Loan A's debt - BEAST_EXPECT(afterFirstDebtTotal == 50'134); - - // CoverAvailable should have decreased significantly - BEAST_EXPECT(afterFirstCoverAvailable == 946); - - env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - - brokerSle = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerSle)) - return; - auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal); - auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable); - - BEAST_EXPECT(afterSecondDebtTotal == 0); - - BEAST_EXPECT(afterSecondCoverAvailable == 0); - } - - void - testYieldTheftRounding(std::uint32_t flags) - { - testcase("Rounding manipulation does not permit yield theft"); - using namespace jtx; - using namespace loan; - - // 1. Setup Environment - Env env(*this, all_); - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1000), issuer, lender, borrower); - env.close(); - - // 2. Asset Selection - PrettyAsset const iou = issuer["USD"]; - env(trust(lender, iou(100'000'000))); - env(trust(borrower, iou(100'000'000))); - env(pay(issuer, lender, iou(100'000'000))); - env(pay(issuer, borrower, iou(100'000'000))); - env.close(); - - // 3. Create Vault and Broker with High Debt Limit (100M) - auto const brokerInfo = createVaultAndBroker( - env, - iou, - lender, - { - .vaultDeposit = 5'000'000, - .debtMax = Number{100'000'000}, - .coverDeposit = 500'000, - }); - auto const [currentSeq, vaultKeylet] = [&]() { - auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return std::make_tuple(0u, keylet::unchecked(beast::kZero)); - auto const currentSeq = brokerSle->at(sfLoanSequence); - auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID)); - return std::make_tuple(currentSeq, vaultKeylet); - }(); - - // 4. Loan Parameters (Attack Vector) - Number const principal = 1'000'000; - TenthBips32 const interestRate = TenthBips32{1}; // 0.001% - std::uint32_t const paymentInterval = 86400; - std::uint32_t const paymentTotal = 3650; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags), - Sig(sfCounterpartySignature, lender), - loan::kInterestRate(interestRate), - loan::kPaymentInterval(paymentInterval), - loan::kPaymentTotal(paymentTotal), - Fee(loanSetFee)); - env.close(); - - // --- RETRIEVE OBJECTS & SETUP ATTACK --- - - auto borrowerBalance = [&]() { return env.balance(borrower, iou); }; - auto const borrowerScale = static_cast(borrowerBalance()).exponent(); - - auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq); - auto const maybePeriodicPayment = [&]() -> std::optional { - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - return std::nullopt; - // Construct Payment - return STAmount{iou, loanSle->at(sfPeriodicPayment)}; - }(); - if (!maybePeriodicPayment) - return; - auto const periodicPayment = *maybePeriodicPayment; - auto const roundedPayment = - roundToScale(periodicPayment, borrowerScale, Number::RoundingMode::Upward); - - // ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution - STAmount const paymentBuffer{iou, Number(1, -9)}; - STAmount const attackPayment = periodicPayment + paymentBuffer; - - auto const maybeInitialVaultAssets = [&]() -> std::optional { - auto const vault = env.le(vaultKeylet); - if (!BEAST_EXPECT(vault)) - return std::nullopt; - return vault->at(sfAssetsTotal); - }(); - if (!maybeInitialVaultAssets) - return; - auto const initialVaultAssets = *maybeInitialVaultAssets; - - // 5. Execution Loop - int yieldTheftCount = 0; - auto previousAssetsTotal = initialVaultAssets; - - for (int i = 0; i < 100; ++i) - { - auto const balanceBefore = borrowerBalance(); - env(pay(borrower, loanKeylet.key, attackPayment, flags)); - env.close(); - auto const borrowerDelta = balanceBefore - borrowerBalance(); - BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum()); - - auto const loanSle = env.le(loanKeylet); - if (!BEAST_EXPECT(loanSle)) - break; - auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)}; - BEAST_EXPECT( - (roundToScale(updatedPayment, borrowerScale, Number::RoundingMode::Upward) == - roundedPayment)); - BEAST_EXPECT( - (updatedPayment == periodicPayment) || - (flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment)); - - auto const currentVaultSle = env.le(vaultKeylet); - if (!BEAST_EXPECT(currentVaultSle)) - break; - - auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal); - auto const delta = currentAssetsTotal - previousAssetsTotal; - - BEAST_EXPECT( - (delta == beast::kZero && borrowerDelta <= roundedPayment) || - (delta > beast::kZero && borrowerDelta > roundedPayment)); - - // If tx succeeded but Assets Total didn't change, interest was - // stolen. - if (delta == beast::kZero && borrowerDelta > roundedPayment) - { - yieldTheftCount++; - } - - previousAssetsTotal = currentAssetsTotal; - } - - BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount)); - } - - // Tests that vault withdrawals work correctly when the vault has unrealized - // loss from an impaired loan, ensuring the invariant check properly - // accounts for the loss. - void - testWithdrawReflectsUnrealizedLoss(FeatureBitset features) - { - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - testcase("Vault withdraw reflects sfLossUnrealized"); - - // Test constants - static constexpr std::int64_t kInitialFunding = 1'000'000; - static constexpr std::int64_t kLenderInitialIou = 5'000'000; - static constexpr std::int64_t kDepositorInitialIou = 1'000'000; - static constexpr std::int64_t kBorrowerInitialIou = 100'000; - static constexpr std::int64_t kDepositAmount = 5'000; - static constexpr std::int64_t kPrincipalAmount = 99; - static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000; - static constexpr std::uint32_t kLocalPaymentInterval = 600; - static constexpr std::uint32_t kLocalPaymentTotal = 2; - - Env env{*this, features}; - - // Setup accounts - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const depositorA{"lpA"}; - Account const depositorB{"lpB"}; - Account const borrower{"borrowerA"}; - - env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower); - env.close(); - - // Setup trust lines - PrettyAsset const iouAsset = issuer[iouCurrency_]; - env(trust(lender, iouAsset(10'000'000))); - env(trust(depositorA, iouAsset(10'000'000))); - env(trust(depositorB, iouAsset(10'000'000))); - env(trust(borrower, iouAsset(10'000'000))); - env.close(); - - // Fund accounts with IOUs - env(pay(issuer, lender, iouAsset(kLenderInitialIou))); - env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou))); - env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou))); - env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou))); - env.close(); - - // Create vault and broker, then add deposits from two depositors - auto const broker = createVaultAndBroker(env, iouAsset, lender); - Vault v{env}; - - env(v.deposit({ - .depositor = depositorA, - .id = broker.vaultKeylet().key, - .amount = iouAsset(kDepositAmount), - }), - Ter(tesSUCCESS)); - env(v.deposit({ - .depositor = depositorB, - .id = broker.vaultKeylet().key, - .amount = iouAsset(kDepositAmount), - }), - Ter(tesSUCCESS)); - env.close(); - - // Create a loan - auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID)); - if (!BEAST_EXPECT(sleBroker)) - return; - - auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence)); - - env(set(borrower, broker.brokerID, kPrincipalAmount), - Sig(sfCounterpartySignature, lender), - kPaymentTotal(kLocalPaymentTotal), - kPaymentInterval(kLocalPaymentInterval), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - // Impair the loan to create unrealized loss - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); - env.close(); - - // Verify unrealized loss is recorded in the vault - auto const vaultAfterImpair = env.le(broker.vaultKeylet()); - if (!BEAST_EXPECT(vaultAfterImpair)) - return; - - BEAST_EXPECT( - vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value()); - - // Helper to get share balance for a depositor - auto const shareAsset = vaultAfterImpair->at(sfShareMPTID); - auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t { - auto const token = env.le(keylet::mptoken(shareAsset, depositor.id())); - return token ? token->getFieldU64(sfMPTAmount) : 0; - }; - - // Verify both depositors have equal shares - auto const sharesLpA = getShareBalance(depositorA); - auto const sharesLpB = getShareBalance(depositorB); - BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor); - BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor); - BEAST_EXPECT(sharesLpA == sharesLpB); - - // Helper to attempt withdrawal - auto const attemptWithdrawShares = [&](Account const& depositor, - std::uint64_t shareAmount, - TER expected) { - STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)}; - env(v.withdraw( - {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}), - Ter(expected)); - env.close(); - }; - - // Regression test: Both depositors should successfully withdraw despite - // unrealized loss. Previously failed with invariant violation: - // "withdrawal must change vault and destination balance by equal - // amount". This was caused by sharesToAssetsWithdraw rounding down, - // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta - // when unrealized loss exists. - attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS); - attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS); - } - - // A residual overpayment can reduce the stored principal by one scale-unit - // *less* than computeOverpaymentComponents predicts, firing the - // "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment: - // - // trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding - // - // tryOverpayment re-amortizes the loan at the reduced principal, then - // re-derives the theoretical principal from the new periodic payment via - // (P * paymentFactor) / paymentFactor. That round-trip is not exact in - // Number's 19-digit arithmetic; a positive residual pushes the recomputed - // principal a hair above the exact grid point `oldPrincipal - delta`, and - // the Upward rounding in tryOverpayment then bumps it a full scale-unit - // higher. The principal therefore drops by `delta - 1 unit`, not `delta`. - // - // Concrete case (isolated, at the tryOverpayment level): - // A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10. - // After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of - // 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by - // 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10. - // - // With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact, - // on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the - // lossy (P*factor)/factor round-trip, so the assertion holds and the - // overpayment applies cleanly. The three "principal change agrees" / - // "interest paid agrees" / "principal payment matches" assertions are - // gated behind the same amendment, so without it they are disabled (the - // server does not abort) and the loan keeps the pre-amendment computation. - // - // The test runs the same scenario under both amendment settings and checks - // the stored principal against a ground-truth value derived independently of - // the loan-state computation under test. - void - testBugOverpaymentPrincipalChange() - { - testcase("bug: doOverpayment asserts 'principal change agrees'"); - - using namespace jtx; - using namespace loan; - using namespace xrpl::detail; - - struct Params - { - TenthBips32 interestRate; - TenthBips16 managementFeeRate; - std::uint32_t paymentTotal; - std::uint32_t paymentInterval; - std::int64_t principal; - Number overpayment; - TenthBips32 overpaymentInterestRate; - TenthBips32 overpaymentFeeRate; - std::optional vaultScale; - }; - - struct Result - { - Number principalOutstanding; // stored principal after the LoanPay - Number expectedNewPrincipal; // ground truth, independent of the fix - Number managementFeeChange; // managementFeeOutstanding after - before - Number unit; // one scale-unit at the loan scale - }; - - auto runScenario = [this](FeatureBitset features, Params const& p) -> Result { - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"vaultOwner"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - Asset const asset = iouAsset.raw(); - STAmount const iouLimit{asset, Number{9'999'999'999'999'999LL}}; - env(trust(lender, iouLimit)); - env(trust(borrower, iouLimit)); - env(pay(issuer, lender, iouAsset(1'000'000))); - env(pay(issuer, borrower, iouAsset(1'000'000))); - env.close(); - - auto const broker = createVaultAndBroker( - env, - iouAsset, - lender, - {.vaultDeposit = 900'000, - .debtMax = 0, - .managementFeeRate = p.managementFeeRate, - .vaultScale = p.vaultScale}); - - 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); - - env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment), - Sig(sfCounterpartySignature, lender), - kInterestRate(p.interestRate), - kPaymentTotal(p.paymentTotal), - kPaymentInterval(p.paymentInterval), - kGracePeriod(p.paymentInterval), - kOverpaymentFee(p.overpaymentFeeRate), - kOverpaymentInterestRate(p.overpaymentInterestRate), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - // The single LoanPay below makes one regular payment (the overpayment - // is smaller than one period) and leaves the residual as an - // overpayment. - auto const s = getCurrentState(env, broker, loanKeylet); - auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval); - auto const onePeriod = computePaymentComponents( - env.current()->rules(), - asset, - s.loanScale, - s.totalValue, - s.principalOutstanding, - s.managementFeeOutstanding, - s.periodicPayment, - periodicRate, - s.paymentRemaining, - p.managementFeeRate); - - // Ground truth: the stored principal must drop by exactly the regular - // payment's principal portion plus the overpayment's principal - // portion. computeOverpaymentComponents depends only on the - // overpayment amount and rates (not on the loan-state computation - // under test), so it is an independent oracle. Both components are - // computed under the same rules as the env so the payment factor - // matches. - auto const overpaymentComponents = computeOverpaymentComponents( - env.current()->rules(), - asset, - s.loanScale, - p.overpayment, - p.overpaymentInterestRate, - p.overpaymentFeeRate, - p.managementFeeRate); - Number const expectedNewPrincipal = s.principalOutstanding - - onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta; - - Number const managementFeeBefore = s.managementFeeOutstanding; - - STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment}; - env(pay(borrower, loanKeylet.key, payAmount), - Txflags(tfLoanOverpayment), - Ter(tesSUCCESS)); - env.close(); - - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - - return Result{ - .principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0, - .expectedNewPrincipal = expectedNewPrincipal, - .managementFeeChange = - (loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) - - managementFeeBefore, - .unit = Number{1, s.loanScale}}; - }; - - // Scenario 1: the original near-zero-rate principal reproduction - // (loanScale -10, no management fee). 0.049999998 is smaller than one - // period, so it stays a residual overpayment. - Params const principalCase{ - .interestRate = TenthBips32{1}, - .managementFeeRate = TenthBips16{0}, - .paymentTotal = 3, - .paymentInterval = 60, - .principal = 100, - .overpayment = Number{49999998, -9}, - .overpaymentInterestRate = TenthBips32{1000}, - .overpaymentFeeRate = TenthBips32{1000}, - .vaultScale = 1}; - - // With fixCleanup3_2_0 the stored principal lands exactly on the - // ground-truth grid point: it is reduced by exactly the overpayment's - // principal portion. This is the key correctness check: if the principal - // pin were removed (even with the assertions still gated off), the lossy - // (P * factor) / factor round-trip would leave the principal one - // scale-unit high and this would fail. - Result const fixed = runScenario(all_, principalCase); - BEAST_EXPECTS( - fixed.principalOutstanding == fixed.expectedNewPrincipal, - "fixed principal " + to_string(fixed.principalOutstanding) + " != expected " + - to_string(fixed.expectedNewPrincipal)); - - // Without the amendment the loan amortizes with the catastrophically - // cancelling near-zero payment factor, so its schedule (and ground truth) - // differ from the fixed case; the gated assertions keep the server from - // aborting and the overpayment still lands exactly on that schedule. - Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase); - BEAST_EXPECTS( - legacy.principalOutstanding == legacy.expectedNewPrincipal, - "legacy principal " + to_string(legacy.principalOutstanding) + " != expected " + - to_string(legacy.expectedNewPrincipal)); - - // Scenario 2: a normal-rate loan with a 10% management fee. At a normal - // rate the payment factor is identical across the amendment, so toggling - // fixCleanup3_2_0 isolates the fix. This overpayment (found by search) - // lands on a state where both the principal and the management fee differ - // by one scale-unit between the fixed and legacy paths. - Params const feeCase{ - .interestRate = TenthBips32{10000}, - .managementFeeRate = TenthBips16{10000}, - .paymentTotal = 6, - .paymentInterval = 30u * 24 * 60 * 60, - .principal = 1000, - .overpayment = Number{214367363, -10}, - .overpaymentInterestRate = TenthBips32{0}, - .overpaymentFeeRate = TenthBips32{0}, - .vaultScale = std::nullopt}; - - Result const feeFixed = runScenario(all_, feeCase); - Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase); - - // With the fix the principal is the exact reduction; without it the lossy - // (P * factor) / factor round-trip leaves it one scale-unit high. - BEAST_EXPECTS( - feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal, - "fee-case fixed principal " + to_string(feeFixed.principalOutstanding) + - " != expected " + to_string(feeFixed.expectedNewPrincipal)); - BEAST_EXPECTS( - feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit, - "fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) + - " != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit)); - - // Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee - // outstanding drops. - // - // Unlike the principal that is already at the correct precision, the re-amortized - // management fee is tenthBipsOfValue of the new schedule's gross interest, which depends - // on the recomputed periodic payment. So the expected change below is a pinned constant - // captured from a passing run a magic value only because there is nothing simpler to - // compare against. - // - // At the integration level, toggling the amendment also changes the regular payment's - // rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee - // fix. - BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange); - BEAST_EXPECTS( - (feeFixed.managementFeeChange == Number{-8219709543, -10}), - "fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange)); - } - - // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero - // rate). At such a near-zero rate the closed-form payment factor - // (1 + r)^n - 1 cancels catastrophically. - // - // Without fixCleanup3_2_0 the resulting amortization is degenerate and the - // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the - // amendment, computePowerMinusOneHybrid uses a numerically-stable series - // expansion, so the loan is created and the scheduled payments - // (2 * periodicPayment) cover the principal — no economic underpayment - // (yield theft). - // - // The test runs the same LoanSet under both amendment settings and pins the - // exact outcome for each. - void - testLoanSetNearZeroInterestRateSucceeds() - { - testcase("LoanSet near-zero interest rate covers principal"); - - using namespace jtx; - using namespace loan; - - Number const principalRequested{1000}; - - struct Result - { - TER ter = tesSUCCESS; - bool created = false; - std::int32_t loanScale = 0; - Number principal; - Number totalValue; - Number managementFee; - Number periodicPayment; - }; - - auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result { - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"vaultOwner"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}}; - env(trust(lender, iouLimit)); - env(trust(borrower, iouLimit)); - env(pay(issuer, lender, iouAsset(1'000'000))); - env(pay(issuer, borrower, iouAsset(1'000'000))); - env.close(); - - auto const broker = createVaultAndBroker( - env, - iouAsset, - lender, - {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}}); - - 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); - - env(set(borrower, broker.brokerID, principalRequested), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32{1}), - kPaymentTotal(2), - kPaymentInterval(400), - Fee(env.current()->fees().base * 2), - Ter(expectedTer)); - env.close(); - - Result r; - r.ter = env.ter(); - if (auto const loanSle = env.le(loanKeylet)) - { - r.created = true; - r.loanScale = loanSle->at(sfLoanScale); - r.principal = loanSle->at(sfPrincipalOutstanding); - r.totalValue = loanSle->at(sfTotalValueOutstanding); - r.managementFee = loanSle->at(sfManagementFeeOutstanding); - r.periodicPayment = loanSle->at(sfPeriodicPayment); - } - return r; - }; - - Result const fixed = runScenario(all_, tesSUCCESS); - Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS); - - // Without the amendment, the catastrophically-cancelling closed-form - // payment factor produces a degenerate amortization that fails - // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no - // loan is created. - BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS); - BEAST_EXPECT(!legacy.created); - - // With the amendment the stable series expansion produces a valid loan - // at loanScale -10. - BEAST_EXPECT(fixed.ter == tesSUCCESS); - BEAST_EXPECT(fixed.created); - BEAST_EXPECT(fixed.loanScale == -10); - BEAST_EXPECT(fixed.principal == principalRequested); - BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10})); - BEAST_EXPECT(fixed.managementFee == beast::kZero); - - // Periodic payment from the numerically-stable series expansion, and the - // scheduled total (2 * periodicPayment) which exceeds the 1000 principal - // — no economic underpayment / yield theft. - BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16})); - BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15})); - BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested); - } - - // An overpayment whose residual amount has more precision than loanScale - // fires the isRounded(asset, overpayment, loanScale) assertion in - // computeOverpaymentComponents (and a downstream "interest paid agrees" - // assertion in doOverpayment). fixCleanup3_2_0 rounds the residual down - // to loanScale before passing it in. The pre-amendment path can't be - // tested here because the assertion fires in Debug builds and aborts - // the test process — see the PR description for context. - void - testBugOverpayUnroundedAmount() - { - testcase("bug: computeOverpaymentComponents isRounded assertion"); - - using namespace jtx; - using namespace loan; - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"vaultOwner"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}}; - env(trust(lender, iouLimit)); - env(trust(borrower, iouLimit)); - env(pay(issuer, lender, iouAsset(1'000'000))); - env(pay(issuer, borrower, iouAsset(1'000'000))); - env.close(); - - auto const broker = createVaultAndBroker( - env, - iouAsset, - lender, - {.vaultDeposit = 100'000, - .debtMax = 5000, - .managementFeeRate = TenthBips16{1000}, - .vaultScale = 1}); - - auto const sleBroker = env.le(broker.brokerKeylet()); - if (!BEAST_EXPECT(sleBroker)) - return; - auto const loanSequence = sleBroker->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - using namespace loan; - env(set(borrower, broker.brokerID, Number{1000}, tfLoanOverpayment), - Sig(sfCounterpartySignature, lender), - kInterestRate(TenthBips32{10000}), - kPaymentTotal(12), - kPaymentInterval(60), - kGracePeriod(60), - kOverpaymentFee(TenthBips32{1000}), - kOverpaymentInterestRate(TenthBips32{1000}), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - // periodic * 1.5 at 15-sig-digit precision: 125.000154585042. This - // has too many digits to round cleanly to loanScale=-10, so the - // overpayment residual fails the isRounded check. - STAmount const payAmount{iouAsset.raw(), Number{125'000'154'585'042LL, -12}}; - env(pay(borrower, loanKeylet.key, payAmount), Txflags(tfLoanOverpayment), Ter(tesSUCCESS)); - env.close(); - } - - // Regression for the dual-rounding fix at coarse (integer-MPT) scale. - // - // Loan: P=1, r=50% (50000 tenth-bips), n=3, yearly interval. The - // amortization schedule produces a fractional principal - // (~0.47) which under round-to-nearest collapses to 0 in a single - // step, causing `doPayment`'s strict `>` assertion on principal to - // fire mid-loan. With fixCleanup3_2_0 enabled, principal is rounded - // upward (sticks at 1 across the first two periods) and only clears - // in the final payment. - // - // The test pays one period at a time across three LoanPay - // transactions and verifies the loan completes (paymentRemaining=0) - // with totals matching the loan's economics (1 principal + 2 interest). - void - testIntegerScalePrincipalSticks(FeatureBitset features) - { - // Without fixCleanup3_2_0, this behavior will abort the server, so - // don't run without it. - if (!features[fixCleanup3_2_0]) - return; - - testcase("edge: integer MPT principal stuck mid-loan completes via final"); - - using namespace jtx; - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(100'000), issuer, lender, borrower); - env.close(); - - MPTTester mptt{env, issuer, kMptInitNoFund}; - mptt.create({.maxAmt = 100'000, .flags = tfMPTCanTransfer}); - PrettyAsset const asset{mptt.issuanceID()}; - - mptt.authorize({.account = lender}); - mptt.authorize({.account = borrower}); - - env(pay(issuer, lender, asset(10'000))); - env(pay(issuer, borrower, asset(10'000))); - env.close(); - - Vault const vault{env}; - auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); - env(vaultTx); - env.close(); - - env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)})); - env.close(); - - auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); - env(loan_broker::set(lender, vaultKeylet.key), - loan_broker::kDebtMaximum(Number{100}), - Fee(env.current()->fees().base * 2)); - env.close(); - - auto const brokerStateBefore = env.le(brokerKeylet); - if (!BEAST_EXPECT(brokerStateBefore)) - return; - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); - - env(loan::set(borrower, brokerKeylet.key, Number{1}), - Sig(sfCounterpartySignature, lender), - loan::kInterestRate(TenthBips32{50'000}), - loan::kPaymentTotal(3), - loan::kPaymentInterval(31'536'000), - Fee(env.current()->fees().base * 2)); - env.close(); - - auto const borrowerStart = env.balance(borrower, asset).value(); - - // Three separate periodic payments of 1 each. Expected per-period - // evolution at integer MPT scale (TVO = PO + interestDue + - // managementFeeDue): - // start: PO=1, TVO=3, paymentRemaining=3 - // after pay #1: PO=1, TVO=2, paymentRemaining=2 (principal sticks) - // after pay #2: PO=1, TVO=1, paymentRemaining=1 (principal sticks) - // after pay #3: PO=0, TVO=0, paymentRemaining=0 (final clears) - std::array const expectedPO{Number{1}, Number{1}, Number{0}}; - std::array const expectedTVO{Number{2}, Number{1}, Number{0}}; - std::array const expectedRemaining{2, 1, 0}; - - for (int i = 0; i < 3; ++i) - { - env(loan::pay(borrower, loanKeylet.key, asset(1)), Ter(tesSUCCESS)); - env.close(); - - auto const sle = env.le(loanKeylet); - if (!BEAST_EXPECT(sle)) - return; - BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]); - BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]); - BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]); - } - - // Borrower paid 3 total regardless of fee split (1 principal + 2 - // interest+fee, matching loan economics). - auto const borrowerEnd = env.balance(borrower, asset).value(); - BEAST_EXPECT(borrowerStart - borrowerEnd == asset(3).value()); - } - - // A near-zero interest rate on a 100 USD loan - // produces total interest of ~6 units at loanScale -9. Numerical error - // in the amortization formula pushes the theoretical principal above - // the theoretical value, producing a negative theoretical interest. - // The payment delta then exceeds the actual outstanding interest, - // violating XRPL_ASSERT_PARTS in computePaymentComponents. - void - testBugInterestDueDeltaCrash() - { - testcase("bug: LoanPay asserts 'interest due delta' on near-zero rate"); - - using namespace jtx; - using namespace std::chrono_literals; - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - env(trust(lender, iouAsset(1'000'000'000))); - env(trust(borrower, iouAsset(1'000'000'000))); - env(pay(issuer, lender, iouAsset(5'000'000))); - env(pay(issuer, borrower, iouAsset(5'000'000))); - env.close(); - - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000'000, - .debtMax = 1'000'000, - .coverRateMin = TenthBips32{0}, - .coverDeposit = 0, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - using namespace loan; - - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{100}; - - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - - createJson["InterestRate"] = 1; // minimum non-zero rate - createJson["PaymentTotal"] = 3; - createJson["PaymentInterval"] = 600; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const keylet = keylet::loan(broker.brokerID, loanSequence); - - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - // For principal=100, n=3 the amortization schedule produces a - // periodic payment ≈ 33.33 USD. We pay 35 USD, which is more than - // one period's worth — enough for the LoanPay path to enter - // computePaymentComponents and reach the assertion that fires - // when the bug is present. With the fix, the tx applies cleanly. - env(pay(borrower, keylet.key, iouAsset(35)), Ter(tesSUCCESS)); - env.close(); - } - - // Integration test: full lifecycle of a $1B loan in the bug regime. - // Verifies that the vault collects the economically-correct interest - // income and that conservation holds at the trust-line level. - // - // Pre-fix (closed-form `power(1+r, n) - 1`): vault collected only - // ~$0.058 per $1B due to cancellation of `(1+r)^n - 1` at r*n ~ 5.7e-10. - // Post-fix (hybrid binomial path): vault collects ~$0.38 per $1B, - // matching the value computed independently with arbitrary-precision - // Decimal arithmetic. - void - testFullLifecycleVaultPnLNearZeroRate() - { - testcase("integration: full loan lifecycle, vault interest at near-zero rate"); - - using namespace jtx; - using namespace jtx::loan; - using namespace std::chrono_literals; - Env env(*this, all_); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000), issuer, lender, borrower); - env.close(); - env(fset(issuer, asfDefaultRipple)); - env.close(); - - PrettyAsset const iouAsset = issuer["USD"]; - STAmount const trustLimit{iouAsset.raw(), Number{1, 17}}; - env(trust(lender, trustLimit)); - env(trust(borrower, trustLimit)); - env.close(); - env(pay(issuer, lender, iouAsset(5'000'000'000LL))); - env(pay(issuer, borrower, iouAsset(5'000'000'000LL))); - env.close(); - - auto usdBalance = [&](Account const& a) { - return env.balance(a, iouAsset.raw().get()).value(); - }; - STAmount const borrowerStartBal = usdBalance(borrower); - - BrokerParameters const brokerParams{ - .vaultDeposit = Number{2, 9}, - .debtMax = Number{0}, - .coverRateMin = TenthBips32{0}, - .coverDeposit = 0, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; - - auto const vaultBefore = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultBefore); - Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable); - - // Loan: $1B principal, 3 payments, 600s interval, rate=1 TenthBips32. - auto const loanSetFee = Fee(env.current()->fees().base * 2); - Number const principalRequest{1, 9}; - auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest), - Fee(loanSetFee), - Json(sfCounterpartySignature, json::ValueType::Object)); - createJson["InterestRate"] = 1; - createJson["PaymentTotal"] = 3; - createJson["PaymentInterval"] = 600; - - auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); - auto const loanSequence = brokerStateBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); - env(createJson, Ter(tesSUCCESS)); - env.close(); - - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - Number const expectedTotalInterest = - loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding); - - env(pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfter = env.le(broker.vaultKeylet()); - Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable); - Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore; - - STAmount const borrowerEndBal = usdBalance(borrower); - STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal; - - // Self-consistency: vault gained exactly the expected interest - // computed at LoanSet, and the borrower's outflow matches. - BEAST_EXPECT(vaultGain == expectedTotalInterest); - BEAST_EXPECT(Number(borrowerNetOut) == expectedTotalInterest); - - // Mathematical correctness: the total interest for this loan - // configuration is 0.38051750382930729983, calculated - // independently using 50-digit Decimal arithmetic (no - // cancellation possible at that precision). At Number's 19-digit - // mantissa this rounds to 0.38051750382930729 — the literal - // below. The vault's actual gain must agree to within - // sub-microcent precision. - Number const decimalReference{38051750382930729LL, -17}; - Number const tolerance{1, -6}; // 1e-6 USD = sub-microcent - Number const error = abs(vaultGain - decimalReference); - BEAST_EXPECTS( - error < tolerance, - "vault gain " + to_string(vaultGain) + " differs from Decimal reference " + - to_string(decimalReference) + " by " + to_string(error) + " — exceeds tolerance " + - to_string(tolerance)); - } - - // Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the - // same vault-scale minimum cover when fixCleanup3_2_0 is enabled. - // Before the amendment, each transactor computed its minimum cover at a - // different precision (loanScale, debtScale, or the raw unrounded - // tenthBipsOfValue), which could lead to inconsistent decisions for the - // same broker state. After the amendment all three use - // minimumBrokerCover at vaultScale. - void - testMinimumBrokerCoverConsistency(FeatureBitset features) - { - using namespace jtx; - using namespace loan; - using namespace loan_broker; - - bool const withAmendment = features[fixCleanup3_2_0]; - - struct Ctx - { - jtx::Account issuer; - jtx::Account lender; - jtx::Account borrower; - jtx::PrettyAsset iou; - BrokerInfo broker; - BrokerParameters brokerParams; - }; - - // Shared setup, parametrized by vaultDeposit (the only varying setup - // field across the three scenarios). Each call runs in its own Env - // so multiple invocations within one scenario cannot interfere. - // The caller is responsible for invoking testcase(...) before the - // first runTest call of each scenario. - auto runTest = [&](Number vaultDeposit, auto&& body) { - Env env(*this, features); - - Account const issuer{"issuer"}; - Account const lender{"lender"}; - Account const borrower{"borrower"}; - - env.fund(XRP(1'000'000'000), issuer, lender, borrower); - env.close(); - - // Enable clawback on the issuer *before* any trust lines exist - // (asfAllowTrustLineClawback requires an empty owner directory). - env(fset(issuer, asfAllowTrustLineClawback)); - env.close(); - - PrettyAsset const iou = issuer[iouCurrency_]; - env(trust(lender, iou(1'000'000'000))); - env(trust(borrower, iou(1'000'000'000))); - env.close(); - env(pay(issuer, lender, iou(100'000'000))); - env(pay(issuer, borrower, iou(100'000'000))); - env.close(); - - // 13.37% — non-round rate produces a messier minimum. - BrokerParameters const brokerParams{ - .vaultDeposit = vaultDeposit, - .debtMax = 0, - .coverRateMin = TenthBips32{13'370}, - .coverDeposit = 5'000, - .managementFeeRate = TenthBips16{500}}; - - BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams); - - body( - env, - Ctx{.issuer = issuer, - .lender = lender, - .borrower = borrower, - .iou = iou, - .broker = broker, - .brokerParams = brokerParams}); - }; - - // Scenario 1 — LoanPay - // - // Verify that LoanPay's minimum cover check uses vault scale (not - // loan scale). Before the amendment, different loans could produce - // different fee routing decisions for the same broker-level state. - // Small vault deposit => vaultScale = -12. - testcase("LoanPay minimum cover scale consistency"); - { - struct LoanKeylets - { - Keylet tiny; - Keylet big; - }; - - // Create the tiny + big loans and reduce cover via clawback so - // that subsequent LoanPay calls hit the minimum-cover boundary. - // Used by the two pay-and-check sub-tests below so each can run - // in its own Env. - auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional { - Asset const asset{c.iou}; - - // Create the TINY loan first (while vaultScale is still - // small). principal 0.01, 0% interest, 1 payment => - // loanScale = vaultScale. - auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle1)) - return std::nullopt; - auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence); - auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq); - - env(set(c.borrower, c.broker.brokerID, Number{1, -2}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // Create the BIG loan second. 100% annual interest over 20 - // payments pushes totalValueOutstanding high enough that - // loanScale > vaultScale. - auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle2)) - return std::nullopt; - auto const bigLoanSeq = brokerSle2->at(sfLoanSequence); - auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq); - - env(set(c.borrower, c.broker.brokerID, Number{500}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{100'000}), - kPaymentTotal(20), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // The tiny loan's scale is frozen at the vault's pre-big-loan - // scale, so it is strictly smaller than the big loan's. - // After the big loan is created the vault absorbs its value, - // pushing vaultScale up to match bigLoanScale. - auto const tinyLoanSle = env.le(tinyLoanKeylet); - auto const bigLoanSle = env.le(bigLoanKeylet); - auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); - if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) || - !BEAST_EXPECT(vaultSle)) - return std::nullopt; - if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) || - !BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) || - !BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11)) - return std::nullopt; - - // Use issuer clawback to reduce cover to the minimum the - // clawback transactor allows. Compute the amount as - // initialCover - expectedCoverAfter so we exercise the exact - // clawback rather than relying on the transactor to clip - // down. - // - // Before the amendment the clawback minimum is the - // *unrounded* tenthBipsOfValue — strictly less than the - // rounded-at-vaultScale minimum LoanPay uses for the big - // loan. After the amendment both clawback and LoanPay use - // the same rounded minimum (via minimumBrokerCover), so - // cover lands exactly at that threshold. - Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} - : Number{1330651855688458000, -15}; - Number const clawbackAmount = - Number{c.brokerParams.coverDeposit} - expectedCoverAfter; - - env(coverClawback(c.issuer), - kLoanBrokerId(c.broker.brokerID), - kAmount(STAmount{asset, clawbackAmount})); - env.close(); - - auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle) || - !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter)) - return std::nullopt; - - return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet}; - }; - - // Pay one loan and report whether the fee went to the broker's - // pseudo account (the fallback when cover < minimum) rather - // than to the owner. - auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool { - Asset const asset{c.iou}; - auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); - if (!BEAST_EXPECT(brokerSle)) - return false; - auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount)); - auto const pseudoBefore = env.balance(pseudoAcct, c.iou); - - auto const payLoan = env.le(loanKeylet); - if (!BEAST_EXPECT(payLoan)) - return false; - auto const periodicPayment = payLoan->at(sfPeriodicPayment); - auto const serviceFee = payLoan->at(sfLoanServiceFee); - std::int32_t const loanScale = payLoan->at(sfLoanScale); - - auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale); - auto const payAmt = STAmount{asset, payment + serviceFee}; - - env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10))); - env.close(); - - auto const pseudoAfter = env.balance(pseudoAcct, c.iou); - return pseudoAfter.number() > pseudoBefore.number(); - }; - - // Pay the BIG loan in its own Env so its outcome cannot affect - // the TINY-loan check. With the fix, LoanPay and clawback use - // the same vaultScale minimum (cover == minAtVaultScale => - // fee to owner). Without the fix, LoanPay uses bigLoanScale=-11, - // rounds up to a larger minimum than what clawback used => - // cover < min => fee to pseudo. - runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { - auto const loans = setupLoansAndClawback(env, c); - if (!loans) - return; - BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment); - }); - - // Pay the TINY loan in its own Env. Fee goes to the owner - // either way: - // - With the fix: LoanPay uses vaultScale=-11 (same as - // clawback) => owner. - // - Without the fix: LoanPay uses tinyLoanScale=-12, rounds - // up at -12 (a no-op) => min == cover => owner. - runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { - auto const loans = setupLoansAndClawback(env, c); - if (!loans) - return; - BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny)); - }); - } - - // Scenario 2 — LoanBrokerCoverWithdraw - // - // Verify that CoverWithdraw's minimum cover check uses vault scale - // (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw - // used: - // roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset)) - // which could disagree with LoanPay's minimum (which used loanScale). - // - // Use a large vault deposit so that vaultScale (from AssetsTotal) is - // strictly larger than debtScale (from DebtTotal). With - // vaultDeposit = 100,000: after the big loan - // AssetsTotal ≈ 109,500 → vaultScale = -10 - // DebtTotal ≈ 10,000 → debtScale = -11 - // The one-order-of-magnitude gap makes roundToAsset at -10 truncate - // more aggressively than at -11, exposing the bug. - testcase("CoverWithdraw minimum cover scale consistency"); - runTest( - /*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) { - Asset const asset{c.iou}; - - // Create only the big loan to push DebtTotal up to ~10,000 - // while AssetsTotal stays around 109,500 (dominated by the - // large vault deposit). - env(set(c.borrower, c.broker.brokerID, Number{500}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{100'000}), - kPaymentTotal(20), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // Read broker state and compute both old and new minimums. - auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); - auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); - if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle)) - return; - - auto const coverAvail = brokerSle->at(sfCoverAvailable); - auto const debtTotal = brokerSle->at(sfDebtTotal); - auto const vaultScale = getAssetsTotalScale(vaultSle); - auto const debtScale = scale(debtTotal, asset); - - // Sanity: debt scale differs from vault scale for this setup. - BEAST_EXPECT(debtScale < vaultScale); - - auto const oldMin = [&]() { - NumberRoundModeGuard const mg(Number::RoundingMode::Upward); - return roundToAsset( - asset, - tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}), - debtScale); - }(); - auto const newMin = minimumBrokerCover( - debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle); - - // The new (vaultScale) minimum must be strictly larger than - // the old (debtScale) minimum — that is the gap the amendment - // closes. - Number const expectedNewMin{1330650518688500000, -15}; - Number const expectedOldMin{1330650518688472000, -15}; - BEAST_EXPECT(newMin == expectedNewMin); - BEAST_EXPECT(oldMin == expectedOldMin); - - // Try to withdraw so that remaining cover lands between the - // two minimums: oldMin < target < newMin. - auto const target = oldMin + (newMin - oldMin) / 2; - auto const withdrawAmount = STAmount{asset, coverAvail - target}; - - if (withAmendment) - { - // CoverWithdraw now uses vaultScale: target < newMin - // => FAILS. - env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount), - Ter(tecINSUFFICIENT_FUNDS)); - } - else - { - // Old CoverWithdraw uses debtScale: target > oldMin - // => SUCCEEDS. - env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount)); - } - env.close(); - }); - - // Scenario 3 — LoanSet - // - // Verify that LoanSet's minimum cover check uses vault scale (not the - // raw unrounded tenthBipsOfValue). Before the amendment, LoanSet - // used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no - // roundToAsset), while clawback/withdraw used different formulas. - // After the amendment all use minimumBrokerCover at vaultScale, and - // rounding at a coarser scale can absorb a tiny debt increase — - // allowing a loan that would otherwise be rejected. - testcase("LoanSet minimum cover scale consistency"); - runTest( - /*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { - // Create the tiny loan (scale -12) AND the big loan (scale - // -11). Both loans are needed so that DebtTotal has a full - // 16-digit mantissa — a "messy" value where roundToAsset at - // vaultScale actually truncates digits and produces a - // different result from the raw tenthBipsOfValue. With only - // the big loan, DebtTotal has ~4 significant digits and - // rounding at scale -11 is a no-op, masking the amendment's - // effect. - env(set(c.borrower, c.broker.brokerID, Number{1, -2}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - env(set(c.borrower, c.broker.brokerID, Number{500}), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{100'000}), - kPaymentTotal(20), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - env.close(); - - // Clawback to reduce cover to the clawback transactor's - // minimum. Pass the exact amount rather than relying on the - // transactor to clip down; the setup matches Scenario 1 so - // the same residual-cover values apply. - Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} - : Number{1330651855688458000, -15}; - Number const clawbackAmount = - Number{c.brokerParams.coverDeposit} - expectedCoverAfter; - env(coverClawback(c.issuer), - kLoanBrokerId(c.broker.brokerID), - kAmount(c.iou(clawbackAmount))); - env.close(); - - // Verify scales. - auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); - if (!BEAST_EXPECT(vaultSle)) - return; - auto const vaultScale = getAssetsTotalScale(vaultSle); - BEAST_EXPECT(vaultScale == -11); - - // Now try to create a tiny additional loan. Principal is - // 1e-11 (the smallest value that survives the precision - // check at loanScale = vaultScale = -11), with 0% interest - // and 1 payment. - // - // The tiny debt increase adds ~1.337e-12 to the unrounded - // minimum. - // - Without the amendment: the old LoanSet formula rounds - // up during tenthBipsOfValue (16-digit Number - // normalisation), pushing the minimum past the cover left - // by clawback => tecINSUFFICIENT_FUNDS. - // - With the amendment: minimumBrokerCover rounds at - // vaultScale=-11, which absorbs the tiny increase — the - // rounded minimum stays the same => tesSUCCESS. - auto const tinyPrincipal = Number{1, -11}; - - if (withAmendment) - { - env(set(c.borrower, c.broker.brokerID, tinyPrincipal), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10))); - } - else - { - env(set(c.borrower, c.broker.brokerID, tinyPrincipal), - Sig(sfCounterpartySignature, c.lender), - kInterestRate(TenthBips32{0}), - kPaymentTotal(1), - kPaymentInterval(86400 * 365), - Fee(XRP(10)), - Ter(tecINSUFFICIENT_FUNDS)); - } - env.close(); - }); - } - - // LendingProtocolV1_1 ("cash-basis" accounting) dedicated coverage. - // - // Existing tests never enable featureLendingProtocolV1_1 (see `all_` - // above), so these are the only tests in this file that exercise the - // amendment. They are called once, directly, from - // runAmendmentIndependent() -- not looped through - // runAmendmentSensitive()/amendmentCombinations(), since doing so would - // require re-deriving whole-life-specific expected values for ~15 - // unrelated regression tests. - - // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas, - // and the AssetsMaximum/DebtMaximum guards (which always check against - // principal + interestDue, regardless of the amendment). - void - testCashBasisLoanSetOrigination() - { - testcase("cash-basis: LoanSet origination"); - - using namespace jtx; - using namespace loan; - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .coverDeposit = 0, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - - Number const principalRequest{10'000}; - TenthBips32 const interestRate{percentageToTenthBips(10)}; - std::uint32_t const paymentTotal = 2; - std::uint32_t const paymentInterval = 86400; - - // Creates a broker/vault, submits a single LoanSet with a nonzero - // interest rate, and returns the observed Vault.AssetsTotal / - // LoanBroker.DebtTotal deltas plus the loan's own computed - // interestDue and principalOutstanding. - auto runOrigination = [&](FeatureBitset features) { - Env env(*this, features); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - auto const vaultBefore = env.le(broker.vaultKeylet()); - auto const brokerBefore = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultBefore && brokerBefore); - Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); - Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); - - auto const loanSequence = brokerBefore->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), - kCounterparty(lender), - kInterestRate(interestRate), - kPaymentTotal(paymentTotal), - kPaymentInterval(paymentInterval), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 2), - Ter(tesSUCCESS)); - env.close(); - - auto const loanSle = env.le(loanKeylet); - BEAST_EXPECT(loanSle); - Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding); - Number const interestDue = totalValueOutstanding - principalOutstanding; - BEAST_EXPECT(interestDue > beast::kZero); - BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value()); - - auto const vaultAfter = env.le(broker.vaultKeylet()); - auto const brokerAfter = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultAfter && brokerAfter); - Number const assetsTotalDelta = - Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; - Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; - - return std::make_tuple( - assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding); - }; - - Number interestDueCash{}; - Number principalOutstandingCash{}; - { - auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = - runOrigination(all_ | featureLendingProtocolV1_1); - interestDueCash = interestDue; - principalOutstandingCash = principalOutstanding; - - BEAST_EXPECTS( - assetsTotalDelta == beast::kZero, - "cash-basis origination must not change AssetsTotal; delta=" + - to_string(assetsTotalDelta)); - BEAST_EXPECTS( - debtTotalDelta == principalOutstanding, - "cash-basis origination must add principal-only to DebtTotal; delta=" + - to_string(debtTotalDelta) + " principal=" + to_string(principalOutstanding)); - } - - { - auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = - runOrigination(all_); - - BEAST_EXPECTS( - assetsTotalDelta == interestDue, - "whole-life origination must add interestDue to AssetsTotal; delta=" + - to_string(assetsTotalDelta) + " interestDue=" + to_string(interestDue)); - BEAST_EXPECTS( - debtTotalDelta == principalOutstanding + interestDue, - "whole-life origination must add principal+interest to DebtTotal; delta=" + - to_string(debtTotalDelta)); - } - - // AssetsMaximum guard checks interestDue headroom only under - // whole-life accounting; DebtMaximum guard also varies by model. - auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) { - Env env(*this, features); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - auto const vaultSle = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultSle); - Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal); - - Vault const vault{env}; - auto tx = vault.set({.owner = lender, .id = broker.vaultID}); - tx[sfAssetsMaximum] = assetsTotalBefore + slack; - env(tx); - env.close(); - - env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), - kCounterparty(lender), - kInterestRate(interestRate), - kPaymentTotal(paymentTotal), - kPaymentInterval(paymentInterval), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 2), - Ter(expected)); - env.close(); - }; - - auto runBrokerGuard = [&](FeatureBitset features, Number const& debtMaximum, TER expected) { - Env env(*this, features); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - env(loan_broker::set(lender, broker.vaultID), - loan_broker::kLoanBrokerId(broker.brokerID), - loan_broker::kDebtMaximum(debtMaximum), - Fee(env.current()->fees().base * 2)); - env.close(); - - env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), - kCounterparty(lender), - kInterestRate(interestRate), - kPaymentTotal(paymentTotal), - kPaymentInterval(paymentInterval), - Sig(sfCounterpartySignature, lender), - Fee(env.current()->fees().base * 2), - Ter(expected)); - env.close(); - }; - - Number const oneDrop = xrpAsset(1).value(); - { - testcase("whole-life: LoanSet AssetsMaximum guard checks interestDue headroom"); - // Guard rejects when there's not quite enough headroom for the - // interest. - runVaultGuard(all_, interestDueCash - oneDrop, tecLIMIT_EXCEEDED); - // Guard accepts at the exact boundary. - runVaultGuard(all_, interestDueCash, tesSUCCESS); - } - - { - testcase("cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom"); - // Even far less headroom than interestDue still succeeds, since - // cash-basis origination never adds interest to AssetsTotal. - runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS); - } - - // DebtMaximum guard: cash-basis projects principal-only DebtTotal; - // whole-life projects principal + interestDue. - for (auto const cashBasis : {true, false}) - { - testcase( - std::string("LoanSet DebtMaximum guard (") + - (cashBasis ? "cash-basis)" : "whole-life)")); - auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_; - Number const newDebtTotal = - principalOutstandingCash + (cashBasis ? Number{} : interestDueCash); - runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED); - runBrokerGuard(features, newDebtTotal, tesSUCCESS); - } - } - - // 2. LoanPay: regular, late, overpayment, and full-payment types. - // Assert Vault.AssetsTotal/LoanBroker.DebtTotal deltas match - // interestPaid/principalPaid under cash-basis, and cross-check the - // amendment-disabled run's deltas against the documented whole-life - // formula (AssetsTotal += valueChange; DebtTotal mirrors the loan's own - // TotalValueOutstanding delta exactly, since whole-life debt recognition - // tracks total loan value). - void - testCashBasisLoanPay() - { - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - using tp = NetClock::time_point; - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000'000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .coverDeposit = 0, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - - Number const principalRequest{12'000}; - TenthBips32 const interestRate{percentageToTenthBips(12)}; - std::uint32_t const paymentTotal = 4; - std::uint32_t const paymentInterval = 600; - std::uint32_t const gracePeriod = 300; - - struct PaymentDeltas - { - Number principalPaid; - Number assetsTotalDelta; - Number debtTotalDelta; - Number totalValueDelta; - }; - - // Sets up a fresh broker + loan, advances time, submits a single - // payment of the given type/amount, and returns the observed deltas. - auto runPayment = [&](FeatureBitset features, - std::uint32_t loanSetFlags, - std::uint32_t payFlags, - std::function const& advanceTime, - std::function const& paymentAmount) { - Env env(*this, features); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(10'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .principalRequest = principalRequest, - .interest = interestRate, - .payTotal = paymentTotal, - .payInterval = paymentInterval, - .gracePd = gracePeriod, - .flags = loanSetFlags, - }; - - auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerBeforeLoan); - auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(loanParams(env, broker)); - env.close(); - - LoanState const state = getCurrentState(env, broker, loanKeylet); - - advanceTime(env, state.startDate); - - auto const vaultBefore = env.le(broker.vaultKeylet()); - auto const brokerBefore = env.le(broker.brokerKeylet()); - auto const loanBefore = env.le(loanKeylet); - BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore); - - Number const principalBefore = loanBefore->at(sfPrincipalOutstanding); - Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding); - Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); - Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); - - STAmount const amount = paymentAmount(state); - env(pay(borrower, loanKeylet.key, amount, payFlags), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfter = env.le(broker.vaultKeylet()); - auto const brokerAfter = env.le(broker.brokerKeylet()); - auto const loanAfter = env.le(loanKeylet); - BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter); - - Number const principalAfter = loanAfter->at(sfPrincipalOutstanding); - Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding); - Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); - Number const debtTotalAfter = brokerAfter->at(sfDebtTotal); - - return PaymentDeltas{ - .principalPaid = principalBefore - principalAfter, - .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore, - .debtTotalDelta = debtTotalAfter - debtTotalBefore, - .totalValueDelta = totalValueAfter - totalValueBefore}; - }; - - // Compares the disabled (whole-life) and enabled (cash-basis) runs - // of the same payment scenario, and asserts the documented - // relationships between them. - auto checkScenario = [&](std::string const& label, - PaymentDeltas const& off, - PaymentDeltas const& on) { - testcase("cash-basis: LoanPay " + label); - - // The loan's own PrincipalOutstanding field is untouched by - // the amendment. - BEAST_EXPECTS( - off.principalPaid == on.principalPaid, - "principalPaid must be amendment-independent; off=" + to_string(off.principalPaid) + - " on=" + to_string(on.principalPaid)); - - // Whole-life structural invariant: DebtTotal (which - // recognizes a loan's full remaining value as debt) must - // change exactly as the loan's own TotalValueOutstanding - // does. - BEAST_EXPECTS( - off.debtTotalDelta == off.totalValueDelta, - "whole-life DebtTotal delta must mirror TotalValueOutstanding delta; " - "debtTotalDelta=" + - to_string(off.debtTotalDelta) + - " totalValueDelta=" + to_string(off.totalValueDelta)); - - // Derive interestPaid from the whole-life run's independent - // ledger deltas: - // assetsTotalDelta_off == valueChange - // debtTotalDelta_off == valueChange - (principalPaid + interestPaid) - // => interestPaid == assetsTotalDelta_off - debtTotalDelta_off - principalPaid - Number const interestPaid = - off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid; - BEAST_EXPECTS( - interestPaid >= beast::kZero, - "derived interestPaid must be non-negative: " + to_string(interestPaid)); - - BEAST_EXPECTS( - on.assetsTotalDelta == interestPaid, - "cash-basis AssetsTotal delta must equal interestPaid; delta=" + - to_string(on.assetsTotalDelta) + " interestPaid=" + to_string(interestPaid)); - BEAST_EXPECTS( - on.debtTotalDelta == -on.principalPaid, - "cash-basis DebtTotal delta must equal -principalPaid; delta=" + - to_string(on.debtTotalDelta) + " principalPaid=" + to_string(on.principalPaid)); - }; - - // ---- Regular, on-time payment ---- - { - auto const noAdvance = [](Env& env, tp const&) { env.close(); }; - auto const regularAmount = [&](LoanState const& state) { - return STAmount{ - xrpAsset, - roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * - Number{3, -1} * 5}; // 1.5x, so only a single period is paid - }; - - auto const off = runPayment(all_, 0, 0, noAdvance, regularAmount); - auto const on = - runPayment(all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount); - - // Regular, on-time payments never change the loan's value beyond - // normal amortization (production asserts valueChange == 0), so - // AssetsTotal must be unaffected in the whole-life run. - BEAST_EXPECTS( - off.assetsTotalDelta == beast::kZero, - "regular on-time payment must not change AssetsTotal under whole-life; delta=" + - to_string(off.assetsTotalDelta)); - - checkScenario("regular payment", off, on); - } - - // ---- Late payment ---- - { - auto const advancePastDue = [&](Env& env, tp const& startDate) { - env.close(startDate + std::chrono::seconds(paymentInterval + 1)); - }; - auto const lateAmount = [&](LoanState const& state) { - return STAmount{ - xrpAsset, - roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * - Number{3}}; // generous; excess is not withdrawn - }; - - auto const off = runPayment(all_, 0, tfLoanLatePayment, advancePastDue, lateAmount); - auto const on = runPayment( - all_ | featureLendingProtocolV1_1, - 0, - tfLoanLatePayment, - advancePastDue, - lateAmount); - - checkScenario("late payment", off, on); - } - - // ---- Overpayment ---- - { - auto const noAdvance = [](Env& env, tp const&) { env.close(); }; - auto const overpayAmount = [&](LoanState const& state) { - // One regular period, plus a generous extra principal - // paydown. - return STAmount{ - xrpAsset, - roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) + - xrpAsset(2'000).value()}; - }; - - auto const off = - runPayment(all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount); - auto const on = runPayment( - all_ | featureLendingProtocolV1_1, - tfLoanOverpayment, - tfLoanOverpayment, - noAdvance, - overpayAmount); - - checkScenario("overpayment", off, on); - } - - // ---- Full payment ---- - { - auto const noAdvance = [](Env& env, tp const&) { env.close(); }; - auto const fullAmount = [&](LoanState const&) { - // Generously large: full payment only ever consumes exactly - // what's due (principal + accrued interest; close fee/ - // prepayment penalty are 0 here), excess is not withdrawn. - return STAmount{xrpAsset, xrpAsset(principalRequest).value() * Number{2}}; - }; - - auto const off = runPayment(all_, 0, tfLoanFullPayment, noAdvance, fullAmount); - auto const on = runPayment( - all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount); - - checkScenario("full payment", off, on); - } - } - - // 3. LoanManage: impair, unimpair, and default. - void - testCashBasisLoanManage() - { - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000'000, - .debtMax = 0, - .coverRateMin = TenthBips32{percentageToTenthBips(10)}, - .coverDeposit = 5'000, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; - - Number const principalRequest{10'000}; - TenthBips32 const interestRate{percentageToTenthBips(12)}; - std::uint32_t const paymentTotal = 4; - std::uint32_t const paymentInterval = 600; - std::uint32_t const gracePeriod = 60; - - auto setupLoan = [&](Env& env) { - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(10'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .principalRequest = principalRequest, - .interest = interestRate, - .payTotal = paymentTotal, - .payInterval = paymentInterval, - .gracePd = gracePeriod, - }; - - auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerBeforeLoan); - auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - env(loanParams(env, broker)); - env.close(); - - return std::make_tuple(broker, loanKeylet, lender, borrower); - }; - - // ---- impair / unimpair ---- - auto runImpairUnimpair = [&](FeatureBitset features) { - Env env(*this, features); - auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); - - auto const loanBefore = env.le(loanKeylet); - BEAST_EXPECT(loanBefore); - Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding); - Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding); - Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding); - - Number const expectedExposure = - env.current()->rules().enabled(featureLendingProtocolV1_1) - ? principalOutstanding - : totalValueOutstanding - managementFeeOutstanding; - - auto const vaultBeforeImpair = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultBeforeImpair); - Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized); - - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfterImpair = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultAfterImpair); - Number const impairDelta = Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore; - - env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfterUnimpair = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultAfterUnimpair); - Number const netDelta = Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore; - - return std::make_tuple(expectedExposure, impairDelta, netDelta); - }; - - for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) - { - testcase( - std::string("cash-basis: LoanManage impair/unimpair (") + - (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); - auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features); - - BEAST_EXPECTS( - impairDelta == expectedExposure, - "impair must add loanVaultExposure to LossUnrealized; delta=" + - to_string(impairDelta) + " expected=" + to_string(expectedExposure)); - BEAST_EXPECTS( - netDelta == beast::kZero, - "unimpair must be an exact reversal of impair; net=" + to_string(netDelta)); - } - - // ---- impair, then default ---- - auto runDefault = [&](FeatureBitset features) { - Env env(*this, features); - auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); - - auto const loanBeforeImpair = env.le(loanKeylet); - BEAST_EXPECT(loanBeforeImpair); - Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding); - Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding); - Number const managementFeeOutstanding = - loanBeforeImpair->at(sfManagementFeeOutstanding); - - Number const expectedExposure = - env.current()->rules().enabled(featureLendingProtocolV1_1) - ? principalOutstanding - : totalValueOutstanding - managementFeeOutstanding; - - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); - env.close(); - - LoanState const state = getCurrentState(env, broker, loanKeylet); - env.close( - state.startDate + std::chrono::seconds(paymentInterval) + - std::chrono::seconds(gracePeriod) + 60s); - - auto const vaultBefore = env.le(broker.vaultKeylet()); - auto const brokerBefore = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultBefore && brokerBefore); - Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); - Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); - Number const lossBefore = vaultBefore->at(sfLossUnrealized); - Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); - - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfter = env.le(broker.vaultKeylet()); - auto const brokerAfter = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultAfter && brokerAfter); - Number const assetsTotalDelta = - Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; - Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; - Number const lossDelta = Number(vaultAfter->at(sfLossUnrealized)) - lossBefore; - Number const coverAvailableDelta = - Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore; - - Number const defaultCovered = -coverAvailableDelta; - Number const vaultDefaultAmount = expectedExposure - defaultCovered; - - return std::make_tuple( - expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount); - }; - - for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) - { - testcase( - std::string("cash-basis: LoanManage default (") + - (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); - auto const - [expectedExposure, - assetsTotalDelta, - debtTotalDelta, - lossDelta, - vaultDefaultAmount] = runDefault(features); - - BEAST_EXPECTS( - debtTotalDelta == -expectedExposure, - "default must reduce DebtTotal by the unified default amount; delta=" + - to_string(debtTotalDelta) + " expected=" + to_string(expectedExposure)); - BEAST_EXPECTS( - lossDelta == -expectedExposure, - "default must reverse the earlier impair's LossUnrealized exactly; delta=" + - to_string(lossDelta) + " expected=" + to_string(expectedExposure)); - BEAST_EXPECTS( - assetsTotalDelta == -vaultDefaultAmount, - "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" + - to_string(assetsTotalDelta) + " expected=" + to_string(-vaultDefaultAmount)); - } - } - - // 3b. LEVersion regression: a Vault created before featureLendingProtocolV1_1 - // activates (LEVersion absent) must keep whole-life (accrual) accounting - // forever, even after the amendment is later enabled -- the switch is - // per-Vault (LEVersion == VaultVersion::CashBasis), not a single global amendment - // flag. - void - testLegacyVaultKeepsAccrualAfterAmendmentEnabled() - { - testcase("LEVersion: legacy vault keeps accrual after amendment enabled"); - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{ - .vaultDeposit = 1'000'000, - .debtMax = 0, - .coverRateMin = TenthBips32{percentageToTenthBips(10)}, - .coverDeposit = 5'000, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; - - Number const principalRequest{10'000}; - TenthBips32 const interestRate{percentageToTenthBips(12)}; - std::uint32_t const paymentTotal = 4; - std::uint32_t const paymentInterval = 600; - std::uint32_t const gracePeriod = 60; - - // Amendment disabled at Vault creation time: LEVersion stays absent. - Env env(*this, all_); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(10'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - { - auto const vaultSle = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultSle); - BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); - } - - // Now enable the amendment -- production dispatch must still treat - // this specific Vault as accrual-basis, since its LEVersion is - // (and remains) absent. - env.enableFeature(featureLendingProtocolV1_1); - env.close(); - - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .principalRequest = principalRequest, - .interest = interestRate, - .payTotal = paymentTotal, - .payInterval = paymentInterval, - .gracePd = gracePeriod, - }; - - auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); - BEAST_EXPECT(brokerBeforeLoan); - auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - // ---- LoanSet origination: whole-life formulas expected ---- - auto const vaultBeforeSet = env.le(broker.vaultKeylet()); - auto const brokerBeforeSet = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet); - Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal); - Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal); - - env(loanParams(env, broker)); - env.close(); - - auto const loanAfterSet = env.le(loanKeylet); - BEAST_EXPECT(loanAfterSet); - Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding); - Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding); - Number const interestDue = totalValueOutstanding - principalOutstanding; - BEAST_EXPECT(interestDue > beast::kZero); - - auto const vaultAfterSet = env.le(broker.vaultKeylet()); - auto const brokerAfterSet = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultAfterSet && brokerAfterSet); - Number const assetsTotalDeltaSet = - Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet; - Number const debtTotalDeltaSet = - Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet; - - BEAST_EXPECTS( - assetsTotalDeltaSet == interestDue, - "legacy vault origination must still add interestDue to AssetsTotal; delta=" + - to_string(assetsTotalDeltaSet) + " interestDue=" + to_string(interestDue)); - BEAST_EXPECTS( - debtTotalDeltaSet == principalOutstanding + interestDue, - "legacy vault origination must still add principal+interest to DebtTotal; delta=" + - to_string(debtTotalDeltaSet)); - - LoanState const state = getCurrentState(env, broker, loanKeylet); - env.close(); - - // ---- LoanPay: whole-life formulas expected ---- - auto const vaultBeforePay = env.le(broker.vaultKeylet()); - auto const brokerBeforePay = env.le(broker.brokerKeylet()); - auto const loanBeforePay = env.le(loanKeylet); - BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay); - Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding); - Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal); - Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal); - - STAmount const paymentAmount{ - xrpAsset, roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale)}; - env(pay(borrower, loanKeylet.key, paymentAmount), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfterPay = env.le(broker.vaultKeylet()); - auto const brokerAfterPay = env.le(broker.brokerKeylet()); - auto const loanAfterPay = env.le(loanKeylet); - BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay); - Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding); - Number const assetsTotalDeltaPay = - Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay; - Number const debtTotalDeltaPay = - Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay; - Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay; - - // A regular, on-time payment has valueChange == 0, so whole-life - // AssetsTotal is untouched and DebtTotal mirrors TotalValueOutstanding. - BEAST_EXPECTS( - assetsTotalDeltaPay == beast::kZero, - "legacy vault regular payment must not change AssetsTotal; delta=" + - to_string(assetsTotalDeltaPay)); - BEAST_EXPECTS( - debtTotalDeltaPay == totalValueDeltaPay, - "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; " - "debtTotalDelta=" + - to_string(debtTotalDeltaPay) + " totalValueDelta=" + to_string(totalValueDeltaPay)); - - // ---- LoanManage: impair, then default -- whole-life exposure expected ---- - auto const loanBeforeImpair = env.le(loanKeylet); - BEAST_EXPECT(loanBeforeImpair); - Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding); - Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding); - Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair; - - env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); - env.close(); - - LoanState const stateAtImpair = getCurrentState(env, broker, loanKeylet); - env.close( - stateAtImpair.startDate + std::chrono::seconds(paymentInterval) + - std::chrono::seconds(gracePeriod) + 60s); - - auto const vaultBeforeDefault = env.le(broker.vaultKeylet()); - auto const brokerBeforeDefault = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault); - Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal); - Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized); - - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - env.close(); - - auto const vaultAfterDefault = env.le(broker.vaultKeylet()); - auto const brokerAfterDefault = env.le(broker.brokerKeylet()); - BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault); - Number const debtTotalDeltaDefault = - Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault; - Number const lossDeltaDefault = - Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault; - - BEAST_EXPECTS( - debtTotalDeltaDefault == -expectedExposure, - "legacy vault default must reduce DebtTotal by whole-life exposure; delta=" + - to_string(debtTotalDeltaDefault) + " expected=" + to_string(expectedExposure)); - BEAST_EXPECTS( - lossDeltaDefault == -expectedExposure, - "legacy vault default must reverse the earlier impair's LossUnrealized exactly; " - "delta=" + - to_string(lossDeltaDefault) + " expected=" + to_string(expectedExposure)); - - // Confirm the Vault's LEVersion truly never got set, throughout. - { - auto const vaultSle = env.le(broker.vaultKeylet()); - BEAST_EXPECT(vaultSle); - BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); - BEAST_EXPECT(getVaultVersion(vaultSle) == VaultVersion::Legacy); - } - } - - // 4. End-to-end trajectory: LoanSet -> 2 LoanPays -> LoanManage(default), - // entirely under the amendment, with independently hand-computed - // expected AssetsTotal/DebtTotal/LossUnrealized/CoverAvailable values at - // each step. 0% interest keeps the arithmetic exact and tractable; the - // divergence from whole-life accounting is already covered directly by - // testCashBasisLoanSetOrigination/LoanPay/LoanManage above, so this test - // focuses purely on an independent, from-scratch trajectory check. - void - testCashBasisEndToEndTrajectory() - { - testcase("cash-basis: end-to-end trajectory"); - - using namespace jtx; - using namespace loan; - using namespace std::chrono_literals; - - PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; - BrokerParameters const brokerParams{ - .vaultDeposit = 100'000, .managementFeeRate = TenthBips16{0}}; - - Env env(*this, all_ | featureLendingProtocolV1_1); - - Account const lender{"lender"}; - Account const borrower{"borrower"}; - env.fund(XRP(10'000'000), lender, borrower); - env.close(); - - BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; - - // Hand computation (all values in XRP, drops == 1e-6 XRP): - // Vault: AssetsTotal starts at 100'000 (the deposit). - // Broker: DebtTotal starts at 0, CoverAvailable starts at 1'000 - // (BrokerParameters::defaults().coverDeposit). - auto const vaultKeylet = broker.vaultKeylet(); - auto const brokerKeylet = broker.brokerKeylet(); - - // All the "human XRP unit" constants below (e.g. `100'000`) are - // converted to raw native (drops) values via xrpAsset(...), since - // that's how the ledger fields are actually denominated. - auto const checkVaultBroker = [&](Number const& assetsTotalUnits, - Number const& debtTotalUnits, - Number const& lossUnrealizedUnits, - Number const& coverAvailableUnits, - char const* step) { - Number const assetsTotal = xrpAsset(assetsTotalUnits).value(); - Number const debtTotal = xrpAsset(debtTotalUnits).value(); - Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value(); - Number const coverAvailable = xrpAsset(coverAvailableUnits).value(); - - auto const vaultSle = env.le(vaultKeylet); - auto const brokerSle = env.le(brokerKeylet); - BEAST_EXPECT(vaultSle && brokerSle); - BEAST_EXPECTS( - vaultSle->at(sfAssetsTotal) == assetsTotal, - std::string(step) + ": AssetsTotal expected " + to_string(assetsTotal) + " got " + - to_string(Number(vaultSle->at(sfAssetsTotal)))); - BEAST_EXPECTS( - brokerSle->at(sfDebtTotal) == debtTotal, - std::string(step) + ": DebtTotal expected " + to_string(debtTotal) + " got " + - to_string(Number(brokerSle->at(sfDebtTotal)))); - BEAST_EXPECTS( - vaultSle->at(sfLossUnrealized) == lossUnrealized, - std::string(step) + ": LossUnrealized expected " + to_string(lossUnrealized) + - " got " + to_string(Number(vaultSle->at(sfLossUnrealized)))); - BEAST_EXPECTS( - brokerSle->at(sfCoverAvailable) == coverAvailable, - std::string(step) + ": CoverAvailable expected " + to_string(coverAvailable) + - " got " + to_string(Number(brokerSle->at(sfCoverAvailable)))); - }; - - checkVaultBroker(100'000, 0, 0, 1'000, "before LoanSet"); - - // Loan: principal=1200, 0% interest, 12 payments of 100 each, no fees. - Number const principalRequest{1'200}; - std::uint32_t const paymentTotal = 12; - std::uint32_t const paymentInterval = 600; - std::uint32_t const gracePeriod = 60; - - auto const brokerBeforeLoan = env.le(brokerKeylet); - BEAST_EXPECT(brokerBeforeLoan); - auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); - auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); - - LoanParameters const loanParams{ - .account = borrower, - .counter = lender, - .principalRequest = principalRequest, - .interest = TenthBips32{0}, - .payTotal = paymentTotal, - .payInterval = paymentInterval, - .gracePd = gracePeriod, - }; - env(loanParams(env, broker)); - env.close(); - - // Origination (cash-basis): AssetsTotal += 0, DebtTotal += principal. - checkVaultBroker(100'000, 1'200, 0, 1'000, "after LoanSet"); - - LoanState const state = getCurrentState(env, broker, loanKeylet); - BEAST_EXPECT(state.periodicPayment == xrpAsset(100).value()); - - // Payment 1: principalPaid=100, interestPaid=0. - // AssetsTotal += 0; DebtTotal -= 100. - env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); - env.close(); - checkVaultBroker(100'000, 1'100, 0, 1'000, "after payment 1"); - - // Payment 2: same as above. - env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); - env.close(); - checkVaultBroker(100'000, 1'000, 0, 1'000, "after payment 2"); - - // Default (no impair): principalOutstanding remaining is 1'000. - // totalDefaultAmount (cash-basis) = PrincipalOutstanding = 1'000. - // minimumCover = DebtTotal(1'000) * coverRateMin(10%) = 100. - // covered = min(minimumCover * coverRateLiquidation(25%), totalDefaultAmount) - // = min(25, 1'000) = 25. - // defaultCovered = min(covered, CoverAvailable(1'000)) = 25. - // vaultDefaultAmount = 1'000 - 25 = 975. - // DebtTotal -= 1'000 -> 0. CoverAvailable -= 25 -> 975. - // AssetsTotal -= 975 -> 99'025. LossUnrealized unaffected (never impaired). - auto const loanBeforeDefault = env.le(loanKeylet); - BEAST_EXPECT(loanBeforeDefault); - BEAST_EXPECT( - Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value()); - - env.close(state.startDate + std::chrono::seconds((3 * paymentInterval) + gracePeriod) + 1s); - - env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); - env.close(); - - checkVaultBroker(99'025, 0, 0, 975, "after LoanManage(default)"); - } - - void - runAmendmentIndependent() - { - testDisabled(); - testInvalidLoanSet(); - testInvalidLoanDelete(); - testInvalidLoanManage(); - testInvalidLoanPay(); - testIssuerLoan(); - testServiceFeeOnBrokerDeepFreeze(); - testRequireAuth(); - testRIPD3901(); - testBorrowerIsBroker(); - testLimitExceeded(); - testLendingCanTradeDisabledNoImpact(); - testBugOverpaymentPrincipalChange(); - testBugOverpayUnroundedAmount(); - - for (auto const flags : {0u, tfLoanOverpayment}) - testYieldTheftRounding(flags); - testBugInterestDueDeltaCrash(); - testFullLifecycleVaultPnLNearZeroRate(); - testLoanSetNearZeroInterestRateSucceeds(); - - testCashBasisLoanSetOrigination(); - testCashBasisLoanPay(); - testCashBasisLoanManage(); - testLegacyVaultKeepsAccrualAfterAmendmentEnabled(); - testCashBasisEndToEndTrajectory(); - } - - // Tests run under each entry in amendmentCombinations(). - void - runAmendmentSensitive(FeatureBitset features) - { -#if LOAN_TODO - testLoanPayLateFullPaymentBypassesPenalties(features); - testLoanCoverMinimumRoundingExploit(features); -#endif - // Lifecycle - testLifecycle(features); - testLoanSet(features); - testDosLoanPay(features); - testSelfLoan(features); - - // Payment paths - testWithdrawReflectsUnrealizedLoss(features); - testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features); - testBatchBypassCounterparty(features); - testLoanNextPaymentDueDateOverflow(features); - testSequentialFLCDepletion(features); - - // Invariants - testLoanPayComputePeriodicPaymentValidRateInvariant(features); - testAccountSendMptMinAmountInvariant(features); - testLoanPayDebtDecreaseInvariant(features); - testWrongMaxDebtBehavior(features); - testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(features); - testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(features); - testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(features); - - // RPC - testRPC(features); - - // Edge / rounding - testDustManipulation(features); - testRoundingAllowsUndercoverage(features); - testOverpaymentManagementFee(features); - testIssuerIsBorrower(features); - testIntegerScalePrincipalSticks(features); - testMinimumBrokerCoverConsistency(features); - - // RIPD regressions - testRIPD3831(features); - testRIPD3459(features); - testRIPD3902(features); - - // Broker-owner permissions - testLoanPayBrokerOwnerMissingTrustline(features); - testLoanPayBrokerOwnerUnauthorizedMPT(features); - testLoanPayBrokerOwnerNoPermissionedDomainMPT(features); - testLoanSetBrokerOwnerNoPermissionedDomainMPT(features); - } - -public: - void - run() override - { - runAmendmentIndependent(); - for (auto const& features : jtx::amendmentCombinations( - {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) - runAmendmentSensitive(features); - } -}; - -class LoanBatch_test : public Loan_test -{ -protected: - beast::xor_shift_engine engine_; - - std::uniform_int_distribution<> assetDist_{0, 2}; - std::uniform_int_distribution principalDist_{100'000, 1'000'000'000}; - std::uniform_int_distribution interestRateDist_{0, 10000}; - std::uniform_int_distribution<> paymentTotalDist_{12, 10000}; - std::uniform_int_distribution<> paymentIntervalDist_{60, 3600 * 24 * 30}; - std::uniform_int_distribution managementFeeRateDist_{0, 10'000}; - std::uniform_int_distribution<> serviceFeeDist_{0, 20}; - /* - # Generate parameters that are more likely to be valid - principal = Decimal(str(rand.randint(100000, - 100'000'000))).quantize(ROUND_TARGET) - - interest_rate = Decimal(rand.randint(1, 10000)) / - Decimal(100000) - - payment_total = rand.randint(12, 10000) - - payment_interval = Decimal(str(rand.randint(60, 2629746))) - - interest_fee = Decimal(rand.randint(0, 100000)) / - Decimal(100000) -*/ - - void - testRandomLoan() - { - using namespace jtx; - - Account const issuer("issuer"); - Account const lender("lender"); - Account const borrower("borrower"); - - // Determine all the random parameters at once - auto const assetType = static_cast(assetDist_(engine_)); - auto const principalRequest = principalDist_(engine_); - TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)}; - auto const serviceFee = serviceFeeDist_(engine_); - TenthBips32 interest{interestRateDist_(engine_)}; - auto const payTotal = paymentTotalDist_(engine_); - auto const payInterval = paymentIntervalDist_(engine_); - - BrokerParameters const brokerParams{ - .vaultDeposit = principalRequest * 10, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = managementFeeRate}; - LoanParameters const loanParams{ - .account = lender, - .counter = borrower, - .principalRequest = principalRequest, - .serviceFee = serviceFee, - .interest = interest, - .payTotal = payTotal, - .payInterval = payInterval, - }; - - runLoan(assetType, brokerParams, loanParams, all_); - } - -public: - void - run() override - { - auto const numIterations = [s = arg()]() -> int { - int const defaultNum = 5; - if (s.empty()) - return defaultNum; - try - { - std::size_t pos = 0; - auto const r = stoi(s, &pos); - if (pos != s.size()) - return defaultNum; - return r; - } - catch (...) - { - return defaultNum; - } - }(); - - using namespace jtx; - - auto const updateInterval = std::min(numIterations / 5, 100); - - for (int i = 0; i < numIterations; ++i) - { - if (i % updateInterval == 0) - testcase << "Random Loan Test iteration " << (i + 1) << "/" << numIterations; - testRandomLoan(); - } - } -}; - -class LoanArbitrary_test : public LoanBatch_test -{ - void - run() override - { - using namespace jtx; - - BrokerParameters const brokerParams{ - .vaultDeposit = 10000, - .debtMax = 0, - .coverRateMin = TenthBips32{0}, - .managementFeeRate = TenthBips16{0}, - .coverRateLiquidation = TenthBips32{0}}; - LoanParameters const loanParams{ - .account = Account("lender"), - .counter = Account("borrower"), - .principalRequest = Number{200000, -6}, - .interest = TenthBips32{50000}, - .payTotal = 2, - .payInterval = 200}; - - runLoan(AssetType::XRP, brokerParams, loanParams, all_); - } -}; - -BEAST_DEFINE_TESTSUITE(Loan, tx, xrpl); -BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, xrpl); -BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, xrpl); - -} // namespace xrpl::test diff --git a/src/test/app/LendingHelpers_test.cpp b/src/test/app/lending/LendingHelpers_test.cpp similarity index 100% rename from src/test/app/LendingHelpers_test.cpp rename to src/test/app/lending/LendingHelpers_test.cpp diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/lending/LoanBroker_test.cpp similarity index 99% rename from src/test/app/LoanBroker_test.cpp rename to src/test/app/lending/LoanBroker_test.cpp index ee398bfc3b..a610dbe931 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/lending/LoanBroker_test.cpp @@ -290,7 +290,7 @@ class LoanBroker_test : public beast::unit_test::Suite { auto const amount = vault.asset(n); BEAST_EXPECT(broker->at(sfCoverAvailable) == amount.number()); - env.require(Balance(pseudoAccount, amount)); + env.require(jtx::Balance(pseudoAccount, amount)); } }; @@ -537,8 +537,8 @@ class LoanBroker_test : public beast::unit_test::Suite auto const expectedBalance = aliceBalance + coverFunds - (aliceBalance.value().native() ? STAmount(env.current()->fees().base.value()) : vault.asset(0)); - env.require(Balance(alice, expectedBalance)); - env.require(Balance(pseudoAccount, vault.asset(kNone))); + env.require(jtx::Balance(alice, expectedBalance)); + env.require(jtx::Balance(pseudoAccount, vault.asset(kNone))); } } diff --git a/src/test/app/lending/LoanCashBasis_test.cpp b/src/test/app/lending/LoanCashBasis_test.cpp new file mode 100644 index 0000000000..c1334180c1 --- /dev/null +++ b/src/test/app/lending/LoanCashBasis_test.cpp @@ -0,0 +1,1013 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +// LendingProtocolV1_1 ("cash-basis" accounting) dedicated coverage. +// +// Existing tests never enable featureLendingProtocolV1_1 (see `all_` +// above), so these are the only tests in this file that exercise the +// amendment. They are called once, directly, from +// runAmendmentIndependent() -- not looped through +// runAmendmentSensitive()/amendmentCombinations(), since doing so would +// require re-deriving whole-life-specific expected values for ~15 +// unrelated regression tests. +class LoanCashBasis_test : public LoanTestBase +{ +private: + // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas, + // and the AssetsMaximum/DebtMaximum guards (which always check against + // principal + interestDue, regardless of the amendment). + void + testCashBasisLoanSetOrigination() + { + testcase("cash-basis: LoanSet origination"); + + using namespace jtx; + using namespace loan; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(10)}; + std::uint32_t const paymentTotal = 2; + std::uint32_t const paymentInterval = 86400; + + // Creates a broker/vault, submits a single LoanSet with a nonzero + // interest rate, and returns the observed Vault.AssetsTotal / + // LoanBroker.DebtTotal deltas plus the loan's own computed + // interestDue and principalOutstanding. + auto runOrigination = [&](FeatureBitset features) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBefore && brokerBefore); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + + auto const loanSequence = brokerBefore->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding); + Number const interestDue = totalValueOutstanding - principalOutstanding; + BEAST_EXPECT(interestDue > beast::kZero); + BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value()); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfter && brokerAfter); + Number const assetsTotalDelta = + Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; + Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; + + return std::make_tuple( + assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding); + }; + + Number interestDueCash{}; + Number principalOutstandingCash{}; + { + auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = + runOrigination(all_ | featureLendingProtocolV1_1); + interestDueCash = interestDue; + principalOutstandingCash = principalOutstanding; + + BEAST_EXPECTS( + assetsTotalDelta == beast::kZero, + "cash-basis origination must not change AssetsTotal; delta=" + + to_string(assetsTotalDelta)); + BEAST_EXPECTS( + debtTotalDelta == principalOutstanding, + "cash-basis origination must add principal-only to DebtTotal; delta=" + + to_string(debtTotalDelta) + " principal=" + to_string(principalOutstanding)); + } + + { + auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] = + runOrigination(all_); + + BEAST_EXPECTS( + assetsTotalDelta == interestDue, + "whole-life origination must add interestDue to AssetsTotal; delta=" + + to_string(assetsTotalDelta) + " interestDue=" + to_string(interestDue)); + BEAST_EXPECTS( + debtTotalDelta == principalOutstanding + interestDue, + "whole-life origination must add principal+interest to DebtTotal; delta=" + + to_string(debtTotalDelta)); + } + + // AssetsMaximum guard checks interestDue headroom only under + // whole-life accounting; DebtMaximum guard also varies by model. + auto runVaultGuard = [&](FeatureBitset features, Number const& slack, TER expected) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal); + + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = assetsTotalBefore + slack; + env(tx); + env.close(); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(expected)); + env.close(); + }; + + auto runBrokerGuard = [&](FeatureBitset features, Number const& debtMaximum, TER expected) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(1'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + env(loan_broker::set(lender, broker.vaultID), + loan_broker::kLoanBrokerId(broker.brokerID), + loan_broker::kDebtMaximum(debtMaximum), + Fee(env.current()->fees().base * 2)); + env.close(); + + env(set(borrower, broker.brokerID, xrpAsset(principalRequest).value()), + kCounterparty(lender), + kInterestRate(interestRate), + kPaymentTotal(paymentTotal), + kPaymentInterval(paymentInterval), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2), + Ter(expected)); + env.close(); + }; + + Number const oneDrop = xrpAsset(1).value(); + { + testcase("whole-life: LoanSet AssetsMaximum guard checks interestDue headroom"); + // Guard rejects when there's not quite enough headroom for the + // interest. + runVaultGuard(all_, interestDueCash - oneDrop, tecLIMIT_EXCEEDED); + // Guard accepts at the exact boundary. + runVaultGuard(all_, interestDueCash, tesSUCCESS); + } + + { + testcase("cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom"); + // Even far less headroom than interestDue still succeeds, since + // cash-basis origination never adds interest to AssetsTotal. + runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS); + } + + // DebtMaximum guard: cash-basis projects principal-only DebtTotal; + // whole-life projects principal + interestDue. + for (auto const cashBasis : {true, false}) + { + testcase( + std::string("LoanSet DebtMaximum guard (") + + (cashBasis ? "cash-basis)" : "whole-life)")); + auto const features = cashBasis ? all_ | featureLendingProtocolV1_1 : all_; + Number const newDebtTotal = + principalOutstandingCash + (cashBasis ? Number{} : interestDueCash); + runBrokerGuard(features, newDebtTotal - oneDrop, tecLIMIT_EXCEEDED); + runBrokerGuard(features, newDebtTotal, tesSUCCESS); + } + } + + // 2. LoanPay: regular, late, overpayment, and full-payment types. + // Assert Vault.AssetsTotal/LoanBroker.DebtTotal deltas match + // interestPaid/principalPaid under cash-basis, and cross-check the + // amendment-disabled run's deltas against the documented whole-life + // formula (AssetsTotal += valueChange; DebtTotal mirrors the loan's own + // TotalValueOutstanding delta exactly, since whole-life debt recognition + // tracks total loan value). + void + testCashBasisLoanPay() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + using tp = NetClock::time_point; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + Number const principalRequest{12'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 300; + + struct PaymentDeltas + { + Number principalPaid; + Number assetsTotalDelta; + Number debtTotalDelta; + Number totalValueDelta; + }; + + // Sets up a fresh broker + loan, advances time, submits a single + // payment of the given type/amount, and returns the observed deltas. + auto runPayment = [&](FeatureBitset features, + std::uint32_t loanSetFlags, + std::uint32_t payFlags, + std::function const& advanceTime, + std::function const& paymentAmount) { + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + .flags = loanSetFlags, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(loanParams(env, broker)); + env.close(); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + + advanceTime(env, state.startDate); + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + auto const loanBefore = env.le(loanKeylet); + BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore); + + Number const principalBefore = loanBefore->at(sfPrincipalOutstanding); + Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + + STAmount const amount = paymentAmount(state); + env(pay(borrower, loanKeylet.key, amount, payFlags), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + auto const loanAfter = env.le(loanKeylet); + BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter); + + Number const principalAfter = loanAfter->at(sfPrincipalOutstanding); + Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding); + Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); + Number const debtTotalAfter = brokerAfter->at(sfDebtTotal); + + return PaymentDeltas{ + .principalPaid = principalBefore - principalAfter, + .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore, + .debtTotalDelta = debtTotalAfter - debtTotalBefore, + .totalValueDelta = totalValueAfter - totalValueBefore}; + }; + + // Compares the disabled (whole-life) and enabled (cash-basis) runs + // of the same payment scenario, and asserts the documented + // relationships between them. + auto checkScenario = [&](std::string const& label, + PaymentDeltas const& off, + PaymentDeltas const& on) { + testcase("cash-basis: LoanPay " + label); + + // The loan's own PrincipalOutstanding field is untouched by + // the amendment. + BEAST_EXPECTS( + off.principalPaid == on.principalPaid, + "principalPaid must be amendment-independent; off=" + to_string(off.principalPaid) + + " on=" + to_string(on.principalPaid)); + + // Whole-life structural invariant: DebtTotal (which + // recognizes a loan's full remaining value as debt) must + // change exactly as the loan's own TotalValueOutstanding + // does. + BEAST_EXPECTS( + off.debtTotalDelta == off.totalValueDelta, + "whole-life DebtTotal delta must mirror TotalValueOutstanding delta; " + "debtTotalDelta=" + + to_string(off.debtTotalDelta) + + " totalValueDelta=" + to_string(off.totalValueDelta)); + + // Derive interestPaid from the whole-life run's independent + // ledger deltas: + // assetsTotalDelta_off == valueChange + // debtTotalDelta_off == valueChange - (principalPaid + interestPaid) + // => interestPaid == assetsTotalDelta_off - debtTotalDelta_off - principalPaid + Number const interestPaid = + off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid; + BEAST_EXPECTS( + interestPaid >= beast::kZero, + "derived interestPaid must be non-negative: " + to_string(interestPaid)); + + BEAST_EXPECTS( + on.assetsTotalDelta == interestPaid, + "cash-basis AssetsTotal delta must equal interestPaid; delta=" + + to_string(on.assetsTotalDelta) + " interestPaid=" + to_string(interestPaid)); + BEAST_EXPECTS( + on.debtTotalDelta == -on.principalPaid, + "cash-basis DebtTotal delta must equal -principalPaid; delta=" + + to_string(on.debtTotalDelta) + " principalPaid=" + to_string(on.principalPaid)); + }; + + // ---- Regular, on-time payment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const regularAmount = [&](LoanState const& state) { + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * + Number{3, -1} * 5}; // 1.5x, so only a single period is paid + }; + + auto const off = runPayment(all_, 0, 0, noAdvance, regularAmount); + auto const on = + runPayment(all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount); + + // Regular, on-time payments never change the loan's value beyond + // normal amortization (production asserts valueChange == 0), so + // AssetsTotal must be unaffected in the whole-life run. + BEAST_EXPECTS( + off.assetsTotalDelta == beast::kZero, + "regular on-time payment must not change AssetsTotal under whole-life; delta=" + + to_string(off.assetsTotalDelta)); + + checkScenario("regular payment", off, on); + } + + // ---- Late payment ---- + { + auto const advancePastDue = [&](Env& env, tp const& startDate) { + env.close(startDate + std::chrono::seconds(paymentInterval + 1)); + }; + auto const lateAmount = [&](LoanState const& state) { + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * + Number{3}}; // generous; excess is not withdrawn + }; + + auto const off = runPayment(all_, 0, tfLoanLatePayment, advancePastDue, lateAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, + 0, + tfLoanLatePayment, + advancePastDue, + lateAmount); + + checkScenario("late payment", off, on); + } + + // ---- Overpayment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const overpayAmount = [&](LoanState const& state) { + // One regular period, plus a generous extra principal + // paydown. + return STAmount{ + xrpAsset, + roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) + + xrpAsset(2'000).value()}; + }; + + auto const off = + runPayment(all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, + tfLoanOverpayment, + tfLoanOverpayment, + noAdvance, + overpayAmount); + + checkScenario("overpayment", off, on); + } + + // ---- Full payment ---- + { + auto const noAdvance = [](Env& env, tp const&) { env.close(); }; + auto const fullAmount = [&](LoanState const&) { + // Generously large: full payment only ever consumes exactly + // what's due (principal + accrued interest; close fee/ + // prepayment penalty are 0 here), excess is not withdrawn. + return STAmount{xrpAsset, xrpAsset(principalRequest).value() * Number{2}}; + }; + + auto const off = runPayment(all_, 0, tfLoanFullPayment, noAdvance, fullAmount); + auto const on = runPayment( + all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount); + + checkScenario("full payment", off, on); + } + } + + // 3. LoanManage: impair, unimpair, and default. + void + testCashBasisLoanManage() + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{percentageToTenthBips(10)}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + auto setupLoan = [&](Env& env) { + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + env(loanParams(env, broker)); + env.close(); + + return std::make_tuple(broker, loanKeylet, lender, borrower); + }; + + // ---- impair / unimpair ---- + auto runImpairUnimpair = [&](FeatureBitset features) { + Env env(*this, features); + auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); + + auto const loanBefore = env.le(loanKeylet); + BEAST_EXPECT(loanBefore); + Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding); + Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding); + + Number const expectedExposure = + env.current()->rules().enabled(featureLendingProtocolV1_1) + ? principalOutstanding + : totalValueOutstanding - managementFeeOutstanding; + + auto const vaultBeforeImpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultBeforeImpair); + Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized); + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterImpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultAfterImpair); + Number const impairDelta = Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore; + + env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterUnimpair = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultAfterUnimpair); + Number const netDelta = Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore; + + return std::make_tuple(expectedExposure, impairDelta, netDelta); + }; + + for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + { + testcase( + std::string("cash-basis: LoanManage impair/unimpair (") + + (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); + auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features); + + BEAST_EXPECTS( + impairDelta == expectedExposure, + "impair must add loanVaultExposure to LossUnrealized; delta=" + + to_string(impairDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + netDelta == beast::kZero, + "unimpair must be an exact reversal of impair; net=" + to_string(netDelta)); + } + + // ---- impair, then default ---- + auto runDefault = [&](FeatureBitset features) { + Env env(*this, features); + auto const [broker, loanKeylet, lender, borrower] = setupLoan(env); + + auto const loanBeforeImpair = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeImpair); + Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding); + Number const managementFeeOutstanding = + loanBeforeImpair->at(sfManagementFeeOutstanding); + + Number const expectedExposure = + env.current()->rules().enabled(featureLendingProtocolV1_1) + ? principalOutstanding + : totalValueOutstanding - managementFeeOutstanding; + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + env.close( + state.startDate + std::chrono::seconds(paymentInterval) + + std::chrono::seconds(gracePeriod) + 60s); + + auto const vaultBefore = env.le(broker.vaultKeylet()); + auto const brokerBefore = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBefore && brokerBefore); + Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal); + Number const debtTotalBefore = brokerBefore->at(sfDebtTotal); + Number const lossBefore = vaultBefore->at(sfLossUnrealized); + Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + auto const brokerAfter = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfter && brokerAfter); + Number const assetsTotalDelta = + Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore; + Number const debtTotalDelta = Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore; + Number const lossDelta = Number(vaultAfter->at(sfLossUnrealized)) - lossBefore; + Number const coverAvailableDelta = + Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore; + + Number const defaultCovered = -coverAvailableDelta; + Number const vaultDefaultAmount = expectedExposure - defaultCovered; + + return std::make_tuple( + expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount); + }; + + for (auto const features : {all_ | featureLendingProtocolV1_1, all_}) + { + testcase( + std::string("cash-basis: LoanManage default (") + + (features[featureLendingProtocolV1_1] ? "enabled)" : "disabled)")); + auto const + [expectedExposure, + assetsTotalDelta, + debtTotalDelta, + lossDelta, + vaultDefaultAmount] = runDefault(features); + + BEAST_EXPECTS( + debtTotalDelta == -expectedExposure, + "default must reduce DebtTotal by the unified default amount; delta=" + + to_string(debtTotalDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + lossDelta == -expectedExposure, + "default must reverse the earlier impair's LossUnrealized exactly; delta=" + + to_string(lossDelta) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + assetsTotalDelta == -vaultDefaultAmount, + "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" + + to_string(assetsTotalDelta) + " expected=" + to_string(-vaultDefaultAmount)); + } + } + + // 3b. LEVersion regression: a Vault created before featureLendingProtocolV1_1 + // activates (LEVersion absent) must keep whole-life (accrual) accounting + // forever, even after the amendment is later enabled -- the switch is + // per-Vault (LEVersion == VaultVersion::CashBasis), not a single global amendment + // flag. + void + testLegacyVaultKeepsAccrualAfterAmendmentEnabled() + { + testcase("LEVersion: legacy vault keeps accrual after amendment enabled"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 0, + .coverRateMin = TenthBips32{percentageToTenthBips(10)}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{percentageToTenthBips(25)}}; + + Number const principalRequest{10'000}; + TenthBips32 const interestRate{percentageToTenthBips(12)}; + std::uint32_t const paymentTotal = 4; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + // Amendment disabled at Vault creation time: LEVersion stays absent. + Env env(*this, all_); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + { + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); + } + + // Now enable the amendment -- production dispatch must still treat + // this specific Vault as accrual-basis, since its LEVersion is + // (and remains) absent. + env.enableFeature(featureLendingProtocolV1_1); + env.close(); + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = interestRate, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + + auto const brokerBeforeLoan = env.le(broker.brokerKeylet()); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + // ---- LoanSet origination: whole-life formulas expected ---- + auto const vaultBeforeSet = env.le(broker.vaultKeylet()); + auto const brokerBeforeSet = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet); + Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal); + Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal); + + env(loanParams(env, broker)); + env.close(); + + auto const loanAfterSet = env.le(loanKeylet); + BEAST_EXPECT(loanAfterSet); + Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding); + Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding); + Number const interestDue = totalValueOutstanding - principalOutstanding; + BEAST_EXPECT(interestDue > beast::kZero); + + auto const vaultAfterSet = env.le(broker.vaultKeylet()); + auto const brokerAfterSet = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfterSet && brokerAfterSet); + Number const assetsTotalDeltaSet = + Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet; + Number const debtTotalDeltaSet = + Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet; + + BEAST_EXPECTS( + assetsTotalDeltaSet == interestDue, + "legacy vault origination must still add interestDue to AssetsTotal; delta=" + + to_string(assetsTotalDeltaSet) + " interestDue=" + to_string(interestDue)); + BEAST_EXPECTS( + debtTotalDeltaSet == principalOutstanding + interestDue, + "legacy vault origination must still add principal+interest to DebtTotal; delta=" + + to_string(debtTotalDeltaSet)); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + env.close(); + + // ---- LoanPay: whole-life formulas expected ---- + auto const vaultBeforePay = env.le(broker.vaultKeylet()); + auto const brokerBeforePay = env.le(broker.brokerKeylet()); + auto const loanBeforePay = env.le(loanKeylet); + BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay); + Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding); + Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal); + Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal); + + STAmount const paymentAmount{ + xrpAsset, roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale)}; + env(pay(borrower, loanKeylet.key, paymentAmount), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterPay = env.le(broker.vaultKeylet()); + auto const brokerAfterPay = env.le(broker.brokerKeylet()); + auto const loanAfterPay = env.le(loanKeylet); + BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay); + Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding); + Number const assetsTotalDeltaPay = + Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay; + Number const debtTotalDeltaPay = + Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay; + Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay; + + // A regular, on-time payment has valueChange == 0, so whole-life + // AssetsTotal is untouched and DebtTotal mirrors TotalValueOutstanding. + BEAST_EXPECTS( + assetsTotalDeltaPay == beast::kZero, + "legacy vault regular payment must not change AssetsTotal; delta=" + + to_string(assetsTotalDeltaPay)); + BEAST_EXPECTS( + debtTotalDeltaPay == totalValueDeltaPay, + "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; " + "debtTotalDelta=" + + to_string(debtTotalDeltaPay) + " totalValueDelta=" + to_string(totalValueDeltaPay)); + + // ---- LoanManage: impair, then default -- whole-life exposure expected ---- + auto const loanBeforeImpair = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeImpair); + Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding); + Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding); + Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair; + + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + LoanState const stateAtImpair = getCurrentState(env, broker, loanKeylet); + env.close( + stateAtImpair.startDate + std::chrono::seconds(paymentInterval) + + std::chrono::seconds(gracePeriod) + 60s); + + auto const vaultBeforeDefault = env.le(broker.vaultKeylet()); + auto const brokerBeforeDefault = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault); + Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal); + Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfterDefault = env.le(broker.vaultKeylet()); + auto const brokerAfterDefault = env.le(broker.brokerKeylet()); + BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault); + Number const debtTotalDeltaDefault = + Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault; + Number const lossDeltaDefault = + Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault; + + BEAST_EXPECTS( + debtTotalDeltaDefault == -expectedExposure, + "legacy vault default must reduce DebtTotal by whole-life exposure; delta=" + + to_string(debtTotalDeltaDefault) + " expected=" + to_string(expectedExposure)); + BEAST_EXPECTS( + lossDeltaDefault == -expectedExposure, + "legacy vault default must reverse the earlier impair's LossUnrealized exactly; " + "delta=" + + to_string(lossDeltaDefault) + " expected=" + to_string(expectedExposure)); + + // Confirm the Vault's LEVersion truly never got set, throughout. + { + auto const vaultSle = env.le(broker.vaultKeylet()); + BEAST_EXPECT(vaultSle); + BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion)); + BEAST_EXPECT(getVaultVersion(vaultSle) == VaultVersion::Legacy); + } + } + + // 4. End-to-end trajectory: LoanSet -> 2 LoanPays -> LoanManage(default), + // entirely under the amendment, with independently hand-computed + // expected AssetsTotal/DebtTotal/LossUnrealized/CoverAvailable values at + // each step. 0% interest keeps the arithmetic exact and tractable; the + // divergence from whole-life accounting is already covered directly by + // testCashBasisLoanSetOrigination/LoanPay/LoanManage above, so this test + // focuses purely on an independent, from-scratch trajectory check. + void + testCashBasisEndToEndTrajectory() + { + testcase("cash-basis: end-to-end trajectory"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, .managementFeeRate = TenthBips16{0}}; + + Env env(*this, all_ | featureLendingProtocolV1_1); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + // Hand computation (all values in XRP, drops == 1e-6 XRP): + // Vault: AssetsTotal starts at 100'000 (the deposit). + // Broker: DebtTotal starts at 0, CoverAvailable starts at 1'000 + // (BrokerParameters::defaults().coverDeposit). + auto const vaultKeylet = broker.vaultKeylet(); + auto const brokerKeylet = broker.brokerKeylet(); + + // All the "human XRP unit" constants below (e.g. `100'000`) are + // converted to raw native (drops) values via xrpAsset(...), since + // that's how the ledger fields are actually denominated. + auto const checkVaultBroker = [&](Number const& assetsTotalUnits, + Number const& debtTotalUnits, + Number const& lossUnrealizedUnits, + Number const& coverAvailableUnits, + char const* step) { + Number const assetsTotal = xrpAsset(assetsTotalUnits).value(); + Number const debtTotal = xrpAsset(debtTotalUnits).value(); + Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value(); + Number const coverAvailable = xrpAsset(coverAvailableUnits).value(); + + auto const vaultSle = env.le(vaultKeylet); + auto const brokerSle = env.le(brokerKeylet); + BEAST_EXPECT(vaultSle && brokerSle); + BEAST_EXPECTS( + vaultSle->at(sfAssetsTotal) == assetsTotal, + std::string(step) + ": AssetsTotal expected " + to_string(assetsTotal) + " got " + + to_string(Number(vaultSle->at(sfAssetsTotal)))); + BEAST_EXPECTS( + brokerSle->at(sfDebtTotal) == debtTotal, + std::string(step) + ": DebtTotal expected " + to_string(debtTotal) + " got " + + to_string(Number(brokerSle->at(sfDebtTotal)))); + BEAST_EXPECTS( + vaultSle->at(sfLossUnrealized) == lossUnrealized, + std::string(step) + ": LossUnrealized expected " + to_string(lossUnrealized) + + " got " + to_string(Number(vaultSle->at(sfLossUnrealized)))); + BEAST_EXPECTS( + brokerSle->at(sfCoverAvailable) == coverAvailable, + std::string(step) + ": CoverAvailable expected " + to_string(coverAvailable) + + " got " + to_string(Number(brokerSle->at(sfCoverAvailable)))); + }; + + checkVaultBroker(100'000, 0, 0, 1'000, "before LoanSet"); + + // Loan: principal=1200, 0% interest, 12 payments of 100 each, no fees. + Number const principalRequest{1'200}; + std::uint32_t const paymentTotal = 12; + std::uint32_t const paymentInterval = 600; + std::uint32_t const gracePeriod = 60; + + auto const brokerBeforeLoan = env.le(brokerKeylet); + BEAST_EXPECT(brokerBeforeLoan); + auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .principalRequest = principalRequest, + .interest = TenthBips32{0}, + .payTotal = paymentTotal, + .payInterval = paymentInterval, + .gracePd = gracePeriod, + }; + env(loanParams(env, broker)); + env.close(); + + // Origination (cash-basis): AssetsTotal += 0, DebtTotal += principal. + checkVaultBroker(100'000, 1'200, 0, 1'000, "after LoanSet"); + + LoanState const state = getCurrentState(env, broker, loanKeylet); + BEAST_EXPECT(state.periodicPayment == xrpAsset(100).value()); + + // Payment 1: principalPaid=100, interestPaid=0. + // AssetsTotal += 0; DebtTotal -= 100. + env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); + env.close(); + checkVaultBroker(100'000, 1'100, 0, 1'000, "after payment 1"); + + // Payment 2: same as above. + env(pay(borrower, loanKeylet.key, xrpAsset(100).value()), Ter(tesSUCCESS)); + env.close(); + checkVaultBroker(100'000, 1'000, 0, 1'000, "after payment 2"); + + // Default (no impair): principalOutstanding remaining is 1'000. + // totalDefaultAmount (cash-basis) = PrincipalOutstanding = 1'000. + // minimumCover = DebtTotal(1'000) * coverRateMin(10%) = 100. + // covered = min(minimumCover * coverRateLiquidation(25%), totalDefaultAmount) + // = min(25, 1'000) = 25. + // defaultCovered = min(covered, CoverAvailable(1'000)) = 25. + // vaultDefaultAmount = 1'000 - 25 = 975. + // DebtTotal -= 1'000 -> 0. CoverAvailable -= 25 -> 975. + // AssetsTotal -= 975 -> 99'025. LossUnrealized unaffected (never impaired). + auto const loanBeforeDefault = env.le(loanKeylet); + BEAST_EXPECT(loanBeforeDefault); + BEAST_EXPECT( + Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value()); + + env.close(state.startDate + std::chrono::seconds((3 * paymentInterval) + gracePeriod) + 1s); + + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + checkVaultBroker(99'025, 0, 0, 975, "after LoanManage(default)"); + } + +public: + void + run() override + { + testCashBasisLoanSetOrigination(); + testCashBasisLoanPay(); + testCashBasisLoanManage(); + testLegacyVaultKeepsAccrualAfterAmendmentEnabled(); + testCashBasisEndToEndTrajectory(); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanCashBasis, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp new file mode 100644 index 0000000000..ec0238c173 --- /dev/null +++ b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp @@ -0,0 +1,722 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl::test { + +class LoanCoverFreezeAuth_test : public LoanTestBase +{ +private: + void + testSequentialFLCDepletion(FeatureBitset features) + { + testcase << "First-Loss Capital Depletion on Sequential Defaults"; + + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + Env env{*this, features}; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrowerA{"borrowerA"}; + Account const borrowerB{"borrowerB"}; + + env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB); + env.close(); + + PrettyAsset const asset = xrpIssue(); + auto const vaultDepositAmount = + asset(200'000); // Enough for 2 x 50k loans plus interest/fees + + auto const brokerInfo = createVaultAndBroker( + env, + asset, + lender, + { + .vaultDeposit = vaultDepositAmount.value(), + .debtMax = 0, + .coverRateMin = TenthBips32(20000), // 20% + .coverDeposit = 21'000, + .managementFeeRate = TenthBips16(100), // 0.1% + .coverRateLiquidation = TenthBips32(100000), + }); + auto const brokerKeylet = brokerInfo.brokerKeylet(); + + // Create two identical loans: each 50,000 XRP principal (scaled down to + // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal + // + interest) Formula will calculate cover as: 100% × (20% × 100,000) = + // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first + // default + auto const principalAmount = Number(50'000); + auto const loanPaymentInterval = 2592000; // 30 days + auto const loanGracePeriod = 604800; // 7 days + + // Create Loan A + auto loanATx = env.jt( + set(borrowerA, brokerKeylet.key, principalAmount), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(500)), // 5% + kPaymentTotal(12), + loan::kPaymentInterval(loanPaymentInterval), + loan::kGracePeriod(loanGracePeriod), + Fee(XRP(10))); // Sufficient fee for multi-sig transaction + env(loanATx); + env.close(); + + auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1); + + // Create Loan B + auto loanBTx = env.jt( + set(borrowerB, brokerKeylet.key, principalAmount), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(500)), // 5% + kPaymentTotal(12), + loan::kPaymentInterval(loanPaymentInterval), + loan::kGracePeriod(loanGracePeriod), + Fee(XRP(10))); // Sufficient fee for multi-sig transaction + env(loanBTx); + env.close(); + + auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2); + + auto loanASle = env.le(loanAKeylet); + if (!BEAST_EXPECT(loanASle)) + return; + + // Advance time past grace period for both loans to be defaultable + auto const loanANextDue = loanASle->at(sfNextPaymentDueDate); + auto const loanAGrace = loanASle->at(sfGracePeriod); + env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60}); + + env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + env.close(); + + // Verify Loan A is defaulted + loanASle = env.le(loanAKeylet); + if (!BEAST_EXPECT(loanASle)) + return; + BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault)); + BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0); + + // Check broker state after first default (from committed ledger) + auto brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal); + auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable); + + // DebtTotal should have decreased by Loan A's debt + BEAST_EXPECT(afterFirstDebtTotal == 50'134); + + // CoverAvailable should have decreased significantly + BEAST_EXPECT(afterFirstCoverAvailable == 946); + + env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS)); + + brokerSle = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal); + auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable); + + BEAST_EXPECT(afterSecondDebtTotal == 0); + + BEAST_EXPECT(afterSecondCoverAvailable == 0); + } + + // Tests that vault withdrawals work correctly when the vault has unrealized + // loss from an impaired loan, ensuring the invariant check properly + // accounts for the loss. + void + testWithdrawReflectsUnrealizedLoss(FeatureBitset features) + { + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + testcase("Vault withdraw reflects sfLossUnrealized"); + + // Test constants + static constexpr std::int64_t kInitialFunding = 1'000'000; + static constexpr std::int64_t kLenderInitialIou = 5'000'000; + static constexpr std::int64_t kDepositorInitialIou = 1'000'000; + static constexpr std::int64_t kBorrowerInitialIou = 100'000; + static constexpr std::int64_t kDepositAmount = 5'000; + static constexpr std::int64_t kPrincipalAmount = 99; + static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000; + static constexpr std::uint32_t kLocalPaymentInterval = 600; + static constexpr std::uint32_t kLocalPaymentTotal = 2; + + Env env{*this, features}; + + // Setup accounts + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const depositorA{"lpA"}; + Account const depositorB{"lpB"}; + Account const borrower{"borrowerA"}; + + env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower); + env.close(); + + // Setup trust lines + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(depositorA, iouAsset(10'000'000))); + env(trust(depositorB, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env.close(); + + // Fund accounts with IOUs + env(pay(issuer, lender, iouAsset(kLenderInitialIou))); + env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou))); + env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou))); + env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou))); + env.close(); + + // Create vault and broker, then add deposits from two depositors + auto const broker = createVaultAndBroker(env, iouAsset, lender); + Vault v{env}; + + env(v.deposit({ + .depositor = depositorA, + .id = broker.vaultKeylet().key, + .amount = iouAsset(kDepositAmount), + }), + Ter(tesSUCCESS)); + env(v.deposit({ + .depositor = depositorB, + .id = broker.vaultKeylet().key, + .amount = iouAsset(kDepositAmount), + }), + Ter(tesSUCCESS)); + env.close(); + + // Create a loan + auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(sleBroker)) + return; + + auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence)); + + env(set(borrower, broker.brokerID, kPrincipalAmount), + Sig(sfCounterpartySignature, lender), + kPaymentTotal(kLocalPaymentTotal), + kPaymentInterval(kLocalPaymentInterval), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + // Impair the loan to create unrealized loss + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS)); + env.close(); + + // Verify unrealized loss is recorded in the vault + auto const vaultAfterImpair = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultAfterImpair)) + return; + + BEAST_EXPECT( + vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value()); + + // Helper to get share balance for a depositor + auto const shareAsset = vaultAfterImpair->at(sfShareMPTID); + auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t { + auto const token = env.le(keylet::mptoken(shareAsset, depositor.id())); + return token ? token->getFieldU64(sfMPTAmount) : 0; + }; + + // Verify both depositors have equal shares + auto const sharesLpA = getShareBalance(depositorA); + auto const sharesLpB = getShareBalance(depositorB); + BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor); + BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor); + BEAST_EXPECT(sharesLpA == sharesLpB); + + // Helper to attempt withdrawal + auto const attemptWithdrawShares = [&](Account const& depositor, + std::uint64_t shareAmount, + TER expected) { + STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)}; + env(v.withdraw( + {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}), + Ter(expected)); + env.close(); + }; + + // Regression test: Both depositors should successfully withdraw despite + // unrealized loss. Previously failed with invariant violation: + // "withdrawal must change vault and destination balance by equal + // amount". This was caused by sharesToAssetsWithdraw rounding down, + // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta + // when unrealized loss exists. + attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS); + attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS); + } + + void + testServiceFeeOnBrokerDeepFreeze() + { + testcase << "Service Fee On Broker Deep Freeze"; + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + auto const iou = issuer["IOU"]; + + for (bool const deepFreeze : {true, false}) + { + Env env(*this); + + auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) { + if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(le)) + { + auto const account = le->at(accountField); + if (auto const sleLine = env.le(keylet::trustLine(account, iou)); + BEAST_EXPECT(sleLine)) + { + STAmount balance = sleLine->at(sfBalance); + if (account > issuer.id()) + balance.negate(); + return balance; + } + } + return STAmount{iou}; + }; + + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + env(trust(broker, iou(20'000'000))); + env(pay(issuer, broker, iou(10'000'000))); + env.close(); + + auto const brokerInfo = createVaultAndBroker(env, iou, broker); + + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); + + auto const keylet = keylet::loan(brokerInfo.brokerID, 1); + + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(iou(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + + env(trust(borrower, iou(20'000'000))); + // The borrower increases their limit and acquires some IOU so + // they can pay interest + env(pay(issuer, borrower, iou(500))); + env.close(); + + if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le)) + { + if (deepFreeze) + { + env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze)); + env.close(); + } + + env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100))); + env.close(); + + if (deepFreeze) + { + // The fee goes to the broker pseudo-account + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100)); + BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000)); + } + else + { + // The fee goes to the broker account + BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100)); + BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000)); + } + } + }; + } + + void + testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features) + { + testcase << "LoanPay Broker Owner Missing Trustline (PoC)"; + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + auto const iou = issuer["IOU"]; + Env env(*this, features); + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + // Set up trustlines and fund accounts + env(trust(broker, iou(20'000'000))); + env(trust(borrower, iou(20'000'000))); + env(pay(issuer, broker, iou(10'000'000))); + env(pay(issuer, borrower, iou(1'000))); + env.close(); + // 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); + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(iou(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = iou(50'000).value(); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover})); + env.close(); + // Verify broker owner has a trustline + auto const brokerTrustline = keylet::trustLine(broker, iou); + BEAST_EXPECT(env.le(brokerTrustline) != nullptr); + // Broker owner deletes their trustline + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, iou); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Remove the trustline by setting limit to 0 + env(trust(broker, iou(0))); + env.close(); + // Verify trustline is deleted + BEAST_EXPECT(env.le(brokerTrustline) == nullptr); + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_LINE. + env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); + env.close(); + // Verify trustline is still deleted + BEAST_EXPECT(env.le(brokerTrustline) == nullptr); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, iou); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance))); + } + } + + void + testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features) + { + testcase << "LoanPay Broker Owner MPT unauthorized"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env{*this, features}; + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + + PrettyAsset const mpt{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + + env.close(); + + // Fund accounts + env(pay(issuer, broker, mpt(10'000'000))); + env(pay(issuer, borrower, mpt(1'000))); + env.close(); + + // 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); + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(mpt(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = mpt(50'000).value(); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); + env.close(); + // Verify broker owner is authorized + auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); + BEAST_EXPECT(env.le(brokerMpt) != nullptr); + // Broker owner unauthorizes. + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, mpt); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Then, unauthorize the MPT. + mptt.authorize({.account = broker, .flags = tfMPTUnauthorize}); + env.close(); + // Verify the MPT is unauthorized. + BEAST_EXPECT(env.le(brokerMpt) == nullptr); + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_AUTH. + env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); + env.close(); + // Verify the MPT is still unauthorized. + BEAST_EXPECT(env.le(brokerMpt) == nullptr); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, mpt); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); + } + } + + void + testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) + { + testcase << "LoanPay Broker Owner without permissioned domain of the MPT"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env{*this, features}; + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + auto credType = "credential1"; + + pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}}; + env(pdomain::setTx(issuer, credentials1)); + env.close(); + + auto domainID = pdomain::getNewDomain(env.meta()); + + env(credentials::create(broker, issuer, credType)); + env(credentials::accept(broker, issuer, credType)); + env.close(); + + env(credentials::create(borrower, issuer, credType)); + env(credentials::accept(borrower, issuer, credType)); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({ + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, + .domainID = domainID, + }); + + PrettyAsset const mpt{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + + env.close(); + + // Fund accounts + env(pay(issuer, broker, mpt(10'000'000))); + env(pay(issuer, borrower, mpt(1'000))); + env.close(); + + // 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); + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(mpt(100).value()), + kPaymentInterval(100), + Fee(XRP(100))); + env.close(); + // Ensure broker has sufficient cover so brokerPayee == brokerOwner + // We need coverAvailable >= (debtTotal * coverRateMinimum) + // Deposit enough cover to ensure the fee goes to broker owner + // The default coverRateMinimum is 10%, so for a 10,000 loan we need + // at least 1,000 cover. Default cover is 1,000, so we add more to be + // safe. + auto const additionalCover = mpt(50'000).value(); + env(loan_broker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover})); + env.close(); + // Verify broker owner is authorized + auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker); + BEAST_EXPECT(env.le(brokerMpt) != nullptr); + // Remove the credentials for the Broker owner. + // First, pay any positive balance to issuer to zero it out + auto const brokerBalance = env.balance(broker, mpt); + env(pay(broker, issuer, brokerBalance)); + env.close(); + + env(credentials::deleteCred(broker, broker, issuer, credType)); + env.close(); + + // Make sure the broker is not authorized to hold the MPT after we + // deleted the credentials + env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); + + // Now borrower tries to make a payment + // We should get a tesSUCCESS instead of a tecNO_AUTH. + env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS)); + env.close(); + // Verify broker is still not authorized + env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH)); + // Verify the service fee went to the broker pseudo-account + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + Account const pseudo("pseudo-account", brokerSle->at(sfAccount)); + auto const balance = env.balance(pseudo, mpt); + // 1,000 default + 50,000 extra + 100 service fee from LoanPay + BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance))); + } + } + + void + testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features) + { + testcase << "LoanSet Broker Owner without permissioned domain of the MPT"; + using namespace jtx; + using namespace loan; + + Account const issuer("issuer"); + Account const borrower("borrower"); + Account const broker("broker"); + + Env env{*this, features}; + env.fund(XRP(20'000), issuer, broker, borrower); + env.close(); + + auto credType = "credential1"; + + pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}}; + env(pdomain::setTx(issuer, credentials1)); + env.close(); + + auto domainID = pdomain::getNewDomain(env.meta()); + + // Add credentials for the broker and borrower + env(credentials::create(broker, issuer, credType)); + env(credentials::accept(broker, issuer, credType)); + env.close(); + + env(credentials::create(borrower, issuer, credType)); + env(credentials::accept(borrower, issuer, credType)); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({ + .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock, + .domainID = domainID, + }); + + PrettyAsset const mpt{mptt.issuanceID()}; + + // Authorize broker and borrower + mptt.authorize({.account = broker}); + mptt.authorize({.account = borrower}); + env.close(); + + // Fund accounts + env(pay(issuer, broker, mpt(10'000'000))); + env(pay(issuer, borrower, mpt(1'000))); + env.close(); + + // Create vault and broker + auto const brokerInfo = createVaultAndBroker(env, mpt, broker); + + // Remove the credentials for the Broker owner. + // Clear the balance first. + auto const brokerBalance = env.balance(broker, mpt); + env(pay(broker, issuer, brokerBalance)); + env.close(); + // Delete the credentials + env(credentials::deleteCred(broker, broker, issuer, credType)); + env.close(); + + // Create a loan, this should fail for tecNO_AUTH + env(set(borrower, brokerInfo.brokerID, 10'000), + Sig(sfCounterpartySignature, broker), + kLoanServiceFee(mpt(100).value()), + kPaymentInterval(100), + Fee(XRP(100)), + Ter(tecNO_AUTH)); + env.close(); + } + + void + runAmendmentIndependent() + { + testServiceFeeOnBrokerDeepFreeze(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testSequentialFLCDepletion(features); + testWithdrawReflectsUnrealizedLoss(features); + testLoanPayBrokerOwnerMissingTrustline(features); + testLoanPayBrokerOwnerUnauthorizedMPT(features); + testLoanPayBrokerOwnerNoPermissionedDomainMPT(features); + testLoanSetBrokerOwnerNoPermissionedDomainMPT(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanCoverFreezeAuth, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanInvariants_test.cpp b/src/test/app/lending/LoanInvariants_test.cpp new file mode 100644 index 0000000000..381a3f8b48 --- /dev/null +++ b/src/test/app/lending/LoanInvariants_test.cpp @@ -0,0 +1,873 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test { + +class LoanInvariants_test : public LoanTestBase +{ +private: + // Each of these regression tests reproduces a single fuzzer-found (FIND-*) + // scenario against xrpl::detail::computePeriodicPayment / + // loanComputePaymentParts. They're merged into one function, one block + // per finding, because each is a narrow, self-contained repro that + // shares little beyond the surrounding scaffold. + void + testLoanPayComputePeriodicPaymentInvariants(FeatureBitset features) + { + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + + // From FIND-012 + { + testcase << "LoanPay xrpl::detail::computePeriodicPayment : " + "valid rate"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + BrokerParameters const brokerParams; + env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{640562, -5}; + + Number const serviceFee{2462611968}; + std::uint32_t const numPayments{4294967295 / 800}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + kLoanServiceFee(serviceFee), + kPaymentTotal(numPayments), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["CloseInterestRate"] = 55374; + createJson["ClosePaymentFee"] = "3825205248"; + createJson["LatePaymentFee"] = "237"; + createJson["LoanOriginationFee"] = "0"; + createJson["OverpaymentFee"] = 35167; + createJson["OverpaymentInterestRate"] = 1360; + createJson["PaymentInterval"] = 727; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + // Fails in preclaim because principal requested can't be + // represented as XRP + env(createJson, Ter(tecPRECISION_LOSS)); + env.close(); + + BEAST_EXPECT(!env.le(keylet)); + + Number const actualPrincipal{6}; + + createJson[sfPrincipalRequested] = actualPrincipal; + createJson.removeMember(sfSequence.jsonName); + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + // Fails in doApply because the payment is too small to be + // represented as XRP. + env(createJson, Ter(tecPRECISION_LOSS)); + env.close(); + } + + // From FIND-010 + { + testcase << "xrpl::loanComputePaymentParts : valid total interest"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["CloseInterestRate"] = 47299; + createJson["ClosePaymentFee"] = "3985819770"; + createJson["InterestRate"] = 92; + createJson["LatePaymentFee"] = "3866894865"; + createJson["LoanOriginationFee"] = "0"; + createJson["LoanServiceFee"] = "2348810240"; + createJson["OverpaymentFee"] = 58545; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 1; + createJson["PrincipalRequested"] = "0.000763058"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson); + env.close(); + + auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + loanPayTx["Amount"]["value"] = "0.000281284125490196"; + env(loanPayTx, Ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + } + + // From FIND-009 + { + testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid " + "rounded"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["ClosePaymentFee"] = "0"; + createJson["InterestRate"] = 24346; + createJson["LateInterestRate"] = 65535; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const baseFee = env.current()->fees().base; + + auto const stateBefore = getCurrentState(env, broker, keylet); + + { + auto loanPayTx = + env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{3074'745'058'823'529, -12}; + BEAST_EXPECT(to_string(amount) == "3074.745058823529"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); + env.close(); + } + + { + auto loanPayTx = + env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{6732'118'170'944'051, -12}; + BEAST_EXPECT(to_string(amount) == "6732.118170944051"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); + env.close(); + } + + auto const stateAfter = getCurrentState(env, broker, keylet); + // Total interest outstanding is non-negative + BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding); + // Principal paid is non-negative + BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding); + // Total value change is non-negative + BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue); + // Value delta is larger or same as principal delta (meaning + // non-negative interest paid) + BEAST_EXPECT( + (stateBefore.totalValue - stateAfter.totalValue) >= + (stateBefore.principalOutstanding - stateAfter.principalOutstanding)); + } + + // From FIND-008 + { + testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded"; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = + createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + { + auto const coverDepositValue = + broker.asset(broker.params.coverDeposit * 10).value(); + env(loan_broker::coverDeposit(lender, broker.brokerID, coverDepositValue)); + env.close(); + } + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["ClosePaymentFee"] = "0"; + createJson["InterestRate"] = 12833; + createJson["LateInterestRate"] = 77048; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 752; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const baseFee = env.current()->fees().base; + + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining == 5678); + BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); + + auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}})); + Number const amount{9924'81, -2}; + BEAST_EXPECT(to_string(amount) == "9924.81"); + XRPAmount const payFee{ + baseFee * + (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)}; + loanPayTx["Amount"]["value"] = to_string(amount); + env(loanPayTx, Fee(payFee), Ter(tesSUCCESS)); + env.close(); + + auto const stateAfter = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + stateAfter.paymentRemaining == + stateBefore.paymentRemaining - kLoanMaximumPaymentsPerTransaction); + } + } + + void + testLoanPayDebtDecreaseInvariant(FeatureBitset features) + { + // From FIND-007 + testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease " + "rounding good"; + + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const baseFee = env.current()->fees().base; + auto const loanSetFee = Fee(baseFee * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["ClosePaymentFee"] = "0"; + createJson["GracePeriod"] = 60; + createJson["InterestRate"] = 24346; + createJson["LateInterestRate"] = 65535; + createJson["LatePaymentFee"] = "0"; + createJson["LoanOriginationFee"] = "218"; + createJson["LoanServiceFee"] = "0"; + createJson["PaymentInterval"] = 60; + createJson["PaymentTotal"] = 5678; + createJson["PrincipalRequested"] = "9924.81"; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const pseudoAcct = brokerPseudoAccount(env, broker, lender); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); + auto const originalState = getCurrentState(env, broker, keylet); + verifyLoanStatus(originalState); + + Number const payment{3'269'349'176'470'588, -12}; + XRPAmount const payFee{ + baseFee * + ((payment / originalState.periodicPayment) / kLoanPaymentsPerFeeIncrement + 1)}; + auto loanPayTx = + env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), Fee(payFee)); + BEAST_EXPECT(to_string(payment) == "3269.349176470588"); + env(loanPayTx, Ter(tesSUCCESS)); + env.close(); + + auto const newState = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale)); + BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding); + BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale)); + BEAST_EXPECT( + isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale)); + } + + void + testAccountSendMptMinAmountInvariant(FeatureBitset features) + { + // (From FIND-006) + testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount " + "and MPT"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + env(pay(issuer, lender, mptAsset(2'000'000))); + env(pay(issuer, borrower, mptAsset(1'000))); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["CloseInterestRate"] = 76671; + createJson["ClosePaymentFee"] = "2061925410"; + createJson["GracePeriod"] = 434; + createJson["InterestRate"] = 50302; + createJson["LateInterestRate"] = 30322; + createJson["LatePaymentFee"] = "294427911"; + createJson["LoanOriginationFee"] = "3250635102"; + createJson["LoanServiceFee"] = "9557386"; + createJson["OverpaymentFee"] = 51249; + createJson["OverpaymentInterestRate"] = 14304; + createJson["PaymentInterval"] = 434; + createJson["PaymentTotal"] = "2891743748"; + createJson["PrincipalRequested"] = "8516.98"; + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(temINVALID)); + env.close(); + } + + // Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the + // same vault-scale minimum cover when fixCleanup3_2_0 is enabled. + // Before the amendment, each transactor computed its minimum cover at a + // different precision (loanScale, debtScale, or the raw unrounded + // tenthBipsOfValue), which could lead to inconsistent decisions for the + // same broker state. After the amendment all three use + // minimumBrokerCover at vaultScale. + void + testMinimumBrokerCoverConsistency(FeatureBitset features) + { + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + bool const withAmendment = features[fixCleanup3_2_0]; + + struct Ctx + { + jtx::Account issuer; + jtx::Account lender; + jtx::Account borrower; + jtx::PrettyAsset iou; + BrokerInfo broker; + BrokerParameters brokerParams; + }; + + // Shared setup, parametrized by vaultDeposit (the only varying setup + // field across the three scenarios). Each call runs in its own Env + // so multiple invocations within one scenario cannot interfere. + // The caller is responsible for invoking testcase(...) before the + // first runTest call of each scenario. + auto runTest = [&](Number vaultDeposit, auto&& body) { + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + + // Enable clawback on the issuer *before* any trust lines exist + // (asfAllowTrustLineClawback requires an empty owner directory). + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const iou = issuer[iouCurrency_]; + env(trust(lender, iou(1'000'000'000))); + env(trust(borrower, iou(1'000'000'000))); + env.close(); + env(pay(issuer, lender, iou(100'000'000))); + env(pay(issuer, borrower, iou(100'000'000))); + env.close(); + + // 13.37% — non-round rate produces a messier minimum. + BrokerParameters const brokerParams{ + .vaultDeposit = vaultDeposit, + .debtMax = 0, + .coverRateMin = TenthBips32{13'370}, + .coverDeposit = 5'000, + .managementFeeRate = TenthBips16{500}}; + + BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams); + + body( + env, + Ctx{.issuer = issuer, + .lender = lender, + .borrower = borrower, + .iou = iou, + .broker = broker, + .brokerParams = brokerParams}); + }; + + // Scenario 1 — LoanPay + // + // Verify that LoanPay's minimum cover check uses vault scale (not + // loan scale). Before the amendment, different loans could produce + // different fee routing decisions for the same broker-level state. + // Small vault deposit => vaultScale = -12. + testcase("LoanPay minimum cover scale consistency"); + { + struct LoanKeylets + { + Keylet tiny; + Keylet big; + }; + + // Create the tiny + big loans and reduce cover via clawback so + // that subsequent LoanPay calls hit the minimum-cover boundary. + // Used by the two pay-and-check sub-tests below so each can run + // in its own Env. + auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional { + Asset const asset{c.iou}; + + // Create the TINY loan first (while vaultScale is still + // small). principal 0.01, 0% interest, 1 payment => + // loanScale = vaultScale. + auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle1)) + return std::nullopt; + auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence); + auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq); + + env(set(c.borrower, c.broker.brokerID, Number{1, -2}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // Create the BIG loan second. 100% annual interest over 20 + // payments pushes totalValueOutstanding high enough that + // loanScale > vaultScale. + auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle2)) + return std::nullopt; + auto const bigLoanSeq = brokerSle2->at(sfLoanSequence); + auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq); + + env(set(c.borrower, c.broker.brokerID, Number{500}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{100'000}), + kPaymentTotal(20), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // The tiny loan's scale is frozen at the vault's pre-big-loan + // scale, so it is strictly smaller than the big loan's. + // After the big loan is created the vault absorbs its value, + // pushing vaultScale up to match bigLoanScale. + auto const tinyLoanSle = env.le(tinyLoanKeylet); + auto const bigLoanSle = env.le(bigLoanKeylet); + auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); + if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) || + !BEAST_EXPECT(vaultSle)) + return std::nullopt; + if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) || + !BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) || + !BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11)) + return std::nullopt; + + // Use issuer clawback to reduce cover to the minimum the + // clawback transactor allows. Compute the amount as + // initialCover - expectedCoverAfter so we exercise the exact + // clawback rather than relying on the transactor to clip + // down. + // + // Before the amendment the clawback minimum is the + // *unrounded* tenthBipsOfValue — strictly less than the + // rounded-at-vaultScale minimum LoanPay uses for the big + // loan. After the amendment both clawback and LoanPay use + // the same rounded minimum (via minimumBrokerCover), so + // cover lands exactly at that threshold. + Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} + : Number{1330651855688458000, -15}; + Number const clawbackAmount = + Number{c.brokerParams.coverDeposit} - expectedCoverAfter; + + env(coverClawback(c.issuer), + kLoanBrokerId(c.broker.brokerID), + kAmount(STAmount{asset, clawbackAmount})); + env.close(); + + auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle) || + !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter)) + return std::nullopt; + + return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet}; + }; + + // Pay one loan and report whether the fee went to the broker's + // pseudo account (the fallback when cover < minimum) rather + // than to the owner. + auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool { + Asset const asset{c.iou}; + auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return false; + auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount)); + auto const pseudoBefore = env.balance(pseudoAcct, c.iou); + + auto const payLoan = env.le(loanKeylet); + if (!BEAST_EXPECT(payLoan)) + return false; + auto const periodicPayment = payLoan->at(sfPeriodicPayment); + auto const serviceFee = payLoan->at(sfLoanServiceFee); + std::int32_t const loanScale = payLoan->at(sfLoanScale); + + auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale); + auto const payAmt = STAmount{asset, payment + serviceFee}; + + env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10))); + env.close(); + + auto const pseudoAfter = env.balance(pseudoAcct, c.iou); + return pseudoAfter.number() > pseudoBefore.number(); + }; + + // Pay the BIG loan in its own Env so its outcome cannot affect + // the TINY-loan check. With the fix, LoanPay and clawback use + // the same vaultScale minimum (cover == minAtVaultScale => + // fee to owner). Without the fix, LoanPay uses bigLoanScale=-11, + // rounds up to a larger minimum than what clawback used => + // cover < min => fee to pseudo. + runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { + auto const loans = setupLoansAndClawback(env, c); + if (!loans) + return; + BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment); + }); + + // Pay the TINY loan in its own Env. Fee goes to the owner + // either way: + // - With the fix: LoanPay uses vaultScale=-11 (same as + // clawback) => owner. + // - Without the fix: LoanPay uses tinyLoanScale=-12, rounds + // up at -12 (a no-op) => min == cover => owner. + runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { + auto const loans = setupLoansAndClawback(env, c); + if (!loans) + return; + BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny)); + }); + } + + // Scenario 2 — LoanBrokerCoverWithdraw + // + // Verify that CoverWithdraw's minimum cover check uses vault scale + // (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw + // used: + // roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset)) + // which could disagree with LoanPay's minimum (which used loanScale). + // + // Use a large vault deposit so that vaultScale (from AssetsTotal) is + // strictly larger than debtScale (from DebtTotal). With + // vaultDeposit = 100,000: after the big loan + // AssetsTotal ≈ 109,500 → vaultScale = -10 + // DebtTotal ≈ 10,000 → debtScale = -11 + // The one-order-of-magnitude gap makes roundToAsset at -10 truncate + // more aggressively than at -11, exposing the bug. + testcase("CoverWithdraw minimum cover scale consistency"); + runTest( + /*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) { + Asset const asset{c.iou}; + + // Create only the big loan to push DebtTotal up to ~10,000 + // while AssetsTotal stays around 109,500 (dominated by the + // large vault deposit). + env(set(c.borrower, c.broker.brokerID, Number{500}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{100'000}), + kPaymentTotal(20), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // Read broker state and compute both old and new minimums. + auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID)); + auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); + if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle)) + return; + + auto const coverAvail = brokerSle->at(sfCoverAvailable); + auto const debtTotal = brokerSle->at(sfDebtTotal); + auto const vaultScale = getAssetsTotalScale(vaultSle); + auto const debtScale = scale(debtTotal, asset); + + // Sanity: debt scale differs from vault scale for this setup. + BEAST_EXPECT(debtScale < vaultScale); + + auto const oldMin = [&]() { + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + return roundToAsset( + asset, + tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}), + debtScale); + }(); + auto const newMin = minimumBrokerCover( + debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle); + + // The new (vaultScale) minimum must be strictly larger than + // the old (debtScale) minimum — that is the gap the amendment + // closes. + Number const expectedNewMin{1330650518688500000, -15}; + Number const expectedOldMin{1330650518688472000, -15}; + BEAST_EXPECT(newMin == expectedNewMin); + BEAST_EXPECT(oldMin == expectedOldMin); + + // Try to withdraw so that remaining cover lands between the + // two minimums: oldMin < target < newMin. + auto const target = oldMin + (newMin - oldMin) / 2; + auto const withdrawAmount = STAmount{asset, coverAvail - target}; + + if (withAmendment) + { + // CoverWithdraw now uses vaultScale: target < newMin + // => FAILS. + env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount), + Ter(tecINSUFFICIENT_FUNDS)); + } + else + { + // Old CoverWithdraw uses debtScale: target > oldMin + // => SUCCEEDS. + env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount)); + } + env.close(); + }); + + // Scenario 3 — LoanSet + // + // Verify that LoanSet's minimum cover check uses vault scale (not the + // raw unrounded tenthBipsOfValue). Before the amendment, LoanSet + // used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no + // roundToAsset), while clawback/withdraw used different formulas. + // After the amendment all use minimumBrokerCover at vaultScale, and + // rounding at a coarser scale can absorb a tiny debt increase — + // allowing a loan that would otherwise be rejected. + testcase("LoanSet minimum cover scale consistency"); + runTest( + /*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) { + // Create the tiny loan (scale -12) AND the big loan (scale + // -11). Both loans are needed so that DebtTotal has a full + // 16-digit mantissa — a "messy" value where roundToAsset at + // vaultScale actually truncates digits and produces a + // different result from the raw tenthBipsOfValue. With only + // the big loan, DebtTotal has ~4 significant digits and + // rounding at scale -11 is a no-op, masking the amendment's + // effect. + env(set(c.borrower, c.broker.brokerID, Number{1, -2}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + env(set(c.borrower, c.broker.brokerID, Number{500}), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{100'000}), + kPaymentTotal(20), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + env.close(); + + // Clawback to reduce cover to the clawback transactor's + // minimum. Pass the exact amount rather than relying on the + // transactor to clip down; the setup matches Scenario 1 so + // the same residual-cover values apply. + Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15} + : Number{1330651855688458000, -15}; + Number const clawbackAmount = + Number{c.brokerParams.coverDeposit} - expectedCoverAfter; + env(coverClawback(c.issuer), + kLoanBrokerId(c.broker.brokerID), + kAmount(c.iou(clawbackAmount))); + env.close(); + + // Verify scales. + auto const vaultSle = env.le(keylet::vault(c.broker.vaultID)); + if (!BEAST_EXPECT(vaultSle)) + return; + auto const vaultScale = getAssetsTotalScale(vaultSle); + BEAST_EXPECT(vaultScale == -11); + + // Now try to create a tiny additional loan. Principal is + // 1e-11 (the smallest value that survives the precision + // check at loanScale = vaultScale = -11), with 0% interest + // and 1 payment. + // + // The tiny debt increase adds ~1.337e-12 to the unrounded + // minimum. + // - Without the amendment: the old LoanSet formula rounds + // up during tenthBipsOfValue (16-digit Number + // normalisation), pushing the minimum past the cover left + // by clawback => tecINSUFFICIENT_FUNDS. + // - With the amendment: minimumBrokerCover rounds at + // vaultScale=-11, which absorbs the tiny increase — the + // rounded minimum stays the same => tesSUCCESS. + auto const tinyPrincipal = Number{1, -11}; + + if (withAmendment) + { + env(set(c.borrower, c.broker.brokerID, tinyPrincipal), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10))); + } + else + { + env(set(c.borrower, c.broker.brokerID, tinyPrincipal), + Sig(sfCounterpartySignature, c.lender), + kInterestRate(TenthBips32{0}), + kPaymentTotal(1), + kPaymentInterval(86400 * 365), + Fee(XRP(10)), + Ter(tecINSUFFICIENT_FUNDS)); + } + env.close(); + }); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testLoanPayComputePeriodicPaymentInvariants(features); + testLoanPayDebtDecreaseInvariant(features); + testAccountSendMptMinAmountInvariant(features); + testMinimumBrokerCoverConsistency(features); + } + +public: + void + run() override + { + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanInvariants, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanLifecycle_test.cpp b/src/test/app/lending/LoanLifecycle_test.cpp new file mode 100644 index 0000000000..e9868769b7 --- /dev/null +++ b/src/test/app/lending/LoanLifecycle_test.cpp @@ -0,0 +1,689 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanLifecycle_test : public LoanTestBase +{ +private: + void + testLifecycle(FeatureBitset features) + { + testcase("Lifecycle"); + using namespace jtx; + + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. + Env env(*this, features); + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + // Evan will attempt to be naughty + Account const evan{"evan"}; + // Do not fund alice + Account const alice{"alice"}; + + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); + env.close(); + + // Create assets + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env(trust(evan, iouAsset(10'000'000))); + env(pay(issuer, evan, iouAsset(1'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, iouAsset(10'000))); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + // Scale the MPT asset a little bit so we can get some interest + PrettyAsset const mptAsset{mptt.issuanceID(), 100}; + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + mptt.authorize({.account = evan}); + env(pay(issuer, lender, mptAsset(10'000'000))); + env(pay(issuer, evan, mptAsset(1'000'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, mptAsset(10'000))); + env.close(); + + std::array const assets{iouAsset, xrpAsset, mptAsset}; + + // Create vaults and loan brokers + std::vector brokers; + brokers.reserve(assets.size()); + for (auto const& asset : assets) + { + brokers.emplace_back(createVaultAndBroker( + env, asset, lender, BrokerParameters{.data = "spam spam spam spam"})); + } + + // Create and update Loans + for (auto const& broker : brokers) + { + for (int amountExponent = 3; amountExponent >= 3; --amountExponent) + { + Number const loanAmount{1, amountExponent}; + for (int interestExponent = 0; interestExponent >= 0; --interestExponent) + { + testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent); + } + } + + if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0); + + auto const coverAvailable = brokerSle->at(sfCoverAvailable); + env(loan_broker::coverWithdraw( + lender, broker.brokerID, STAmount(broker.asset, coverAvailable))); + env.close(); + + brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0); + } + // Verify we can delete the loan broker + env(loan_broker::del(lender, broker.brokerID)); + env.close(); + } + } + + void + testSelfLoan(FeatureBitset features) + { + testcase << "Self Loan"; + + using namespace jtx; + using namespace std::chrono_literals; + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. + Env env(*this, features); + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender)); + env.close(); + + // Use an XRP asset for simplicity + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + // Create vaults and loan brokers + BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + // The LoanSet json can be created without a counterparty signature, + // but it will not pass preflight + auto createJson = env.json( + set(lender, broker.brokerID, broker.asset(principalRequest).value()), Fee(loanSetFee)); + env(createJson, Ter(temBAD_SIGNER)); + + // Adding an empty counterparty signature object also fails, but + // at the RPC level. + createJson = env.json(createJson, Json(sfCounterpartySignature, json::ValueType::Object)); + env(createJson, Ter(telENV_RPC_FAILED)); + + if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx)) + { + Serializer s; + jt.stx->add(s); + auto const jr = env.rpc("submit", strHex(s.slice())); + + BEAST_EXPECT(jr.isMember(jss::result)); + auto const jResult = jr[jss::result]; + BEAST_EXPECT(jResult[jss::error] == "invalidTransaction"); + BEAST_EXPECT( + jResult[jss::error_exception] == + "fails local checks: Transaction has bad signature."); + } + + // Copy the transaction signature into the counterparty signature. + json::Value counterpartyJson{json::ValueType::Object}; + counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; + counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; + if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) + counterpartyJson[sfSigners] = createJson[sfSigners]; + + // The duplicated signature works + createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); + env(createJson); + + env.close(); + + auto const startDate = env.current()->header().parentCloseTime; + + // Loan is successfully created + { + auto const res = env.rpc("account_objects", lender.human()); + auto const objects = res[jss::result][jss::account_objects]; + + std::map types; + BEAST_EXPECT(objects.size() == 4); + for (auto const& object : objects) + { + ++types[object[sfLedgerEntryType].asString()]; + } + BEAST_EXPECT(types.size() == 4); + for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"}) + { + BEAST_EXPECT(types[type] == 1); + } + } + auto const loanID = [&]() { + json::Value params(json::ValueType::Object); + params[jss::account] = lender.human(); + params[jss::type] = "Loan"; + auto const res = env.rpc("json", "account_objects", to_string(params)); + auto const objects = res[jss::result][jss::account_objects]; + + BEAST_EXPECT(objects.size() == 1); + + auto const loan = objects[0u]; + BEAST_EXPECT(loan[sfBorrower] == lender.human()); + // soeDEFAULT fields are not returned if they're in the default + // state + BEAST_EXPECT(!loan.isMember(sfCloseInterestRate)); + BEAST_EXPECT(!loan.isMember(sfClosePaymentFee)); + BEAST_EXPECT(loan[sfFlags] == 0); + BEAST_EXPECT(loan[sfGracePeriod] == 60); + BEAST_EXPECT(!loan.isMember(sfInterestRate)); + BEAST_EXPECT(!loan.isMember(sfLateInterestRate)); + BEAST_EXPECT(!loan.isMember(sfLatePaymentFee)); + BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID)); + BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee)); + BEAST_EXPECT(loan[sfLoanSequence] == 1); + BEAST_EXPECT(!loan.isMember(sfLoanServiceFee)); + BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60); + BEAST_EXPECT(!loan.isMember(sfOverpaymentFee)); + BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate)); + BEAST_EXPECT(loan[sfPaymentInterval] == 60); + BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000"); + BEAST_EXPECT(loan[sfPaymentRemaining] == 1); + BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate)); + BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); + BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000"); + BEAST_EXPECT(!loan.isMember(sfLoanScale)); + BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); + + return loan["index"].asString(); + }(); + auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})}; + + env.close(startDate); + + // Make a payment + env(pay(lender, loanKeylet.key, broker.asset(1000))); + } + + void + testIssuerLoan() + { + testcase << "Issuer Loan"; + + using namespace jtx; + using namespace loan; + Account const issuer("issuer"); + Account const borrower = issuer; + Account const lender("lender"); + Env env(*this); + + env.fund(XRP(1'000), issuer, lender); + + static constexpr std::int64_t kIssuerBalance = 10'000'000; + MPTTester const asset( + {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance}); + + BrokerParameters const brokerParams{ + .debtMax = 200, + }; + auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); + auto const loanSetFee = Fee(env.current()->fees().base * 2); + // Create Loan + env(set(borrower, broker.brokerID, 200), Sig(sfCounterpartySignature, lender), loanSetFee); + env.close(); + // Issuer should not create MPToken + BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer))); + // 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); + env(pay(borrower, loanKeylet.key, asset(200))); + env.close(); + // Issuer "re-payed" 200, OutstandingAmount increased by 200 + BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance)); + } + + void + testBorrowerIsBroker() + { + testcase("Test Borrower is Broker"); + using namespace jtx; + using namespace loan; + Account const broker{"broker"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const depositor{"depositor"}; + + auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) { + Env env(*this); + Vault const vault(env); + + if (borrower == broker) + { + env.fund(XRP(10'000), broker, issuer, depositor); + } + else + { + env.fund(XRP(10'000), broker, borrower, issuer, depositor); + } + env.close(); + + auto const xrpFee = XRP(100); + auto const txFee = Fee(xrpFee); + + STAmount const debtMaximumRequest = getMaxDebt(env); + + auto const& asset = debtMaximumRequest.asset(); + auto const initialVault = asset(debtMaximumRequest * 100); + + auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset}); + env(tx, txFee); + env.close(); + + env(vault.deposit( + {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}), + txFee); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker)); + + env(loan_broker::set(broker, vaultKeylet.key), txFee); + env.close(); + + auto const serviceFee = 101; + + env(set(broker, brokerKeylet.key, debtMaximumRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + kLoanServiceFee(serviceFee), + kPaymentTotal(10), + txFee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); + + auto const brokerBalanceBefore = env.balance(broker, asset); + + if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle)) + { + auto const payment = loanSle->at(sfPeriodicPayment); + auto const totalPayment = payment + serviceFee; + env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee); + env.close(); + if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle)) + { + auto const expected = [&]() { + // The service fee is transferred to the broker if + // a borrower is not the broker + if (borrower != broker) + return brokerBalanceBefore.number() + serviceFee; + // Since a borrower is the broker, the payment is + // transferred to the Vault from the broker but not + // the service fee. + // If the asset is XRP then the broker pays the txFee. + if (asset.native()) + return brokerBalanceBefore.number() - payment - xrpFee.number(); + return brokerBalanceBefore.number() - payment; + }(); + BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value()); + } + } + }; + // Test when a borrower is the broker and is not to verify correct + // service fee transfer in both cases. + for (auto const& borrowerAcct : {broker, borrower}) + { + testLoanAsset( + [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct); + testLoanAsset( + [&](Env& env) -> STAmount { + auto const iou = issuer["USD"]; + env(trust(broker, iou(1'000'000'000))); + env(trust(depositor, iou(1'000'000'000))); + env(pay(issuer, broker, iou(100'000'000))); + env(pay(issuer, depositor, iou(100'000'000))); + env.close(); + return iou(200'000); + }, + borrowerAcct); + testLoanAsset( + [&](Env& env) -> STAmount { + MPTTester const mpt( + {.env = env, + .issuer = issuer, + .holders = {broker, depositor}, + .pay = 100'000'000}); + return mpt(200'000); + }, + borrowerAcct); + } + } + + void + testIssuerIsBorrower(FeatureBitset features) + { + testcase("RIPD-4096 - Issuer as borrower"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, .counter = issuer, .principalRequest = Number{10000}}; + + auto const assetType = AssetType::IOU; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + issuer, + PaymentParameters{.showStepBalances = true}); + } + + void + testBatchBypassCounterparty(FeatureBitset features) + { + // From FIND-001 + testcase << "Batch Bypass Counterparty"; + + bool const lendingBatchEnabled = !std::ranges::any_of( + Batch::kDisabledTxTypes, [](auto const& disabled) { return disabled == ttLOAN_SET; }); + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, features); + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + BrokerParameters const brokerParams; + env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0); + + json::Value randomData{json::ValueType::Object}; + randomData[jss::SigningPubKey] = json::StaticString{"2600"}; + json::Value sigObject{json::ValueType::Object}; + sigObject[jss::SigningPubKey] = strHex(lender.pk().slice()); + Serializer ss; + ss.add32(HashPrefix::TxSign); + parse(randomData).addWithoutSigningFields(ss); + auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice()); + sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + + forgedLoanSet[json::StaticString{"CounterpartySignature"}] = sigObject; + + // ? Fails because the lender hasn't signed the tx + env(env.json(forgedLoanSet, Fee(loanSetFee)), Ter(telENV_RPC_FAILED)); + + auto const seq = env.seq(borrower); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + // ! Should fail because the lender hasn't signed the tx + env(batch::outer(borrower, seq, batchFee, tfAllOrNothing), + batch::Inner(forgedLoanSet, seq + 1), + batch::Inner(pay(borrower, lender, XRP(1)), seq + 2), + Ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH)); + env.close(); + + // ? Check that the loan was NOT created + { + json::Value params(json::ValueType::Object); + params[jss::account] = borrower.human(); + params[jss::type] = "Loan"; + auto const res = env.rpc("json", "account_objects", to_string(params)); + auto const objects = res[jss::result][jss::account_objects]; + BEAST_EXPECT(objects.size() == 0); + } + } + + // Integration test: full lifecycle of a $1B loan in the bug regime. + // Verifies that the vault collects the economically-correct interest + // income and that conservation holds at the trust-line level. + // + // Pre-fix (closed-form `power(1+r, n) - 1`): vault collected only + // ~$0.058 per $1B due to cancellation of `(1+r)^n - 1` at r*n ~ 5.7e-10. + // Post-fix (hybrid binomial path): vault collects ~$0.38 per $1B, + // matching the value computed independently with arbitrary-precision + // Decimal arithmetic. + void + testFullLifecycleVaultPnLNearZeroRate() + { + testcase("integration: full loan lifecycle, vault interest at near-zero rate"); + + using namespace jtx; + using namespace jtx::loan; + using namespace std::chrono_literals; + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + PrettyAsset const iouAsset = issuer["USD"]; + STAmount const trustLimit{iouAsset.raw(), Number{1, 17}}; + env(trust(lender, trustLimit)); + env(trust(borrower, trustLimit)); + env.close(); + env(pay(issuer, lender, iouAsset(5'000'000'000LL))); + env(pay(issuer, borrower, iouAsset(5'000'000'000LL))); + env.close(); + + auto usdBalance = [&](Account const& a) { + return env.balance(a, iouAsset.raw().get()).value(); + }; + STAmount const borrowerStartBal = usdBalance(borrower); + + BrokerParameters const brokerParams{ + .vaultDeposit = Number{2, 9}, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; + + auto const vaultBefore = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultBefore)) + return; + Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable); + + // Loan: $1B principal, 3 payments, 600s interval, rate=1 TenthBips32. + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 9}; + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + createJson["InterestRate"] = 1; + createJson["PaymentTotal"] = 3; + createJson["PaymentInterval"] = 600; + + auto const loanKeylet = nextLoanKeylet(env, broker); + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return; + Number const expectedTotalInterest = + loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding); + + env(pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)), Ter(tesSUCCESS)); + env.close(); + + auto const vaultAfter = env.le(broker.vaultKeylet()); + if (!BEAST_EXPECT(vaultAfter)) + return; + Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable); + Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore; + + STAmount const borrowerEndBal = usdBalance(borrower); + STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal; + + // Self-consistency: vault gained exactly the expected interest + // computed at LoanSet, and the borrower's outflow matches. + BEAST_EXPECT(vaultGain == expectedTotalInterest); + BEAST_EXPECT(Number(borrowerNetOut) == expectedTotalInterest); + + // Mathematical correctness: the total interest for this loan + // configuration is 0.38051750382930729983, calculated + // independently using 50-digit Decimal arithmetic (no + // cancellation possible at that precision). At Number's 19-digit + // mantissa this rounds to 0.38051750382930729 — the literal + // below. The vault's actual gain must agree to within + // sub-microcent precision. + Number const decimalReference{38051750382930729LL, -17}; + Number const tolerance{1, -6}; // 1e-6 USD = sub-microcent + Number const error = abs(vaultGain - decimalReference); + BEAST_EXPECTS( + error < tolerance, + "vault gain " + to_string(vaultGain) + " differs from Decimal reference " + + to_string(decimalReference) + " by " + to_string(error) + " — exceeds tolerance " + + to_string(tolerance)); + } + + void + runAmendmentIndependent() + { + testIssuerLoan(); + testBorrowerIsBroker(); + testFullLifecycleVaultPnLNearZeroRate(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testLifecycle(features); + testSelfLoan(features); + testIssuerIsBorrower(features); + testBatchBypassCounterparty(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanLifecycle, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanMisc_test.cpp b/src/test/app/lending/LoanMisc_test.cpp new file mode 100644 index 0000000000..798cddda17 --- /dev/null +++ b/src/test/app/lending/LoanMisc_test.cpp @@ -0,0 +1,561 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanMisc_test : public LoanTestBase +{ +private: + void + testRPC(FeatureBitset features) + { + // This will expand as more test cases are added. Some functionality + // is tested in other test functions. + testcase("RPC"); + + using namespace jtx; + + Env env(*this, features); + + auto lowerFee = [&]() { + // Run the local fee back down. + while (env.app().getFeeTrack().lowerLocalFee()) + ; + }; + + auto const baseFee = env.current()->fees().base; + + Account const alice{"alice"}; + std::string const borrowerPass = "borrower"; + Account const borrower{borrowerPass, KeyType::Ed25519}; + auto const lenderPass = "lender"; + Account const lender{lenderPass, KeyType::Ed25519}; + + env.fund(XRP(1'000'000), alice, lender, borrower); + env.close(); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env(noop(lender)); + env.close(); + + { + testcase("RPC AccountSet"); + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "AccountSet"; + txJson[sfAccount] = borrower.human(); + + auto const signParams = [&]() { + json::Value signParams{json::ValueType::Object}; + signParams[jss::passphrase] = borrowerPass; + signParams[jss::key_type] = "ed25519"; + signParams[jss::tx_json] = txJson; + return signParams; + }(); + auto const jSign = env.rpc("json", "sign", to_string(signParams)); + BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json)); + auto txSignResult = jSign[jss::result][jss::tx_json]; + auto txSignBlob = jSign[jss::result][jss::tx_blob].asString(); + txSignResult.removeMember(jss::hash); + + auto const jtx = env.jt(txJson, Sig(borrower)); + BEAST_EXPECT(txSignResult == jtx.jv); + + lowerFee(); + auto const jSubmit = env.rpc("submit", txSignBlob); + BEAST_EXPECT( + jSubmit.isMember(jss::result) && + jSubmit[jss::result].isMember(jss::engine_result) && + jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS"); + + lowerFee(); + env(jtx.jv, Sig(kNone), Seq(kNone), Fee(kNone), Ter(tefPAST_SEQ)); + } + + { + testcase("RPC LoanSet - illegal signature_target"); + + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "AccountSet"; + txJson[sfAccount] = borrower.human(); + + auto const borrowerSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "Destination"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::error) && + jSignBorrower[jss::result][jss::error] == "invalidParams" && + jSignBorrower[jss::result].isMember(jss::error_message) && + jSignBorrower[jss::result][jss::error_message] == "Destination"); + } + { + testcase("RPC LoanSet - sign and submit borrower initiated"); + // 1. Borrower creates the transaction + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "LoanSet"; + txJson[sfAccount] = borrower.human(); + txJson[sfCounterparty] = lender.human(); + txJson[sfLoanBrokerID] = + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" + "F83F" + "5C"; + txJson[sfPrincipalRequested] = "100000000"; + txJson[sfPaymentTotal] = 10000; + txJson[sfPaymentInterval] = 3600; + txJson[sfGracePeriod] = 300; + txJson[sfFlags] = 65536; // tfLoanOverpayment + txJson[sfFee] = to_string(24 * baseFee / 10); + + // 2. Borrower signs the transaction + auto const borrowerSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECTS( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::tx_json), + to_string(jSignBorrower)); + auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; + auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); + + // 2a. Borrower attempts to submit the transaction. It doesn't + // work + { + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + // Transaction fails because the CounterpartySignature is + // missing + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); + } + + // 3. Borrower sends the signed transaction to the lender + // 4. Lender signs the transaction + auto const lenderSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = lenderPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "CounterpartySignature"; + params[jss::tx_json] = txBorrowerSignResult; + return params; + }(); + auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); + BEAST_EXPECT( + jSignLender.isMember(jss::result) && + jSignLender[jss::result].isMember(jss::tx_json)); + auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; + auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); + + // 5. Lender submits the signed transaction blob + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", + to_string(jSubmitBlobResult)); + + BEAST_EXPECT( + !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); + + // 4-alt. Lender submits the transaction json originally + // received from the Borrower. It gets signed, but is now a + // duplicate, so fails. Borrower could done this instead of + // steps 4 and 5. + lowerFee(); + auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams)); + BEAST_EXPECT(jSubmitJson.isMember(jss::result)); + auto const jSubmitJsonResult = jSubmitJson[jss::result]; + BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); + auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( + jSubmitJsonResult.isMember(jss::engine_result) && + jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", + to_string(jSubmitJsonResult)); + + BEAST_EXPECT( + !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); + + BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); + } + + { + testcase("RPC LoanSet - sign and submit lender initiated"); + // 1. Lender creates the transaction + json::Value txJson{json::ValueType::Object}; + txJson[sfTransactionType] = "LoanSet"; + txJson[sfAccount] = lender.human(); + txJson[sfCounterparty] = borrower.human(); + txJson[sfLoanBrokerID] = + "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC" + "F83F" + "5C"; + txJson[sfPrincipalRequested] = "100000000"; + txJson[sfPaymentTotal] = 10000; + txJson[sfPaymentInterval] = 3600; + txJson[sfGracePeriod] = 300; + txJson[sfFlags] = 65536; // tfLoanOverpayment + txJson[sfFee] = to_string(24 * baseFee / 10); + + // 2. Lender signs the transaction + auto const lenderSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = lenderPass; + params[jss::key_type] = "ed25519"; + params[jss::tx_json] = txJson; + return params; + }(); + auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams)); + BEAST_EXPECT( + jSignLender.isMember(jss::result) && + jSignLender[jss::result].isMember(jss::tx_json)); + auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json]; + auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); + + // 2a. Lender attempts to submit the transaction. It doesn't + // work + { + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + // Transaction fails because the CounterpartySignature is + // missing + BEAST_EXPECT( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER"); + } + + // 3. Lender sends the signed transaction to the Borrower + // 4. Borrower signs the transaction + auto const borrowerSignParams = [&]() { + json::Value params{json::ValueType::Object}; + params[jss::passphrase] = borrowerPass; + params[jss::key_type] = "ed25519"; + params[jss::signature_target] = "CounterpartySignature"; + params[jss::tx_json] = txLenderSignResult; + return params; + }(); + auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); + BEAST_EXPECT( + jSignBorrower.isMember(jss::result) && + jSignBorrower[jss::result].isMember(jss::tx_json)); + auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; + auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); + + // 5. Borrower submits the signed transaction blob + lowerFee(); + auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); + BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); + auto const jSubmitBlobResult = jSubmitBlob[jss::result]; + BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); + auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( + jSubmitBlobResult.isMember(jss::engine_result) && + jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY", + to_string(jSubmitBlobResult)); + + BEAST_EXPECT( + !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); + + // 4-alt. Borrower submits the transaction json originally + // received from the Lender. It gets signed, but is now a + // duplicate, so fails. Lender could done this instead of steps + // 4 and 5. + lowerFee(); + auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams)); + BEAST_EXPECT(jSubmitJson.isMember(jss::result)); + auto const jSubmitJsonResult = jSubmitJson[jss::result]; + BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); + auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( + jSubmitJsonResult.isMember(jss::engine_result) && + jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ", + to_string(jSubmitJsonResult)); + + BEAST_EXPECT( + !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error)); + + BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx); + } + } + + void + testLendingCanTradeDisabledNoImpact() + { + testcase("Lending: CanTrade disabled has no impact"); + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + MPTTester mpt( + {.env = env, + .issuer = issuer, + .holders = {lender, borrower}, + .flags = tfMPTCanTransfer | tfMPTCanLock, + .mutableFlags = tmfMPTCanEnableCanTrade}); + PrettyAsset const asset = mpt.issuanceID(); + env(pay(issuer, lender, asset(10'000'000))); + env(pay(issuer, borrower, asset(100'000))); + env.close(); + + auto const broker = createVaultAndBroker(env, asset, lender); + + // CanTrade is not set + env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION}); + env.close(); + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + // New cover deposits still work. + env(coverDeposit(lender, broker.brokerID, asset(100))); + env.close(); + + // New loan issuance still works. + env(loan::set(borrower, broker.brokerID, 1'000), + Sig(sfCounterpartySignature, lender), + loanSetFee); + env.close(); + auto const loanKeylet = keylet::loan(broker.brokerID, 1); + BEAST_EXPECT(env.le(loanKeylet)); + + // Repayment still works. + env(pay(borrower, loanKeylet.key, asset(1'000))); + env.close(); + + // Cover withdrawal still works. + env(coverWithdraw(lender, broker.brokerID, asset(100))); + env.close(); + + // Enable CanTrade and verify the DEX path is restored. + mpt.set({.mutableFlags = tmfMPTSetCanTrade}); + env.close(); + + env(offer(lender, XRP(1), asset(10))); + env.close(); + } + + void + runAmendmentIndependent() + { + testLendingCanTradeDisabledNoImpact(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testRPC(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +class LoanBatch_test : public LoanTestBase +{ +protected: + beast::xor_shift_engine engine_; + + std::uniform_int_distribution<> assetDist_{0, 2}; + std::uniform_int_distribution principalDist_{100'000, 1'000'000'000}; + std::uniform_int_distribution interestRateDist_{0, 10000}; + std::uniform_int_distribution<> paymentTotalDist_{12, 10000}; + std::uniform_int_distribution<> paymentIntervalDist_{60, 3600 * 24 * 30}; + std::uniform_int_distribution managementFeeRateDist_{0, 10'000}; + std::uniform_int_distribution<> serviceFeeDist_{0, 20}; + /* + # Generate parameters that are more likely to be valid + principal = Decimal(str(rand.randint(100000, + 100'000'000))).quantize(ROUND_TARGET) + + interest_rate = Decimal(rand.randint(1, 10000)) / + Decimal(100000) + + payment_total = rand.randint(12, 10000) + + payment_interval = Decimal(str(rand.randint(60, 2629746))) + + interest_fee = Decimal(rand.randint(0, 100000)) / + Decimal(100000) +*/ + + void + testRandomLoan() + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + // Determine all the random parameters at once + auto const assetType = static_cast(assetDist_(engine_)); + auto const principalRequest = principalDist_(engine_); + TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)}; + auto const serviceFee = serviceFeeDist_(engine_); + TenthBips32 interest{interestRateDist_(engine_)}; + auto const payTotal = paymentTotalDist_(engine_); + auto const payInterval = paymentIntervalDist_(engine_); + + BrokerParameters const brokerParams{ + .vaultDeposit = principalRequest * 10, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = managementFeeRate}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = principalRequest, + .serviceFee = serviceFee, + .interest = interest, + .payTotal = payTotal, + .payInterval = payInterval, + }; + + runLoan(assetType, brokerParams, loanParams, all_); + } + +public: + void + run() override + { + auto const numIterations = [s = arg()]() -> int { + int const defaultNum = 5; + if (s.empty()) + return defaultNum; + try + { + std::size_t pos = 0; + auto const r = stoi(s, &pos); + if (pos != s.size()) + return defaultNum; + return r; + } + catch (...) + { + return defaultNum; + } + }(); + + using namespace jtx; + + auto const updateInterval = std::max(std::min(numIterations / 5, 100), 1); + + for (int i = 0; i < numIterations; ++i) + { + if (i % updateInterval == 0) + testcase << "Random Loan Test iteration " << (i + 1) << "/" << numIterations; + testRandomLoan(); + } + } +}; + +class LoanArbitrary_test : public LoanBatch_test +{ + void + run() override + { + using namespace jtx; + + BrokerParameters const brokerParams{ + .vaultDeposit = 10000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = Account("lender"), + .counter = Account("borrower"), + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 2, + .payInterval = 200}; + + runLoan(AssetType::XRP, brokerParams, loanParams, all_); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanMisc, tx, xrpl); +BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, xrpl); +BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanPay_test.cpp b/src/test/app/lending/LoanPay_test.cpp new file mode 100644 index 0000000000..e06d728c87 --- /dev/null +++ b/src/test/app/lending/LoanPay_test.cpp @@ -0,0 +1,760 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanPay_test : public LoanTestBase +{ +private: +#if LOAN_TODO + void + testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features) + { + testcase("LoanPay full payment skips late penalties"); + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(100'000'000))); + env(trust(borrower, asset(100'000'000))); + env(pay(issuer, lender, asset(50'000'000))); + env(pay(issuer, borrower, asset(5'000'000))); + env.close(); + + BrokerInfo broker{createVaultAndBroker(env, asset, lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID)); + if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value()) + return; + + auto const loanSequence = brokerPreLoan->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + Number const principal = asset(1'000).value(); + Number const serviceFee = asset(2).value(); + Number const lateFee = asset(5).value(); + Number const closeFee = asset(4).value(); + + env(set(borrower, broker.brokerID, principal), + Sig(sfCounterpartySignature, lender), + kLoanServiceFee(serviceFee), + kLatePaymentFee(lateFee), + kClosePaymentFee(closeFee), + kInterestRate(percentageToTenthBips(12)), + kLateInterestRate(percentageToTenthBips(24) / 10), + kCloseInterestRate(percentageToTenthBips(5)), + kPaymentTotal(12), + kPaymentInterval(600), + kGracePeriod(0), + Fee(loanSetFee)); + env.close(); + + auto state1 = getCurrentState(env, broker, loanKeylet); + if (!BEAST_EXPECT(state1.paymentRemaining > 1)) + return; + + using d = NetClock::duration; + using tp = NetClock::time_point; + auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}}; + env.close(overdueClose); + + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(brokerSle && loanSle)) + return; + + auto state = getCurrentState(env, broker, loanKeylet); + + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)}; + TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)}; + TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)}; + + Number const closePaymentFeeRounded = + roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale); + Number const latePaymentFeeRounded = + roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale); + + auto const roundedLoanState = constructLoanState( + state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); + Number const totalInterestOutstanding = roundedLoanState.interestDue; + + auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval); + auto const rawLoanState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + managementFeeRate); + + auto const parentCloseTime = env.current()->parentCloseTime(); + auto const startDateSeconds = + static_cast(state.startDate.time_since_epoch().count()); + + Number const fullPaymentInterest = computeFullPaymentInterest( + rawLoanState.principalOutstanding, + periodicRate, + parentCloseTime, + state.paymentInterval, + state.previousPaymentDate, + startDateSeconds, + closeInterestRateValue); + + Number const roundedFullInterestAmount = + roundToAsset(broker.asset, fullPaymentInterest, state.loanScale); + Number const roundedFullManagementFee = computeManagementFee( + broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale); + Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee; + + Number const trackedValueDelta = + state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding; + Number const untrackedManagementFee = + closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding; + Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding; + + Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee; + BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale)); + + auto const overdueSeconds = + parentCloseTime.time_since_epoch().count() - state.nextPaymentDate; + if (!BEAST_EXPECT(overdueSeconds > 0)) + return; + + Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds); + Number const lateInterestRaw = state.principalOutstanding * overdueRate; + Number const lateInterestRounded = + roundToAsset(broker.asset, lateInterestRaw, state.loanScale); + Number const lateManagementFeeRounded = computeManagementFee( + broker.asset, lateInterestRounded, managementFeeRate, state.loanScale); + Number const penaltyDue = + lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded; + BEAST_EXPECT(penaltyDue > Number{}); + + auto const balanceBefore = env.balance(borrower, broker.asset).number(); + + STAmount const paymentAmount{broker.asset.raw(), baseFullDue}; + env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment)); + env.close(); + + if (auto const meta = env.meta(); BEAST_EXPECT(meta)) + BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); + + auto const balanceAfter = env.balance(borrower, broker.asset).number(); + Number const actualPaid = balanceBefore - balanceAfter; + BEAST_EXPECT(actualPaid == baseFullDue); + + Number const expectedWithPenalty = baseFullDue + penaltyDue; + BEAST_EXPECT(expectedWithPenalty > actualPaid); + BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue); + } +#endif + + void + testOverpaymentManagementFee(FeatureBitset features) + { + testcase("testOverpaymentManagementFee"); + + using namespace jtx; + using namespace loan; + + Env env{*this, features}; + + Account const lender{"lender"}, borrower{"borrower"}; + + env.fund(XRP(10'000'000), lender, borrower); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1000}; + + auto const result = createVaultAndBroker( + env, + asset, + lender, + { + .vaultDeposit = asset(100'000).value(), + .managementFeeRate = TenthBips16(10'000), + }); + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + auto const brokerSle = env.le(result.brokerKeylet()); + if (!BEAST_EXPECT(brokerSle)) + return; + auto const loanKeylet = + keylet::loan(result.brokerKeylet().key, brokerSle->at(sfLoanSequence)); + env(loan::set( + borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment), + Sig(sfCounterpartySignature, lender), + loan::kPaymentInterval(86400 * 30), + loan::kPaymentTotal(3), + loan::kOverpaymentInterestRate(TenthBips32(percentageToTenthBips(20))), + loanSetFee); + + // From calculator + auto const expectedOverpaymentManagementFee = Number{33333, 0}; + auto const loanBrokerBalanceBefore = env.balance(lender); + + auto const loanPayFee = Fee(env.current()->fees().base * 2); + env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee); + env.close(); + + BEAST_EXPECTS( + env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee, + "overpayment management fee mismatch; expected:" + + to_string(expectedOverpaymentManagementFee) + + " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore)); + } + + void + testDosLoanPay(FeatureBitset features) + { + bool const feeCapped = features[fixCleanup3_1_3]; + + // From FIND-005 + testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped"); + + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3)); + + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(100'000'000))); + env(trust(borrower, iouAsset(100'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); + env(pay(issuer, borrower, iouAsset(1'000))); + env.close(); + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{3959'37, -2}; + auto const baseFee = env.current()->fees().base; + + auto const createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object), + kClosePaymentFee(0), + kGracePeriod(60), + kInterestRate(TenthBips32(20930)), + kLateInterestRate(TenthBips32(77049)), + kLatePaymentFee(0), + kLoanServiceFee(0), + kOverpaymentFee(TenthBips32(7)), + kOverpaymentInterestRate(TenthBips32(66653)), + kPaymentInterval(60), + kPaymentTotal(3239184)); + + // There are enough payments due on this loan that it only needs to be + // created once, and can be paid on multiple times. Just don't create a + // gazillion test cases. + auto const keylet = nextLoanKeylet(env, broker); + + env(createJson, Sig(sfCounterpartySignature, lender)); + env.close(); + + auto const roundedPayment = [&]() { + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining == 3239184); + BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); + + return roundToAsset( + iouAsset, + stateBefore.periodicPayment, + stateBefore.loanScale, + Number::RoundingMode::Upward); + }(); + + auto test = [&](int const payFactor, + int const feeFactor, + TER const expectedTer = tesSUCCESS) { + auto const stateBefore = getCurrentState(env, broker, keylet); + BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184); + BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction); + + Number const amount = roundedPayment * payFactor; + auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount})); + XRPAmount const payFee{baseFee * feeFactor}; + env(loanPayTx, Ter(expectedTer), Fee(payFee)); + env.close(); + auto const expectedChange = isTesSuccess(expectedTer) + ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor) + : 0; + + auto const stateAfter = getCurrentState(env, broker, keylet); + BEAST_EXPECT( + stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange); + }; + + static constexpr std::int64_t kMaxFeeIncrements = + kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement; + + TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P; + + // * Amount well above threshold -> capped fee + // The original test case - way over the limit - more fee is always ok + test(1819878, 363976); + // The capped fee is only sufficient if the amendment is enabled. + test(1819878, kMaxFeeIncrements, failWithoutFix); + + // * Amount exactly at threshold -> capped fee + test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements); + // More fee is always ok + test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10); + + // * Amount below threshold -> normal calculation + test(1, 1); + test(kLoanPaymentsPerFeeIncrement * 2, 2); + test(0, 0, temBAD_AMOUNT); + test(0, 1, temBAD_AMOUNT); + // Fee difference rounds evenly + test( + kLoanMaximumPaymentsPerTransaction - 10, + ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1, + telINSUF_FEE_P); + test( + kLoanMaximumPaymentsPerTransaction - 10, + ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement)); + // More fee is always ok + test( + kLoanMaximumPaymentsPerTransaction - 10, + ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3); + // Fee rounds up + for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under) + { + test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P); + test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements); + } + // Only when you get one less fee increment can you pay less + test( + kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, + kMaxFeeIncrements - 1); + // And again, more fee is always ok. + test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements); + } + + // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero + // rate). At such a near-zero rate the closed-form payment factor + // (1 + r)^n - 1 cancels catastrophically. + // + // Without fixCleanup3_2_0 the resulting amortization is degenerate and the + // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the + // amendment, computePowerMinusOneHybrid uses a numerically-stable series + // expansion, so the loan is created and the scheduled payments + // (2 * periodicPayment) cover the principal — no economic underpayment + // (yield theft). + // + // The test runs the same LoanSet under both amendment settings and pins the + // exact outcome for each. + void + testLoanSetNearZeroInterestRateSucceeds() + { + testcase("LoanSet near-zero interest rate covers principal"); + + using namespace jtx; + using namespace loan; + + Number const principalRequested{1000}; + + struct Result + { + TER ter = tesSUCCESS; + bool created = false; + std::int32_t loanScale = 0; + Number principal; + Number totalValue; + Number managementFee; + Number periodicPayment; + }; + + auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result { + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"vaultOwner"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower); + + auto const broker = createVaultAndBroker( + env, + iouAsset, + lender, + {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}}); + + 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); + + env(set(borrower, broker.brokerID, principalRequested), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32{1}), + kPaymentTotal(2), + kPaymentInterval(400), + Fee(env.current()->fees().base * 2), + Ter(expectedTer)); + env.close(); + + Result r; + r.ter = env.ter(); + if (auto const loanSle = env.le(loanKeylet)) + { + r.created = true; + r.loanScale = loanSle->at(sfLoanScale); + r.principal = loanSle->at(sfPrincipalOutstanding); + r.totalValue = loanSle->at(sfTotalValueOutstanding); + r.managementFee = loanSle->at(sfManagementFeeOutstanding); + r.periodicPayment = loanSle->at(sfPeriodicPayment); + } + return r; + }; + + Result const fixed = runScenario(all_, tesSUCCESS); + Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS); + + // Without the amendment, the catastrophically-cancelling closed-form + // payment factor produces a degenerate amortization that fails + // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no + // loan is created. + BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS); + BEAST_EXPECT(!legacy.created); + + // With the amendment the stable series expansion produces a valid loan + // at loanScale -10. + BEAST_EXPECT(fixed.ter == tesSUCCESS); + BEAST_EXPECT(fixed.created); + BEAST_EXPECT(fixed.loanScale == -10); + BEAST_EXPECT(fixed.principal == principalRequested); + BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10})); + BEAST_EXPECT(fixed.managementFee == beast::kZero); + + // Periodic payment from the numerically-stable series expansion, and the + // scheduled total (2 * periodicPayment) which exceeds the 1000 principal + // — no economic underpayment / yield theft. + BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16})); + BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15})); + BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested); + } + + void + testLoanNextPaymentDueDateOverflow(FeatureBitset features) + { + // For FIND-013 + testcase << "Prevent nextPaymentDueDate overflow"; + + using namespace jtx; + using namespace std::chrono_literals; + using namespace lending; + Env env{*this, features}; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = + createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000); + + BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}}; + BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + using timeType = decltype(sfNextPaymentDueDate)::type::value_type; + static_assert(std::is_same_v); + constexpr timeType kMaxTime = std::numeric_limits::max(); + static_assert(kMaxTime == 4'294'967'295); + + auto const baseJson = [&]() { + auto createJson = env.json( + set(borrower, broker.brokerID, Number{55524'81, -2}), + Fee(loanSetFee), + kClosePaymentFee(0), + kGracePeriod(LoanSet::kDefaultGracePeriod), + kInterestRate(TenthBips32(12833)), + kLateInterestRate(TenthBips32(77048)), + kLatePaymentFee(0), + kLoanOriginationFee(218), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson.removeMember(sfSequence.getJsonName()); + + return createJson; + }(); + + auto const baseFee = env.current()->fees().base; + + auto parentCloseTime = [&]() { + return env.current()->parentCloseTime().time_since_epoch().count(); + }; + auto maxLoanTime = [&]() { + auto const startDate = parentCloseTime(); + + BEAST_EXPECT(startDate >= 50); + + return kMaxTime - startDate; + }; + + { + // straight-up overflow: interval + auto const interval = maxLoanTime() + 1; + auto const total = 1; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // straight-up overflow: total + // min interval is 60 + auto const interval = 60; + auto const total = maxLoanTime() + 1; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // straight-up overflow: grace period + // min interval is 60 + auto const interval = maxLoanTime() + 1; + auto const total = 1; + auto const grace = interval; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + // The grace period can't be larger than the interval. + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Overflow with multiplication of a few large intervals + auto const interval = 1'000'000'000; + auto const total = 10; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Overflow with multiplication of many small payments + // min interval is 60 + auto const interval = 60; + auto const total = 1'000'000'000; + auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Overflow with an absurdly large grace period + // min interval is 60 + auto const total = 60; + auto const interval = (maxLoanTime() - total) / total; + auto const grace = interval; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED)); + env.close(); + } + { + // Start date when the ledger is closed will be larger + auto const keylet = nextLoanKeylet(env, broker); + + auto const grace = 100; + auto const interval = maxLoanTime() - grace; + auto const total = 1; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); + env.close(); + + // The transaction is killed in the closed ledger + auto const meta = env.meta(); + if (BEAST_EXPECT(meta)) + { + BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED); + } + + // If the transaction had succeeded, the loan would exist + auto const loanSle = env.le(keylet); + // but it doesn't + BEAST_EXPECT(!loanSle); + } + { + // Start date when the ledger is closed will be larger + auto const keylet = nextLoanKeylet(env, broker); + + auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; + auto const grace = 5'000; + auto const interval = kMaxTime - closeStartDate - grace; + auto const total = 1; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); + env.close(); + + // The transaction succeeds in the closed ledger + auto const meta = env.meta(); + if (BEAST_EXPECT(meta)) + { + BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS); + } + + // This loan exists + auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); + BEAST_EXPECT(afterState.previousPaymentDate == 0); + BEAST_EXPECT(afterState.paymentRemaining == 1); + } + + { + // Ensure the borrower has funds to pay back the loan + env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2}))); + + // Start date when the ledger is closed will be larger + auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10; + auto const grace = 5'000; + auto const maxLoanTime = kMaxTime - closeStartDate - grace; + auto const total = [&]() { + if (maxLoanTime % 5 == 0) + return 5; + if (maxLoanTime % 3 == 0) + return 3; + if (maxLoanTime % 2 == 0) + return 2; + return 0; + }(); + if (!BEAST_EXPECT(total != 0)) + return; + + auto const brokerState = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerState)) + return; + // Intentionally shadow the outer values + auto const loanSequence = brokerState->at(sfLoanSequence); + auto const keylet = keylet::loan(broker.brokerID, loanSequence); + + auto const interval = maxLoanTime / total; + auto createJson = env.json( + baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace)); + + env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS)); + env.close(); + + // This loan exists + auto const beforeState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval); + BEAST_EXPECT(beforeState.previousPaymentDate == 0); + BEAST_EXPECT(beforeState.paymentRemaining == total); + BEAST_EXPECT(beforeState.periodicPayment > 0); + + // pay all but the last payment + { + NumberRoundModeGuard const mg{Number::RoundingMode::Upward}; + Number const payment = beforeState.periodicPayment * (total - 1); + XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)}; + STAmount const paymentAmount = + roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale); + auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee)); + env(loanPayTx, Ter(tesSUCCESS)); + env.close(); + } + + // The loan is on the last payment + auto const afterState = getCurrentState(env, broker, keylet); + BEAST_EXPECT(afterState.paymentRemaining == 1); + BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace); + BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval); + } + } + + void + runAmendmentIndependent() + { + testLoanSetNearZeroInterestRateSucceeds(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { +#if LOAN_TODO + testLoanPayLateFullPaymentBypassesPenalties(features); +#endif + testOverpaymentManagementFee(features); + testDosLoanPay(features); + testLoanNextPaymentDueDateOverflow(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanPay, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanRounding_test.cpp b/src/test/app/lending/LoanRounding_test.cpp new file mode 100644 index 0000000000..b914961e78 --- /dev/null +++ b/src/test/app/lending/LoanRounding_test.cpp @@ -0,0 +1,993 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanRounding_test : public LoanTestBase +{ +private: + void + testDustManipulation(FeatureBitset features) + { + testcase("Dust manipulation"); + + using namespace jtx; + using namespace std::chrono_literals; + Env env{*this, features}; + + // Setup: Create accounts + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + Account const victim{"victim"}; + + env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim); + env.close(); + + // Step 1: Create vault with IOU asset + auto asset = issuer["USD"]; + env(trust(lender, asset(100000))); + env(trust(borrower, asset(100000))); + env(trust(victim, asset(100000))); + env(pay(issuer, lender, asset(50000))); + env(pay(issuer, borrower, asset(50000))); + env(pay(issuer, victim, asset(50000))); + env.close(); + + BrokerParameters const brokerParams{ + .vaultDeposit = 10000, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{1000}, + .coverRateLiquidation = TenthBips32{2500}}; + + auto broker = createVaultAndBroker(env, asset, lender, brokerParams); + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // 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); + }(); + if (!loanKeyletOpt) + return; + + auto const& vaultKeylet = broker.vaultKeylet(); + + { + auto const vaultSle = env.le(vaultKeylet); + Number const assetsTotal = vaultSle->at(sfAssetsTotal); + Number const assetsAvail = vaultSle->at(sfAssetsAvailable); + + log << "Before loan creation:" << std::endl; + log << " AssetsTotal: " << assetsTotal << std::endl; + log << " AssetsAvailable: " << assetsAvail << std::endl; + log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; + + // before the loan the assets total and available should be equal + BEAST_EXPECT(assetsAvail == assetsTotal); + BEAST_EXPECT(assetsAvail == broker.asset(brokerParams.vaultDeposit).number()); + } + + Keylet const& loanKeylet = *loanKeyletOpt; + + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{100}, + .interest = TenthBips32{1922}, + .payTotal = 5816, + .payInterval = 86400 * 6, + .gracePd = 86400 * 5, + }; + + env(loanParams(env, broker)); + env.close(); + + // Wait for loan to be late enough to default + env.close(std::chrono::seconds(86400 * 40)); // 40 days + + { + auto const vaultSle = env.le(vaultKeylet); + Number const assetsTotal = vaultSle->at(sfAssetsTotal); + Number const assetsAvail = vaultSle->at(sfAssetsAvailable); + + log << "After loan creation:" << std::endl; + log << " AssetsTotal: " << assetsTotal << std::endl; + log << " AssetsAvailable: " << assetsAvail << std::endl; + log << " Difference: " << (assetsTotal - assetsAvail) << std::endl; + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return; + auto const state = constructLoanState(loanSle); + + log << "Loan state:" << std::endl; + log << " ValueOutstanding: " << state.valueOutstanding << std::endl; + log << " PrincipalOutstanding: " << state.principalOutstanding << std::endl; + log << " InterestOutstanding: " << state.interestOutstanding() << std::endl; + log << " InterestDue: " << state.interestDue << std::endl; + log << " FeeDue: " << state.managementFeeDue << std::endl; + + // after loan creation the assets total and available should + // reflect the value of the loan + BEAST_EXPECT(assetsAvail < assetsTotal); + BEAST_EXPECT( + assetsAvail == + broker.asset(brokerParams.vaultDeposit - loanParams.principalRequest).number()); + BEAST_EXPECT( + assetsTotal == + broker.asset(brokerParams.vaultDeposit + state.interestDue).number()); + } + + // Step 7: Trigger default (dust adjustment will occur) + env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // Step 8: Verify phantom assets created + { + auto const vaultSle2 = env.le(vaultKeylet); + Number const assetsTotal2 = vaultSle2->at(sfAssetsTotal); + Number const assetsAvail2 = vaultSle2->at(sfAssetsAvailable); + + log << "After default:" << std::endl; + log << " AssetsTotal: " << assetsTotal2 << std::endl; + log << " AssetsAvailable: " << assetsAvail2 << std::endl; + log << " Difference: " << (assetsTotal2 - assetsAvail2) << std::endl; + + // after a default the assets total and available should be equal + BEAST_EXPECT(assetsAvail2 == assetsTotal2); + } + } + + void + testRoundingAllowsUndercoverage(FeatureBitset features) + { + testcase("Minimum cover rounding allows undercoverage (XRP)"); + + using namespace jtx; + using namespace loan_broker; + + Env env{*this, features}; + + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(200'000), lender, borrower); + env.close(); + + // Vault with XRP asset + Vault const vault{env}; + auto [vaultCreate, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()}); + env(vaultCreate); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + // Seed the vault with XRP so it can fund the loan principal + PrettyAsset const xrpAsset{xrpIssue(), 1}; + + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000, + .debtMax = Number{0}, + .coverRateMin = TenthBips32{10'000}, + .coverDeposit = 82, + }; + + auto const brokerInfo = createVaultAndBroker(env, xrpAsset, lender, brokerParams); + // Create a loan with principal 804 XRP and 0% interest (so + // DebtTotal increases by exactly 804) + env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()), + loan::kInterestRate(TenthBips32(0)), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 2)); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + // Verify DebtTotal is exactly 804 + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + log << *brokerSle << std::endl; + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); + } + + // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable. + // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails. + env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()), + Ter(tecINSUFFICIENT_FUNDS)); + BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS); + env.close(); + + // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable. + // because that leaves sufficient cover, this succeeds + env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value())); + BEAST_EXPECT(env.ter() == tesSUCCESS); + env.close(); + + // Validate CoverAvailable == 81 XRP and DebtTotal remains 804 + if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + log << *brokerSle << std::endl; + BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == xrpAsset(81).value()); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804)); + + // Also demonstrate that the true minimum (804 * 10%) exceeds 80 + auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000)); + log << "Theoretical min cover: " << theoreticalMin << std::endl; + BEAST_EXPECT(Number(804, -1) == theoreticalMin); + } + } + + void + testYieldTheftRounding(std::uint32_t flags) + { + testcase("Rounding manipulation does not permit yield theft"); + using namespace jtx; + using namespace loan; + + // 1. Setup Environment + Env env(*this, all_); + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1000), issuer, lender, borrower); + env.close(); + + // 2. Asset Selection + PrettyAsset const iou = issuer["USD"]; + env(trust(lender, iou(100'000'000))); + env(trust(borrower, iou(100'000'000))); + env(pay(issuer, lender, iou(100'000'000))); + env(pay(issuer, borrower, iou(100'000'000))); + env.close(); + + // 3. Create Vault and Broker with High Debt Limit (100M) + auto const brokerInfo = createVaultAndBroker( + env, + iou, + lender, + { + .vaultDeposit = 5'000'000, + .debtMax = Number{100'000'000}, + .coverDeposit = 500'000, + }); + auto const [currentSeq, vaultKeylet] = [&]() { + auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::make_tuple(0u, keylet::unchecked(beast::kZero)); + auto const currentSeq = brokerSle->at(sfLoanSequence); + auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID)); + return std::make_tuple(currentSeq, vaultKeylet); + }(); + + // 4. Loan Parameters (Attack Vector) + Number const principal = 1'000'000; + TenthBips32 const interestRate = TenthBips32{1}; // 0.001% + std::uint32_t const paymentInterval = 86400; + std::uint32_t const paymentTotal = 3650; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags), + Sig(sfCounterpartySignature, lender), + loan::kInterestRate(interestRate), + loan::kPaymentInterval(paymentInterval), + loan::kPaymentTotal(paymentTotal), + Fee(loanSetFee)); + env.close(); + + // --- RETRIEVE OBJECTS & SETUP ATTACK --- + + auto borrowerBalance = [&]() { return env.balance(borrower, iou); }; + auto const borrowerScale = static_cast(borrowerBalance()).exponent(); + + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq); + auto const maybePeriodicPayment = [&]() -> std::optional { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + return std::nullopt; + // Construct Payment + return STAmount{iou, loanSle->at(sfPeriodicPayment)}; + }(); + if (!maybePeriodicPayment) + return; + auto const periodicPayment = *maybePeriodicPayment; + auto const roundedPayment = + roundToScale(periodicPayment, borrowerScale, Number::RoundingMode::Upward); + + // ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution + STAmount const paymentBuffer{iou, Number(1, -9)}; + STAmount const attackPayment = periodicPayment + paymentBuffer; + + auto const maybeInitialVaultAssets = [&]() -> std::optional { + auto const vault = env.le(vaultKeylet); + if (!BEAST_EXPECT(vault)) + return std::nullopt; + return vault->at(sfAssetsTotal); + }(); + if (!maybeInitialVaultAssets) + return; + auto const initialVaultAssets = *maybeInitialVaultAssets; + + // 5. Execution Loop + int yieldTheftCount = 0; + auto previousAssetsTotal = initialVaultAssets; + + for (int i = 0; i < 100; ++i) + { + auto const balanceBefore = borrowerBalance(); + env(pay(borrower, loanKeylet.key, attackPayment, flags)); + env.close(); + auto const borrowerDelta = balanceBefore - borrowerBalance(); + BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum()); + + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + break; + auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)}; + BEAST_EXPECT( + (roundToScale(updatedPayment, borrowerScale, Number::RoundingMode::Upward) == + roundedPayment)); + BEAST_EXPECT( + (updatedPayment == periodicPayment) || + (flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment)); + + auto const currentVaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(currentVaultSle)) + break; + + auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal); + auto const delta = currentAssetsTotal - previousAssetsTotal; + + BEAST_EXPECT( + (delta == beast::kZero && borrowerDelta <= roundedPayment) || + (delta > beast::kZero && borrowerDelta > roundedPayment)); + + // If tx succeeded but Assets Total didn't change, interest was + // stolen. + if (delta == beast::kZero && borrowerDelta > roundedPayment) + { + yieldTheftCount++; + } + + previousAssetsTotal = currentAssetsTotal; + } + + BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount)); + } + + // Regression for the dual-rounding fix at coarse (integer-MPT) scale. + // + // Loan: P=1, r=50% (50000 tenth-bips), n=3, yearly interval. The + // amortization schedule produces a fractional principal + // (~0.47) which under round-to-nearest collapses to 0 in a single + // step, causing `doPayment`'s strict `>` assertion on principal to + // fire mid-loan. With fixCleanup3_2_0 enabled, principal is rounded + // upward (sticks at 1 across the first two periods) and only clears + // in the final payment. + // + // The test pays one period at a time across three LoanPay + // transactions and verifies the loan completes (paymentRemaining=0) + // with totals matching the loan's economics (1 principal + 2 interest). + void + testIntegerScalePrincipalSticks(FeatureBitset features) + { + // Without fixCleanup3_2_0, this behavior will abort the server, so + // don't run without it. + if (!features[fixCleanup3_2_0]) + return; + + testcase("edge: integer MPT principal stuck mid-loan completes via final"); + + using namespace jtx; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(100'000), issuer, lender, borrower); + env.close(); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.maxAmt = 100'000, .flags = tfMPTCanTransfer}); + PrettyAsset const asset{mptt.issuanceID()}; + + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + + env(pay(issuer, lender, asset(10'000))); + env(pay(issuer, borrower, asset(10'000))); + env.close(); + + Vault const vault{env}; + auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); + env(vaultTx); + env.close(); + + env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)})); + env.close(); + + auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender)); + env(loan_broker::set(lender, vaultKeylet.key), + loan_broker::kDebtMaximum(Number{100}), + Fee(env.current()->fees().base * 2)); + env.close(); + + auto const brokerStateBefore = env.le(brokerKeylet); + if (!BEAST_EXPECT(brokerStateBefore)) + return; + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence); + + env(loan::set(borrower, brokerKeylet.key, Number{1}), + Sig(sfCounterpartySignature, lender), + loan::kInterestRate(TenthBips32{50'000}), + loan::kPaymentTotal(3), + loan::kPaymentInterval(31'536'000), + Fee(env.current()->fees().base * 2)); + env.close(); + + auto const borrowerStart = env.balance(borrower, asset).value(); + + // Three separate periodic payments of 1 each. Expected per-period + // evolution at integer MPT scale (TVO = PO + interestDue + + // managementFeeDue): + // start: PO=1, TVO=3, paymentRemaining=3 + // after pay #1: PO=1, TVO=2, paymentRemaining=2 (principal sticks) + // after pay #2: PO=1, TVO=1, paymentRemaining=1 (principal sticks) + // after pay #3: PO=0, TVO=0, paymentRemaining=0 (final clears) + std::array const expectedPO{Number{1}, Number{1}, Number{0}}; + std::array const expectedTVO{Number{2}, Number{1}, Number{0}}; + std::array const expectedRemaining{2, 1, 0}; + + for (int i = 0; i < 3; ++i) + { + env(loan::pay(borrower, loanKeylet.key, asset(1)), Ter(tesSUCCESS)); + env.close(); + + auto const sle = env.le(loanKeylet); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]); + BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]); + BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]); + } + + // Borrower paid 3 total regardless of fee split (1 principal + 2 + // interest+fee, matching loan economics). + auto const borrowerEnd = env.balance(borrower, asset).value(); + BEAST_EXPECT(borrowerStart - borrowerEnd == asset(3).value()); + } + +#if LOAN_TODO + void + testLoanCoverMinimumRoundingExploit(FeatureBitset features) + { + auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) { + testcase << "LoanBrokerCoverClawback drains cover via rounding" + << " principalRequested=" << to_string(principalRequest); + + using namespace jtx; + using namespace loan; + using namespace loan_broker; + + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000'000), issuer, lender, borrower); + env.close(); + + env(fset(issuer, asfAllowTrustLineClawback)); + env.close(); + + PrettyAsset const asset = issuer[iouCurrency]; + env(trust(lender, asset(2'000'0000))); + env(trust(borrower, asset(2'000'0000))); + env.close(); + + env(pay(issuer, lender, asset(2'000'0000))); + env.close(); + + BrokerParameters brokerParams{.debtMax = 0, .coverRateMin = TenthBips32{10'000}}; + BrokerInfo broker{createVaultAndBroker(env, asset, lender, brokerParams)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + auto createTx = env.jt( + set(borrower, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + kPaymentInterval(600), + kPaymentTotal(1), + kGracePeriod(60)); + env(createTx); + env.close(); + + auto const brokerBefore = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerBefore); + if (!brokerBefore) + return; + + Number const debtOutstanding = brokerBefore->at(sfDebtTotal); + Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable); + + BEAST_EXPECT(debtOutstanding > Number{}); + BEAST_EXPECT(coverAvailableBefore > Number{}); + + log << "debt=" << to_string(debtOutstanding) + << " cover_available=" << to_string(coverAvailableBefore); + + env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID)); + env.close(); + + auto const brokerAfter = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerAfter); + if (!brokerAfter) + return; + + Number const debtAfter = brokerAfter->at(sfDebtTotal); + // the debt has not changed + BEAST_EXPECT(debtAfter == debtOutstanding); + + Number const coverAvailableAfter = brokerAfter->at(sfCoverAvailable); + + // since the cover rate min != 0, the cover available should not + // be zero + BEAST_EXPECT(coverAvailableAfter != Number{}); + }; + + // Call the lambda with different principal values + testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units + testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units + testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units + testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units + } +#endif + + // A residual overpayment can reduce the stored principal by one scale-unit + // *less* than computeOverpaymentComponents predicts, firing the + // "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment: + // + // trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding + // + // tryOverpayment re-amortizes the loan at the reduced principal, then + // re-derives the theoretical principal from the new periodic payment via + // (P * paymentFactor) / paymentFactor. That round-trip is not exact in + // Number's 19-digit arithmetic; a positive residual pushes the recomputed + // principal a hair above the exact grid point `oldPrincipal - delta`, and + // the Upward rounding in tryOverpayment then bumps it a full scale-unit + // higher. The principal therefore drops by `delta - 1 unit`, not `delta`. + // + // Concrete case (isolated, at the tryOverpayment level): + // A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10. + // After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of + // 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by + // 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10. + // + // With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact, + // on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the + // lossy (P*factor)/factor round-trip, so the assertion holds and the + // overpayment applies cleanly. The three "principal change agrees" / + // "interest paid agrees" / "principal payment matches" assertions are + // gated behind the same amendment, so without it they are disabled (the + // server does not abort) and the loan keeps the pre-amendment computation. + // + // The test runs the same scenario under both amendment settings and checks + // the stored principal against a ground-truth value derived independently of + // the loan-state computation under test. + void + testBugOverpaymentPrincipalChange() + { + testcase("bug: doOverpayment asserts 'principal change agrees'"); + + using namespace jtx; + using namespace loan; + using namespace xrpl::detail; + + struct Params + { + TenthBips32 interestRate; + TenthBips16 managementFeeRate; + std::uint32_t paymentTotal; + std::uint32_t paymentInterval; + std::int64_t principal; + Number overpayment; + TenthBips32 overpaymentInterestRate; + TenthBips32 overpaymentFeeRate; + std::optional vaultScale; + }; + + struct Result + { + Number principalOutstanding; // stored principal after the LoanPay + Number expectedNewPrincipal; // ground truth, independent of the fix + Number managementFeeChange; // managementFeeOutstanding after - before + Number unit; // one scale-unit at the loan scale + }; + + auto runScenario = [this](FeatureBitset features, Params const& p) -> Result { + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"vaultOwner"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower); + Asset const asset = iouAsset.raw(); + + auto const broker = createVaultAndBroker( + env, + iouAsset, + lender, + {.vaultDeposit = 900'000, + .debtMax = 0, + .managementFeeRate = p.managementFeeRate, + .vaultScale = p.vaultScale}); + + 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); + + env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment), + Sig(sfCounterpartySignature, lender), + kInterestRate(p.interestRate), + kPaymentTotal(p.paymentTotal), + kPaymentInterval(p.paymentInterval), + kGracePeriod(p.paymentInterval), + kOverpaymentFee(p.overpaymentFeeRate), + kOverpaymentInterestRate(p.overpaymentInterestRate), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + // The single LoanPay below makes one regular payment (the overpayment + // is smaller than one period) and leaves the residual as an + // overpayment. + auto const s = getCurrentState(env, broker, loanKeylet); + auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval); + auto const onePeriod = computePaymentComponents( + env.current()->rules(), + asset, + s.loanScale, + s.totalValue, + s.principalOutstanding, + s.managementFeeOutstanding, + s.periodicPayment, + periodicRate, + s.paymentRemaining, + p.managementFeeRate); + + // Ground truth: the stored principal must drop by exactly the regular + // payment's principal portion plus the overpayment's principal + // portion. computeOverpaymentComponents depends only on the + // overpayment amount and rates (not on the loan-state computation + // under test), so it is an independent oracle. Both components are + // computed under the same rules as the env so the payment factor + // matches. + auto const overpaymentComponents = computeOverpaymentComponents( + env.current()->rules(), + asset, + s.loanScale, + p.overpayment, + p.overpaymentInterestRate, + p.overpaymentFeeRate, + p.managementFeeRate); + Number const expectedNewPrincipal = s.principalOutstanding - + onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta; + + Number const managementFeeBefore = s.managementFeeOutstanding; + + STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment}; + env(pay(borrower, loanKeylet.key, payAmount), + Txflags(tfLoanOverpayment), + Ter(tesSUCCESS)); + env.close(); + + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + + return Result{ + .principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0, + .expectedNewPrincipal = expectedNewPrincipal, + .managementFeeChange = + (loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) - + managementFeeBefore, + .unit = Number{1, s.loanScale}}; + }; + + // Scenario 1: the original near-zero-rate principal reproduction + // (loanScale -10, no management fee). 0.049999998 is smaller than one + // period, so it stays a residual overpayment. + Params const principalCase{ + .interestRate = TenthBips32{1}, + .managementFeeRate = TenthBips16{0}, + .paymentTotal = 3, + .paymentInterval = 60, + .principal = 100, + .overpayment = Number{49999998, -9}, + .overpaymentInterestRate = TenthBips32{1000}, + .overpaymentFeeRate = TenthBips32{1000}, + .vaultScale = 1}; + + // With fixCleanup3_2_0 the stored principal lands exactly on the + // ground-truth grid point: it is reduced by exactly the overpayment's + // principal portion. This is the key correctness check: if the principal + // pin were removed (even with the assertions still gated off), the lossy + // (P * factor) / factor round-trip would leave the principal one + // scale-unit high and this would fail. + Result const fixed = runScenario(all_, principalCase); + BEAST_EXPECTS( + fixed.principalOutstanding == fixed.expectedNewPrincipal, + "fixed principal " + to_string(fixed.principalOutstanding) + " != expected " + + to_string(fixed.expectedNewPrincipal)); + + // Without the amendment the loan amortizes with the catastrophically + // cancelling near-zero payment factor, so its schedule (and ground truth) + // differ from the fixed case; the gated assertions keep the server from + // aborting and the overpayment still lands exactly on that schedule. + Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase); + BEAST_EXPECTS( + legacy.principalOutstanding == legacy.expectedNewPrincipal, + "legacy principal " + to_string(legacy.principalOutstanding) + " != expected " + + to_string(legacy.expectedNewPrincipal)); + + // Scenario 2: a normal-rate loan with a 10% management fee. At a normal + // rate the payment factor is identical across the amendment, so toggling + // fixCleanup3_2_0 isolates the fix. This overpayment (found by search) + // lands on a state where both the principal and the management fee differ + // by one scale-unit between the fixed and legacy paths. + Params const feeCase{ + .interestRate = TenthBips32{10000}, + .managementFeeRate = TenthBips16{10000}, + .paymentTotal = 6, + .paymentInterval = 30u * 24 * 60 * 60, + .principal = 1000, + .overpayment = Number{214367363, -10}, + .overpaymentInterestRate = TenthBips32{0}, + .overpaymentFeeRate = TenthBips32{0}, + .vaultScale = std::nullopt}; + + Result const feeFixed = runScenario(all_, feeCase); + Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase); + + // With the fix the principal is the exact reduction; without it the lossy + // (P * factor) / factor round-trip leaves it one scale-unit high. + BEAST_EXPECTS( + feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal, + "fee-case fixed principal " + to_string(feeFixed.principalOutstanding) + + " != expected " + to_string(feeFixed.expectedNewPrincipal)); + BEAST_EXPECTS( + feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit, + "fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) + + " != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit)); + + // Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee + // outstanding drops. + // + // Unlike the principal that is already at the correct precision, the re-amortized + // management fee is tenthBipsOfValue of the new schedule's gross interest, which depends + // on the recomputed periodic payment. So the expected change below is a pinned constant + // captured from a passing run a magic value only because there is nothing simpler to + // compare against. + // + // At the integration level, toggling the amendment also changes the regular payment's + // rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee + // fix. + BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange); + BEAST_EXPECTS( + (feeFixed.managementFeeChange == Number{-8219709543, -10}), + "fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange)); + } + + // An overpayment whose residual amount has more precision than loanScale + // fires the isRounded(asset, overpayment, loanScale) assertion in + // computeOverpaymentComponents (and a downstream "interest paid agrees" + // assertion in doOverpayment). fixCleanup3_2_0 rounds the residual down + // to loanScale before passing it in. The pre-amendment path can't be + // tested here because the assertion fires in Debug builds and aborts + // the test process — see the PR description for context. + void + testBugOverpayUnroundedAmount() + { + testcase("bug: computeOverpaymentComponents isRounded assertion"); + + using namespace jtx; + using namespace loan; + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"vaultOwner"}; + Account const borrower{"borrower"}; + + PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower); + + auto const broker = createVaultAndBroker( + env, + iouAsset, + lender, + {.vaultDeposit = 100'000, + .debtMax = 5000, + .managementFeeRate = TenthBips16{1000}, + .vaultScale = 1}); + + auto const sleBroker = env.le(broker.brokerKeylet()); + if (!BEAST_EXPECT(sleBroker)) + return; + auto const loanSequence = sleBroker->at(sfLoanSequence); + auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence); + + using namespace loan; + env(set(borrower, broker.brokerID, Number{1000}, tfLoanOverpayment), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32{10000}), + kPaymentTotal(12), + kPaymentInterval(60), + kGracePeriod(60), + kOverpaymentFee(TenthBips32{1000}), + kOverpaymentInterestRate(TenthBips32{1000}), + Fee(env.current()->fees().base * 2), + Ter(tesSUCCESS)); + env.close(); + + // periodic * 1.5 at 15-sig-digit precision: 125.000154585042. This + // has too many digits to round cleanly to loanScale=-10, so the + // overpayment residual fails the isRounded check. + STAmount const payAmount{iouAsset.raw(), Number{125'000'154'585'042LL, -12}}; + env(pay(borrower, loanKeylet.key, payAmount), Txflags(tfLoanOverpayment), Ter(tesSUCCESS)); + env.close(); + } + + // A near-zero interest rate on a 100 USD loan + // produces total interest of ~6 units at loanScale -9. Numerical error + // in the amortization formula pushes the theoretical principal above + // the theoretical value, producing a negative theoretical interest. + // The payment delta then exceeds the actual outstanding interest, + // violating XRPL_ASSERT_PARTS in computePaymentComponents. + void + testBugInterestDueDeltaCrash() + { + testcase("bug: LoanPay asserts 'interest due delta' on near-zero rate"); + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, all_); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + PrettyAsset const iouAsset = issuer["USD"]; + env(trust(lender, iouAsset(1'000'000'000))); + env(trust(borrower, iouAsset(1'000'000'000))); + env(pay(issuer, lender, iouAsset(5'000'000))); + env(pay(issuer, borrower, iouAsset(5'000'000))); + env.close(); + + BrokerParameters const brokerParams{ + .vaultDeposit = 1'000'000, + .debtMax = 1'000'000, + .coverRateMin = TenthBips32{0}, + .coverDeposit = 0, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + + BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)}; + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{100}; + + auto createJson = env.json( + set(borrower, broker.brokerID, principalRequest), + Fee(loanSetFee), + Json(sfCounterpartySignature, json::ValueType::Object)); + + createJson["InterestRate"] = 1; // minimum non-zero rate + createJson["PaymentTotal"] = 3; + createJson["PaymentInterval"] = 600; + + auto const keylet = nextLoanKeylet(env, broker); + + createJson = env.json(createJson, Sig(sfCounterpartySignature, lender)); + env(createJson, Ter(tesSUCCESS)); + env.close(); + + // For principal=100, n=3 the amortization schedule produces a + // periodic payment ≈ 33.33 USD. We pay 35 USD, which is more than + // one period's worth — enough for the LoanPay path to enter + // computePaymentComponents and reach the assertion that fires + // when the bug is present. With the fix, the tx applies cleanly. + env(pay(borrower, keylet.key, iouAsset(35)), Ter(tesSUCCESS)); + env.close(); + } + + void + runAmendmentIndependent() + { + for (auto const flags : {0u, tfLoanOverpayment}) + testYieldTheftRounding(flags); + testBugOverpaymentPrincipalChange(); + testBugOverpayUnroundedAmount(); + testBugInterestDueDeltaCrash(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testDustManipulation(features); + testRoundingAllowsUndercoverage(features); + testIntegerScalePrincipalSticks(features); +#if LOAN_TODO + testLoanCoverMinimumRoundingExploit(features); +#endif + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanRounding, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanSecurity_test.cpp b/src/test/app/lending/LoanSecurity_test.cpp new file mode 100644 index 0000000000..b08d80b51c --- /dev/null +++ b/src/test/app/lending/LoanSecurity_test.cpp @@ -0,0 +1,538 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +class LoanSecurity_test : public LoanTestBase +{ +private: + void + testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features) + { + // --- PoC Summary ---------------------------------------------------- + // Scenario: Borrower makes one periodic payment early (before next due) + // so doPayment sets sfPreviousPaymentDueDate to the (future) + // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one + // interval. Borrower then immediately performs a full-payment + // (tfLoanFullPayment). Why it matters: Full-payment interest accrual + // uses + // delta = now - max(prevPaymentDate, startDate) + // with an unsigned clock representation (uint32). If prevPaymentDate is + // in the future, the subtraction underflows to a very large positive + // number. This inflates roundedFullInterest and total full-close due, + // and LoanPay applies the inflated valueChange to the vault + // (sfAssetsTotal), increasing NAV. + // -------------------------------------------------------------------- + testcase("PoC: Unsigned-underflow full-pay accrual after early periodic"); + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + Env env{*this, features}; + + Account const lender{"poc_lender4"}; + Account const borrower{"poc_borrower4"}; + env.fund(XRP(3'000'000), lender, borrower); + env.close(); + + PrettyAsset const asset{xrpIssue(), 1'000'000}; + BrokerParameters const brokerParams{}; + auto const broker = createVaultAndBroker(env, asset, lender, brokerParams); + + // Create a 3-payment loan so full-payment path is enabled after 1 + // periodic payment. + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest = asset(1000).value(); + auto const originationFee = asset(0).value(); + auto const serviceFee = asset(1).value(); + auto const serviceFeePA = asset(1); + auto const lateFee = asset(0).value(); + auto const closeFee = asset(0).value(); + auto const interest = percentageToTenthBips(12); + auto const lateInterest = percentageToTenthBips(12) / 10; + auto const closeInterest = percentageToTenthBips(12) / 10; + auto const overpaymentInterest = percentageToTenthBips(12) / 10; + auto const total = 3u; + auto const interval = 600u; + auto const grace = 60u; + + auto createJtx = env.jt( + set(borrower, broker.brokerID, principalRequest, 0), + Sig(sfCounterpartySignature, lender), + kLoanOriginationFee(originationFee), + kLoanServiceFee(serviceFee), + kLatePaymentFee(lateFee), + kClosePaymentFee(closeFee), + kOverpaymentFee(percentageToTenthBips(5) / 10), + kInterestRate(interest), + kLateInterestRate(lateInterest), + kCloseInterestRate(closeInterest), + kOverpaymentInterestRate(overpaymentInterest), + kPaymentTotal(total), + kPaymentInterval(interval), + kGracePeriod(grace), + Fee(loanSetFee)); + + 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); + + env(createJtx); + env.close(); + + // Compute a regular periodic due and pay it early (before next due). + auto state = getCurrentState(env, broker, loanKeylet); + Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); + auto const components = xrpl::detail::computePaymentComponents( + env.current()->rules(), + asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + brokerParams.managementFeeRate); + STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()}; + // now < nextDue immediately after creation, so this is an early pay. + env(pay(borrower, loanKeylet.key, regularDue)); + env.close(); + + // Immediately attempt a full payoff. Compute the exact full-payment + // due to ensure the tx applies. + auto after = getCurrentState(env, broker, loanKeylet); + auto const loanSle = env.le(loanKeylet); + BEAST_EXPECT(loanSle); + auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle2); + + auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{}; + auto const closeInterestRate = + loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{}; + auto const managementFeeRate = + brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{}; + + Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval); + // Accrued + prepayment-penalty interest based on current periodic + // schedule + auto const fullPaymentInterest = computeFullPaymentInterest( + xrpl::detail::loanPrincipalFromPeriodicPayment( + env.current()->rules(), + after.periodicPayment, + periodicRate2, + after.paymentRemaining), + periodicRate2, + env.current()->parentCloseTime(), + after.paymentInterval, + after.previousPaymentDate, + static_cast(after.startDate.time_since_epoch().count()), + closeInterestRate); + + // Round to asset scale and split interest/fee parts + auto const roundedInterest = + roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale); + Number const roundedFullMgmtFee = + computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale); + Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee; + + // Show both signed and unsigned deltas to highlight the underflow. + auto const nowSecs = + static_cast(env.current()->parentCloseTime().time_since_epoch().count()); + auto const startSecs = + static_cast(after.startDate.time_since_epoch().count()); + auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs); + auto const signedDelta = + static_cast(nowSecs) - static_cast(lastPaymentDate); + auto const unsignedDelta = static_cast(nowSecs - lastPaymentDate); + log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs + << " now=" << nowSecs << " signedDelta=" << signedDelta + << " unsignedDelta=" << unsignedDelta << std::endl; + + // Reference (clamped) computation: emulate a non-negative accrual + // window by clamping prevPaymentDate to 'now' for the full-pay path. + auto const prevClamped = std::min(after.previousPaymentDate, nowSecs); + auto const fullPaymentInterestClamped = computeFullPaymentInterest( + xrpl::detail::loanPrincipalFromPeriodicPayment( + env.current()->rules(), + after.periodicPayment, + periodicRate2, + after.paymentRemaining), + periodicRate2, + env.current()->parentCloseTime(), + after.paymentInterval, + prevClamped, + startSecs, + closeInterestRate); + auto const roundedInterestClamped = + roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale); + Number const roundedFullMgmtFeeClamped = computeManagementFee( + asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale); + Number const roundedFullInterestClamped = + roundedInterestClamped - roundedFullMgmtFeeClamped; + STAmount const fullDueClamped{ + asset, + after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped + + closePaymentFee}; + + // Collect vault NAV before closing payment + auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{}; + auto const vaultKey2 = keylet::vault(vaultId2); + auto const vaultBefore = env.le(vaultKey2); + BEAST_EXPECT(vaultBefore); + Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{}; + + STAmount const fullDue{ + asset, + after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee + + closePaymentFee}; + + log << "PoC payoff: principalOutstanding=" << after.principalOutstanding + << " roundedFullInterest=" << roundedFullInterest + << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee + << " fullDue=" << to_string(fullDue.getJson()) << std::endl; + log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped + << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped + << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl; + + env(pay(borrower, loanKeylet.key, fullDue), Txflags(tfLoanFullPayment)); + env.close(); + + // Sanity: underflow present (unsigned delta very large relative to + // interval) + BEAST_EXPECT(unsignedDelta > after.paymentInterval); + + // Compare vault NAV before/after the full close + auto const vaultAfter = env.le(vaultKey2); + BEAST_EXPECT(vaultAfter); + if (vaultAfter) + { + auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal); + log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore + << " assetsTotalAfter=" << assetsTotalAfter + << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl; + + // Regression check: the underflowed window must be clamped so the + // payoff matches the non-underflow reference, i.e. no overcharge. + BEAST_EXPECT(fullDue == fullDueClamped); + if (fullDue != fullDueClamped) + log << "PoC delta: overcharge (fullDue > clamped)" << std::endl; + } + + // Loan should be paid off + auto const finalLoan = env.le(loanKeylet); + BEAST_EXPECT(finalLoan); + if (finalLoan) + { + BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0); + } + } + + void + testRIPD3831(FeatureBitset features) + { + using namespace jtx; + + testcase("RIPD-3831"); + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + // .managementFeeRate = TenthBips16{5919}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{200'000, -6}, + .lateFee = Number{200, -6}, + .interest = TenthBips32{50'000}, + .payTotal = 10, + .payInterval = 150}; + + auto const assetType = AssetType::XRP; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + + using tp = NetClock::time_point; + using d = NetClock::duration; + + auto state = getCurrentState(env, broker, loanKeylet); + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}}); + } + + topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); + + using namespace jtx::loan; + + auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue))); + + { + auto const submitParam = to_string(jv); + auto const jr = env.rpc("submit", borrower.name(), submitParam); + + BEAST_EXPECT(jr.isMember(jss::result)); + } + + env.close(); + + // Make sure the system keeps responding + env(noop(borrower)); + env.close(); + env(noop(issuer)); + env.close(); + env(noop(lender)); + env.close(); + } + + void + testRIPD3459(FeatureBitset features) + { + testcase("RIPD-3459 - LoanBroker incorrect debt total"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 200'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{500}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{100'000, -4}, + .interest = TenthBips32{100'000}, + .payTotal = 10}; + + auto const assetType = AssetType::MPT; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) + { + if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); + } + } + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + PaymentParameters{.showStepBalances = true}); + + if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle)) + { + if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding)); + BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::kZero); + } + } + } + + void + testRIPD3901() + { + testcase("Crash with tfLoanOverpayment"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const depositor{"depositor"}; + auto const txFee = Fee(XRP(100)); + + Env env(*this); + Vault const vault(env); + + env.fund(XRP(10'000), lender, issuer, borrower, depositor); + env.close(); + + auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()}); + env(tx, txFee); + env.close(); + + env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}), + txFee); + env.close(); + + auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender)); + + env(loan_broker::set(lender, vaultKeyLet.key), txFee); + env.close(); + + STAmount const debtMaximumRequest = XRPAmount(200'000); + + env(set(borrower, brokerKeyLet.key, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(50'000)), + kPaymentTotal(2), + kPaymentInterval(150), + Txflags(tfLoanOverpayment), + txFee); + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence); + + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) + { + env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)), + Txflags(tfLoanOverpayment), + txFee); + env.close(); + } + } + + void + testRIPD3902(FeatureBitset features) + { + testcase("RIPD-3902 - 1 IOU loan payments"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 10, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{1, 0}, + .interest = TenthBips32{100'000}, + .payTotal = 5, + .payInterval = 150, + .gracePd = 60}; + + auto const assetType = AssetType::IOU; + + Env env{*this, features}; + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + PaymentParameters{.showStepBalances = true}); + } + + void + runAmendmentIndependent() + { + testRIPD3901(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features); + testRIPD3831(features); + testRIPD3459(features); + testRIPD3902(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanSecurity, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanSet_test.cpp b/src/test/app/lending/LoanSet_test.cpp new file mode 100644 index 0000000000..85528ee9a0 --- /dev/null +++ b/src/test/app/lending/LoanSet_test.cpp @@ -0,0 +1,607 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanSet_test : public LoanTestBase +{ +private: + void + testLoanSet(FeatureBitset features) + { + using namespace jtx; + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + Account const borrower{"borrower"}; + + struct CaseArgs + { + bool requireAuth = false; + bool authorizeBorrower = false; + int initialXRP = 1'000'000; + }; + + auto const testCase = [&, this]( + std::function mptTest, + std::function iouTest, + CaseArgs args = {}) { + Env env(*this, features); + env.fund(XRP(args.initialXRP), issuer, lender, borrower); + env.close(); + if (args.requireAuth) + { + env(fset(issuer, asfRequireAuth)); + env.close(); + } + + // We need two different asset types, MPT and IOU. Prepare MPT + // first + MPTTester mptt{env, issuer, kMptInitNoFund}; + + auto const kNone = LedgerSpecificFlags(0); + mptt.create( + {.flags = tfMPTCanTransfer | tfMPTCanLock | + (args.requireAuth ? tfMPTRequireAuth : kNone)}); + env.close(); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); + env.close(); + if (args.requireAuth) + { + mptt.authorize({.account = issuer, .holder = lender}); + if (args.authorizeBorrower) + mptt.authorize({.account = issuer, .holder = borrower}); + env.close(); + } + + env(pay(issuer, lender, mptAsset(10'000'000))); + env.close(); + + // Prepare IOU + PrettyAsset const iouAsset = issuer[iouCurrency_]; + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env.close(); + if (args.requireAuth) + { + env(trust(issuer, iouAsset(0), lender, tfSetfAuth)); + env(pay(issuer, lender, iouAsset(10'000'000))); + if (args.authorizeBorrower) + { + env(trust(issuer, iouAsset(0), borrower, tfSetfAuth)); + env(pay(issuer, borrower, iouAsset(10'000))); + } + } + else + { + env(pay(issuer, lender, iouAsset(10'000'000))); + env(pay(issuer, borrower, iouAsset(10'000))); + } + env.close(); + + // Create vaults and loan brokers + std::array const assets{mptAsset, iouAsset}; + std::vector brokers; + brokers.reserve(assets.size()); + for (auto const& asset : assets) + { + brokers.emplace_back(createVaultAndBroker(env, asset, lender)); + } + + if (mptTest) + mptTest(env, brokers[0], mptt); + if (iouTest) + iouTest(env, brokers[1]); + }; + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT issuer is borrower, issuer submits"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + + testcase("MPT issuer is borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(issuer), + Sig(sfCounterpartySignature, issuer), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU issuer is borrower, issuer submits"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + + testcase("IOU issuer is borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(issuer), + Sig(sfCounterpartySignature, issuer), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT unauthorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + + testcase("MPT unauthorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU unauthorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + + testcase("IOU unauthorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + }, + CaseArgs{.requireAuth = true}); + + auto const [acctReserve, incReserve] = [this]() -> std::pair { + Env const env{*this, testableAmendments()}; + return { + env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(), + env.current()->fees().increment.drops() / kDropsPerXrp.drops()}; + }(); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, borrower has " + "no reserve"); + mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize}); + env.close(); + + auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 == nullptr); + + // Burn some XRP + env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); + env.close(); + + // Cannot create loan, not enough reserve to create MPToken + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create MPToken + env(pay(issuer, borrower, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 != nullptr); + }, + {}, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + {}, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, borrower has " + "no reserve"); + // Remove trust line from borrower to issuer + env.trust(broker.asset(0), borrower); + env.close(); + + env(pay(borrower, issuer, broker.asset(10'000))); + env.close(); + auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get()); + auto const sleLine1 = env.le(trustline); + BEAST_EXPECT(sleLine1 == nullptr); + + // Burn some XRP + env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2)))); + env.close(); + + // Cannot create loan, not enough reserve to create trust line + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create trust line + env(pay(issuer, borrower, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleLine2 = env.le(trustline); + BEAST_EXPECT(sleLine2 != nullptr); + }, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, lender has " + "no reserve"); + auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 != nullptr); + + env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); + env.close(); + + mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + + // Burn some XRP + env(noop(lender), Fee(XRP(incReserve))); + env.close(); + + // Cannot create loan, not enough reserve to create MPToken + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecINSUFFICIENT_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create MPToken + env(pay(issuer, lender, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleMPT3 = env.le(mptoken); + BEAST_EXPECT(sleMPT3 != nullptr); + }, + {}, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + {}, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, lender has no " + "reserve"); + // Remove trust line from lender to issuer + env.trust(broker.asset(0), lender); + env.close(); + + auto const trustline = keylet::trustLine(lender, broker.asset.raw().get()); + auto const sleLine1 = env.le(trustline); + BEAST_EXPECT(sleLine1 != nullptr); + + env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value())))); + env.close(); + auto const sleLine2 = env.le(trustline); + BEAST_EXPECT(sleLine2 == nullptr); + + // Burn some XRP + env(noop(lender), Fee(XRP(incReserve))); + env.close(); + + // Cannot create loan, not enough reserve to create trust line + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_LINE_INSUF_RESERVE}); + env.close(); + + // Can create loan now, will implicitly create trust line + env(pay(issuer, lender, XRP(incReserve))); + env.close(); + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + env.close(); + + auto const sleLine3 = env.le(trustline); + BEAST_EXPECT(sleLine3 != nullptr); + }, + CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, unauthorized lender"); + auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender); + auto const sleMPT1 = env.le(mptoken); + BEAST_EXPECT(sleMPT1 != nullptr); + + env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount)))); + env.close(); + + mptt.authorize({.account = lender, .flags = tfMPTUnauthorize}); + env.close(); + + auto const sleMPT2 = env.le(mptoken); + BEAST_EXPECT(sleMPT2 == nullptr); + + // Cannot create loan, lender not authorized to receive fee + env(set(borrower, broker.brokerID, principalRequest), + kLoanOriginationFee(broker.asset(1).value()), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + env.close(); + + // Cannot create loan, even without an origination fee + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter{tecNO_AUTH}); + env.close(); + + // No MPToken for lender - no authorization and no payment + auto const sleMPT3 = env.le(mptoken); + BEAST_EXPECT(sleMPT3 == nullptr); + }, + {}, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU authorized borrower, borrower submits"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("MPT authorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + + testcase("IOU authorized borrower, lender submits"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + jtx::Account const alice{"alice"}; + jtx::Account const bella{"bella"}; + auto const msigSetup = [&](Env& env, Account const& account) { + json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}}); + env(tx1); + env.close(); + }; + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + msigSetup(env, lender); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, borrower submits, lender " + "multisign"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + msigSetup(env, lender); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, borrower submits, lender " + "multisign"); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + msigSetup(env, borrower); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "MPT authorized borrower, lender submits, borrower " + "multisign"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + [&, this](Env& env, BrokerInfo const& broker) { + using namespace loan; + msigSetup(env, borrower); + Number const principalRequest = broker.asset(1'000).value(); + + testcase( + "IOU authorized borrower, lender submits, borrower " + "multisign"); + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(borrower), + Msig(sfCounterpartySignature, alice, bella), + Fee(env.current()->fees().base * 5)); + }, + CaseArgs{.requireAuth = true, .authorizeBorrower = true}); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit; + env(tx); + env.close(); + + testcase("Vault at maximum value"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + kInterestRate(TenthBips32(10'000)), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + Ter(tecLIMIT_EXCEEDED)); + }, + nullptr); + + testCase( + [&, this](Env& env, BrokerInfo const& broker, auto&) { + using namespace loan; + Number const principalRequest = broker.asset(1'000).value(); + Vault const vault{env}; + auto tx = vault.set({.owner = lender, .id = broker.vaultID}); + tx[sfAssetsMaximum] = + BrokerParameters::defaults().vaultDeposit + broker.asset(1).number(); + env(tx); + env.close(); + + testcase("Vault maximum value exceeded"); + env(set(issuer, broker.brokerID, principalRequest), + kCounterparty(lender), + kInterestRate(TenthBips32(100'000)), + Sig(sfCounterpartySignature, lender), + Fee(env.current()->fees().base * 5), + kPaymentTotal(2), + kPaymentInterval(3600 * 24), + Ter(tecLIMIT_EXCEEDED)); + }, + nullptr); + } + +public: + void + run() override + { + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + testLoanSet(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanSet, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanTestBase.h b/src/test/app/lending/LoanTestBase.h new file mode 100644 index 0000000000..35aa1e26cb --- /dev/null +++ b/src/test/app/lending/LoanTestBase.h @@ -0,0 +1,2949 @@ +#pragma once + +#include +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +class LoanTestBase : public beast::unit_test::Suite +{ +protected: + // Ensure that all the features needed for Lending Protocol are included, + // even if they are set to unsupported. + // + // featureLendingProtocolV1_1 is excluded from the default set: it changes + // Vault/LoanBroker accounting (AssetsTotal/DebtTotal/LossUnrealized), and + // most of this file's tests assert whole-life-specific expected values + // for those fields. Tests that specifically exercise the amendment opt + // it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`). + FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1}; + std::string const iouCurrency_{"IOU"}; + + struct BrokerParameters + { + Number vaultDeposit = 1'000'000; + Number debtMax = 25'000; + TenthBips32 coverRateMin = percentageToTenthBips(10); + int coverDeposit = 1000; + TenthBips16 managementFeeRate{100}; + TenthBips32 coverRateLiquidation = percentageToTenthBips(25); + std::string data = {}; // NOLINT(readability-redundant-member-init) + std::uint32_t flags = 0; + // If set, the vault is created with this sfScale value. Useful for + // tests that need finer loanScale to exercise rounding edge cases. + std::optional vaultScale = + std::nullopt; // NOLINT(readability-redundant-member-init) + + [[nodiscard]] Number + maxCoveredLoanValue(Number const& currentDebt) const + { + NumberRoundModeGuard const mg(Number::RoundingMode::Downward); + auto debtLimit = coverDeposit * kTenthBipsPerUnity.value() / coverRateMin.value(); + + return debtLimit - currentDebt; + } + + static BrokerParameters const& + defaults() + { + static BrokerParameters const kResult{}; + return kResult; + } + + // TODO: create an operator() which returns a transaction similar to + // LoanParameters + }; + + struct BrokerInfo + { + jtx::PrettyAsset asset; + uint256 brokerID; + uint256 vaultID; + BrokerParameters params; + BrokerInfo( + jtx::PrettyAsset const& asset, + Keylet const& brokerKeylet, + Keylet const& vaultKeylet, + BrokerParameters p) + : asset(asset) + , brokerID(brokerKeylet.key) + , vaultID(vaultKeylet.key) + , params(std::move(p)) + { + } + + [[nodiscard]] Keylet + brokerKeylet() const + { + return keylet::loanBroker(brokerID); + } + [[nodiscard]] Keylet + vaultKeylet() const + { + return keylet::vault(vaultID); + } + + [[nodiscard]] int + vaultScale(jtx::Env const& env) const + { + using namespace jtx; + + auto const vaultSle = env.le(keylet::vault(vaultID)); + return getAssetsTotalScale(vaultSle); + } + }; + + struct LoanParameters + { + // The account submitting the transaction. May be borrower or broker. + jtx::Account account; + // The counterparty. Should be the other of borrower or broker. + jtx::Account counter; + // Whether the counterparty is specified in the `counterparty` field, or + // only signs. + bool counterpartyExplicit = true; + Number principalRequest; + // NOLINTBEGIN(readability-redundant-member-init) + std::optional setFee = std::nullopt; + std::optional originationFee = std::nullopt; + std::optional serviceFee = std::nullopt; + std::optional lateFee = std::nullopt; + std::optional closeFee = std::nullopt; + std::optional overFee = std::nullopt; + std::optional interest = std::nullopt; + std::optional lateInterest = std::nullopt; + std::optional closeInterest = std::nullopt; + std::optional overpaymentInterest = std::nullopt; + std::optional payTotal = std::nullopt; + std::optional payInterval = std::nullopt; + std::optional gracePd = std::nullopt; + std::optional flags = std::nullopt; + // NOLINTEND(readability-redundant-member-init) + + template + jtx::JTx + operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const + { + using namespace jtx; + using namespace jtx::loan; + + JTx jt{loan::set( + account, + broker.brokerID, + broker.asset(principalRequest).number(), + flags.value_or(0))}; + + Sig(sfCounterpartySignature, counter)(env, jt); + + Fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt); + + if (counterpartyExplicit) + kCounterparty(counter)(env, jt); + if (originationFee) + kLoanOriginationFee(broker.asset(*originationFee).number())(env, jt); + if (serviceFee) + kLoanServiceFee(broker.asset(*serviceFee).number())(env, jt); + if (lateFee) + kLatePaymentFee(broker.asset(*lateFee).number())(env, jt); + if (closeFee) + kClosePaymentFee(broker.asset(*closeFee).number())(env, jt); + if (overFee) + kOverpaymentFee (*overFee)(env, jt); + if (interest) + kInterestRate (*interest)(env, jt); + if (lateInterest) + kLateInterestRate (*lateInterest)(env, jt); + if (closeInterest) + kCloseInterestRate (*closeInterest)(env, jt); + if (overpaymentInterest) + kOverpaymentInterestRate (*overpaymentInterest)(env, jt); + if (payTotal) + kPaymentTotal (*payTotal)(env, jt); + if (payInterval) + kPaymentInterval (*payInterval)(env, jt); + if (gracePd) + kGracePeriod (*gracePd)(env, jt); + + return env.jt(jt, fN...); + } + }; + + struct PaymentParameters + { + Number overpaymentFactor = Number{1}; + std::optional overpaymentExtra = std::nullopt; + std::uint32_t flags = 0; + bool showStepBalances = false; + bool validateBalances = true; + + static PaymentParameters const& + defaults() + { + static PaymentParameters const kResult{}; + return kResult; + } + }; + + struct LoanState + { + std::uint32_t previousPaymentDate = 0; + NetClock::time_point startDate; + std::uint32_t nextPaymentDate = 0; + std::uint32_t paymentRemaining = 0; + std::int32_t const loanScale = 0; + Number totalValue = 0; + Number principalOutstanding = 0; + Number managementFeeOutstanding = 0; + Number periodicPayment = 0; + std::uint32_t flags = 0; + std::uint32_t const paymentInterval = 0; + TenthBips32 const interestRate{}; + }; + + /** + * Helper class to compare the expected state of a loan and loan broker + * against the data in the ledger. + */ + struct VerifyLoanStatus + { + public: + jtx::Env const& env; + BrokerInfo const& broker; + jtx::Account const& pseudoAccount; + Keylet const& loanKeylet; + + VerifyLoanStatus( + jtx::Env const& env, + BrokerInfo const& broker, + jtx::Account const& pseudo, + Keylet const& keylet) + : env(env), broker(broker), pseudoAccount(pseudo), loanKeylet(keylet) + { + } + + /** + * Checks the expected broker state against the ledger + */ + void + checkBroker( + Number const& principalOutstanding, + Number const& interestOwed, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + std::uint32_t ownerCount) const + { + using namespace jtx; + if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + env.test.BEAST_EXPECT(brokerSle)) + { + TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + auto const brokerDebt = brokerSle->at(sfDebtTotal); + + if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + env.test.BEAST_EXPECT(vaultSle)) + { + auto const expectedDebt = + env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? principalOutstanding + : principalOutstanding + interestOwed; + env.test.BEAST_EXPECT(brokerDebt == expectedDebt); + env.test.BEAST_EXPECT( + env.balance(pseudoAccount, broker.asset).number() == + brokerSle->at(sfCoverAvailable)); + env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount); + + Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)}; + env.test.BEAST_EXPECT( + vaultSle->at(sfAssetsAvailable) == + env.balance(vaultPseudo, broker.asset).number()); + if (ownerCount == 0) + { + // The Vault must be perfectly balanced if there + // are no loans outstanding + auto const total = vaultSle->at(sfAssetsTotal); + auto const available = vaultSle->at(sfAssetsAvailable); + env.test.BEAST_EXPECT(total == available); + env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); + } + } + } + } + + void + checkPayment( + std::int32_t loanScale, + jtx::Account const& account, + jtx::PrettyAmount const& balanceBefore, + STAmount const& expectedPayment, + jtx::PrettyAmount const& adjustment) const + { + auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent()); + + STAmount const balanceChangeAmount{ + broker.asset, + roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)}; + { + auto const difference = roundToScale( + env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount), + borrowerScale); + env.test.expect( + roundToScale(difference, loanScale) >= beast::kZero, + "Balance before: " + to_string(balanceBefore.value()) + + ", expected change: " + to_string(balanceChangeAmount) + + ", difference (balance after - expected): " + to_string(difference), + __FILE__, + __LINE__); + } + } + + /** + * Checks both the loan and broker expect states against the ledger + */ + void + operator()( + std::uint32_t previousPaymentDate, + std::uint32_t nextPaymentDate, + std::uint32_t paymentRemaining, + Number const& loanScale, + Number const& totalValue, + Number const& principalOutstanding, + Number const& managementFeeOutstanding, + Number const& periodicPayment, + std::uint32_t flags) const + { + using namespace jtx; + if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan)) + { + env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate); + env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining); + env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate); + env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); + env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue); + env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding); + env.test.BEAST_EXPECT( + loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding); + env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment); + env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); + + auto const ls = constructLoanState(loan); + + auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; + auto const paymentInterval = loan->at(sfPaymentInterval); + checkBroker( + principalOutstanding, + ls.interestDue, + interestRate, + paymentInterval, + paymentRemaining, + 1); + + if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + env.test.BEAST_EXPECT(brokerSle)) + { + if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + env.test.BEAST_EXPECT(vaultSle)) + { + if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u)) + { + env.test.BEAST_EXPECT( + vaultSle->at(sfLossUnrealized) == + (env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? principalOutstanding + : totalValue - managementFeeOutstanding)); + } + else + { + env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0); + } + } + } + } + } + + /** + * Checks both the loan and broker expect states against the ledger + */ + void + operator()(LoanState const& state) const + { + operator()( + state.previousPaymentDate, + state.nextPaymentDate, + state.paymentRemaining, + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + state.flags); + }; + }; + + BrokerInfo + createVaultAndBroker( + jtx::Env& env, + jtx::PrettyAsset const& asset, + jtx::Account const& lender, + BrokerParameters const& params = BrokerParameters::defaults()) + { + using namespace jtx; + + Vault const vault{env}; + + auto const deposit = asset(params.vaultDeposit); + auto const debtMaximumValue = asset(params.debtMax).value(); + auto const coverDepositValue = asset(params.coverDeposit).value(); + + auto const coverRateMinValue = params.coverRateMin; + + auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset}); + if (params.vaultScale) + tx[sfScale] = *params.vaultScale; + env(tx); + env.close(); + BEAST_EXPECT(env.le(vaultKeylet)); + + env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit})); + env.close(); + if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault)) + { + BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value()); + } + + auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender)); + + using namespace loan_broker; + env(set(lender, vaultKeylet.key, params.flags), + kData(params.data), + kManagementFeeRate(params.managementFeeRate), + kDebtMaximum(debtMaximumValue), + kCoverRateMinimum(coverRateMinValue), + kCoverRateLiquidation(TenthBips32(params.coverRateLiquidation))); + + if (coverDepositValue != beast::kZero) + env(coverDeposit(lender, keylet.key, coverDepositValue)); + + env.close(); + + return {asset, keylet, vaultKeylet, params}; + } + + /** + * Get the state without checking anything + */ + LoanState + getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet) + { + using d = NetClock::duration; + using tp = NetClock::time_point; + + // Lookup the current loan state + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + return LoanState{ + .previousPaymentDate = loan->at(sfPreviousPaymentDueDate), + .startDate = tp{d{loan->at(sfStartDate)}}, + .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .paymentRemaining = loan->at(sfPaymentRemaining), + .loanScale = loan->at(sfLoanScale), + .totalValue = loan->at(sfTotalValueOutstanding), + .principalOutstanding = loan->at(sfPrincipalOutstanding), + .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding), + .periodicPayment = loan->at(sfPeriodicPayment), + .flags = loan->at(sfFlags), + .paymentInterval = loan->at(sfPaymentInterval), + .interestRate = TenthBips32{loan->at(sfInterestRate)}, + }; + } + return LoanState{}; + } + + /** + * Get the state and check the values against the parameters used in + * `lifecycle` + */ + LoanState + getCurrentState( + jtx::Env const& env, + BrokerInfo const& broker, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + auto const state = getCurrentState(env, broker, loanKeylet); + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value()); + BEAST_EXPECT( + state.loanScale >= + (broker.asset.integral() + ? 0 + : std::max(broker.vaultScale(env), state.principalOutstanding.exponent()))); + BEAST_EXPECT(state.paymentInterval == 600); + { + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + BEAST_EXPECT( + state.totalValue == + roundToAsset( + broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale)); + } + BEAST_EXPECT( + state.managementFeeOutstanding == + computeManagementFee( + broker.asset, + state.totalValue - state.principalOutstanding, + broker.params.managementFeeRate, + state.loanScale)); + + verifyLoanStatus(state); + + return state; + } + + bool + canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state) + { + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + BEAST_EXPECT(vaultSle)) + { + // log << vaultSle->getJson() << std::endl; + auto const assetsUnavailable = + vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable); + auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + + (env.current()->rules().enabled(featureLendingProtocolV1_1) && + getVaultVersion(vaultSle) == VaultVersion::CashBasis + ? state.principalOutstanding + : state.totalValue - state.managementFeeOutstanding); + + if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable)) + { + return false; + } + } + } + return true; + } + + enum class AssetType { XRP = 0, IOU = 1, MPT = 2 }; + + // Specify the accounts as params to allow other accounts to be used + jtx::PrettyAsset + createAsset( + jtx::Env& env, + AssetType assetType, + BrokerParameters const& brokerParams, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + switch (assetType) + { + case AssetType::XRP: + // TODO: remove the factor, and set up loans in drops + return PrettyAsset{xrpIssue(), 1'000'000}; + + case AssetType::IOU: { + PrettyAsset const asset{issuer[iouCurrency_]}; + + auto const limit = + asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit)); + if (lender != issuer) + env(trust(lender, limit)); + if (borrower != issuer) + env(trust(borrower, limit)); + + return asset; + } + + case AssetType::MPT: { + // Enough to cover initial fees + if (!env.le(keylet::account(issuer))) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); + if (!env.le(keylet::account(lender))) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); + if (!env.le(keylet::account(borrower))) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); + + MPTTester mptt{env, issuer, kMptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + // Scale the MPT asset so interest is interesting + PrettyAsset const asset{mptt.issuanceID(), 10'000}; + // Need to do the authorization here because mptt isn't + // accessible outside + if (lender != issuer) + mptt.authorize({.account = lender}); + if (borrower != issuer) + mptt.authorize({.account = borrower}); + + env.close(); + + return asset; + } + + default: + throw std::runtime_error("Unknown asset type"); + } + } + + // Predicts the keylet of the next loan `broker` will originate, before + // that loan exists, by reading the broker's current LoanSequence. + Keylet + nextLoanKeylet(jtx::Env const& env, BrokerInfo const& broker) + { + auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerStateBefore)) + return keylet::loan(broker.brokerID, 0); + auto const loanSequence = brokerStateBefore->at(sfLoanSequence); + return keylet::loan(broker.brokerID, loanSequence); + } + + // Funds issuer/lender/borrower with XRP, creates an IOU asset issued by + // `issuer`, establishes trustlines for lender and borrower, and pays + // them starting balances. This is the exact setup shared by several of + // the fuzzer-derived regression tests below. + jtx::PrettyAsset + createFundedIouAsset( + jtx::Env& env, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, + Number const& lenderPay = 100'000'000, + Number const& borrowerPay = 1'000'000) + { + using namespace jtx; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env.close(); + + PrettyAsset const iouAsset = issuer[iouCurrency_]; + auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000))); + env(trustLenderTx); + auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000))); + env(trustBorrowerTx); + auto payLenderTx = pay(issuer, lender, iouAsset(lenderPay)); + env(payLenderTx); + auto payIssuerTx = pay(issuer, borrower, iouAsset(borrowerPay)); + env(payIssuerTx); + env.close(); + + return iouAsset; + } + + // Funds issuer/lender/borrower with XRP, sets DefaultRipple on the + // issuer, creates a "USD" IOU asset with a large trust limit, and pays + // lender/borrower starting balances. Shared setup for several + // overpayment/rounding regression tests below. + static jtx::PrettyAsset + createFundedRippleIouAsset( + jtx::Env& env, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, + Number const& lenderPay = 1'000'000, + Number const& borrowerPay = 1'000'000) + { + using namespace jtx; + + env.fund(XRP(1'000'000), issuer, lender, borrower); + env(fset(issuer, asfDefaultRipple)); + env.close(); + + PrettyAsset const iouAsset = issuer["USD"]; + STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}}; + env(trust(lender, iouLimit)); + env(trust(borrower, iouLimit)); + env(pay(issuer, lender, iouAsset(lenderPay))); + env(pay(issuer, borrower, iouAsset(borrowerPay))); + env.close(); + + return iouAsset; + } + + // Returns the broker's pseudo-account, or `fallback` if the broker's + // ledger entry cannot be read. + jtx::Account + brokerPseudoAccount(jtx::Env const& env, BrokerInfo const& broker, jtx::Account const& fallback) + { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return fallback; + auto const brokerPseudo = brokerSle->at(sfAccount); + return jtx::Account("Broker pseudo-account", brokerPseudo); + } + + void + describeLoan( + jtx::Env& env, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + AssetType assetType, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); + auto const principal = asset(loanParams.principalRequest).number(); + auto const interest = loanParams.interest.value_or(TenthBips32{}); + auto const interval = loanParams.payInterval.value_or(LoanSet::kDefaultPaymentInterval); + auto const total = loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal); + auto const feeRate = brokerParams.managementFeeRate; + auto const props = computeLoanProperties( + env.current()->rules(), + asset, + principal, + interest, + interval, + total, + feeRate, + asset(brokerParams.vaultDeposit).number().exponent()); + log << "Loan properties:\n" + << "\tPrincipal: " << principal << std::endl + << "\tInterest rate: " << interest << std::endl + << "\tPayment interval: " << interval << std::endl + << "\tManagement Fee Rate: " << feeRate << std::endl + << "\tTotal Payments: " << total << std::endl + << "\tPeriodic Payment: " << props.periodicPayment << std::endl + << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl + << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl + << "\tLoan Scale: " << props.loanScale << std::endl + << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl; + + // checkGuards returns a TER, so success is 0 + BEAST_EXPECT(!checkLoanGuards( + asset, + asset(loanParams.principalRequest).number(), + loanParams.interest.value_or(TenthBips32{}) != beast::kZero, + loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal), + props, + env.journal)); + } + + std::optional> + createLoan( + jtx::Env& env, + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower) + { + using namespace jtx; + + // Enough to cover initial fees + env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer); + if (lender != issuer) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender)); + if (borrower != issuer && borrower != lender) + env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower)); + + describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower); + + // Make the asset + auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower); + + env.close(); + if (asset.native() || lender != issuer) + { + env( + pay((asset.native() ? env.master : issuer), + lender, + asset(brokerParams.vaultDeposit + brokerParams.coverDeposit))); + } + // Fund the borrower later once we know the total loan + // size + + BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams); + + auto const pseudoAcctOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + auto const brokerPseudo = brokerSle->at(sfAccount); + return Account("Broker pseudo-account", brokerPseudo); + }(); + if (!pseudoAcctOpt) + return std::nullopt; + Account const& pseudoAcct = *pseudoAcctOpt; + + auto const loanKeyletOpt = [&]() -> std::optional { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return std::nullopt; + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // 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); + }(); + if (!loanKeyletOpt) + return std::nullopt; + Keylet const& loanKeylet = *loanKeyletOpt; + + env(loanParams(env, broker)); + + env.close(); + + return std::make_tuple(broker, loanKeylet, pseudoAcct); + } + + static void + topUpBorrower( + jtx::Env& env, + BrokerInfo const& broker, + jtx::Account const& issuer, + jtx::Account const& borrower, + LoanState const& state, + std::optional const& servFee) + { + using namespace jtx; + + STAmount const serviceFee = broker.asset(servFee.value_or(0)); + + // Ensure the borrower has enough funds to make the payments + // (including tx fees, if necessary) + auto const borrowerBalance = env.balance(borrower, broker.asset); + + auto const baseFee = env.current()->fees().base; + + // Add extra for transaction fees and reserves, if appropriate, or a + // tiny amount for the extra paid in each transaction + auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) + + (broker.asset.native() ? Number( + baseFee * state.paymentRemaining + + accountReserve(*env.current(), borrower.id(), env.journal)) + : broker.asset(15).number()); + + auto const shortage = totalNeeded - borrowerBalance.number(); + + if (shortage > beast::kZero && (broker.asset.native() || issuer != borrower)) + { + env( + pay((broker.asset.native() ? env.master : issuer), + borrower, + STAmount{broker.asset, shortage})); + } + } + + void + makeLoanPayments( + jtx::Env& env, + BrokerInfo const& broker, + LoanParameters const& loanParams, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus, + jtx::Account const& issuer, + jtx::Account const& lender, + jtx::Account const& borrower, + PaymentParameters const& paymentParams = PaymentParameters::defaults()) + { + // Make all the individual payments + using namespace jtx; + using namespace jtx::loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + + bool const showStepBalances = paymentParams.showStepBalances; + + auto const currencyLabel = getCurrencyLabel(broker.asset); + + auto const baseFee = env.current()->fees().base; + + env.close(); + auto state = getCurrentState(env, broker, loanKeylet); + + verifyLoanStatus(state); + + STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0)); + + topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; + + if (!showStepBalances) + { + log << currencyLabel << " Payment components: " + << "Payments remaining, " + << "rawInterest, rawPrincipal, " + "rawMFee, " + << "trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special" + << std::endl; + } + + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward); + + auto currentRoundedState = constructLoanState( + state.totalValue, state.principalOutstanding, state.managementFeeOutstanding); + { + auto const raw = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + if (showStepBalances) + { + log << currencyLabel << " Starting loan balances: " + << "\n\tTotal value: " << currentRoundedState.valueOutstanding + << "\n\tPrincipal: " << currentRoundedState.principalOutstanding + << "\n\tInterest: " << currentRoundedState.interestDue + << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue + << "\n\tPayments remaining " << state.paymentRemaining << std::endl; + } + else + { + log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", " + << raw.interestDue << ", " << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", " + << currentRoundedState.principalOutstanding << ", " + << currentRoundedState.interestDue << ", " + << currentRoundedState.managementFeeDue << std::endl; + } + } + + // Try to pay a little extra to show that it's _not_ + // taken + auto const extraAmount = paymentParams.overpaymentExtra + ? broker.asset(*paymentParams.overpaymentExtra).value() + : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20}); + + STAmount const transactionAmount = + STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount; + + auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number(); + auto const initialState = state; + xrpl::detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + Number totalFeesPaid = 0; + std::size_t totalPaymentsMade = 0; + + xrpl::LoanState currentTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + auto validateBorrowerBalance = [&]() { + if (borrower == issuer || !paymentParams.validateBalances) + return; + auto const totalSpent = + (totalPaid.trackedValueDelta + totalFeesPaid + + (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : kNumZero)); + BEAST_EXPECT( + env.balance(borrower, broker.asset).number() == + borrowerInitialBalance - totalSpent); + }; + + auto const defaultRound = broker.asset.integral() ? 3 : 0; + auto truncate = [defaultRound](Number const& n, std::optional places = std::nullopt) { + auto const p = places.value_or(defaultRound); + if (p == 0) + return n; + auto const factor = Number{1, p}; + return (n * factor).truncate() / factor; + }; + while (state.paymentRemaining > 0) + { + validateBorrowerBalance(); + // Compute the expected principal amount + auto const paymentComponents = xrpl::detail::computePaymentComponents( + env.current()->rules(), + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + BEAST_EXPECT( + paymentComponents.trackedValueDelta <= roundedPeriodicPayment || + (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final && + paymentComponents.trackedValueDelta >= roundedPeriodicPayment)); + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() + + paymentComponents.trackedManagementFeeDelta); + + xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); + xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; + BEAST_EXPECT( + deltas.total() == deltas.principal + deltas.interest + deltas.managementFee); + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + deltas.total() == state.periodicPayment || + (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14); + + if (!showStepBalances) + { + log << currencyLabel << " Payment components: " << state.paymentRemaining << ", " + + << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee + << ", " << paymentComponents.trackedValueDelta << ", " + << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* { + if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Final) + return "final"; + if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Extra) + return "extra"; + return "none"; + }() << std::endl; + } + + auto const totalDueAmount = + STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee}; + + if (paymentParams.validateBalances) + { + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be dust. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + diff == beast::kZero || + (diff > beast::kZero && + ((broker.asset.integral() && (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); + + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::kZero && + paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedPrincipalDelta == state.principalOutstanding); + } + + auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags)); + + env.close(d{state.paymentInterval / 2}); + + if (paymentParams.validateBalances) + { + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + } + + if (showStepBalances) + { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + { + // No reason for this not to exist + return; + } + auto const current = constructLoanState(loanSle); + auto const errors = nextTrueState - current; + log << currencyLabel << " Loan balances: " + << "\n\tAmount taken: " << paymentComponents.trackedValueDelta + << "\n\tTotal value: " << current.valueOutstanding + << " (true: " << truncate(nextTrueState.valueOutstanding) + << ", error: " << truncate(errors.total()) + << ")\n\tPrincipal: " << current.principalOutstanding + << " (true: " << truncate(nextTrueState.principalOutstanding) + << ", error: " << truncate(errors.principal) + << ")\n\tInterest: " << current.interestDue + << " (true: " << truncate(nextTrueState.interestDue) + << ", error: " << truncate(errors.interest) + << ")\n\tMgmt fee: " << current.managementFeeDue + << " (true: " << truncate(nextTrueState.managementFeeDue) + << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining " + << loanSle->at(sfPaymentRemaining) << std::endl; + + currentRoundedState = current; + } + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) + { + state.paymentRemaining = 0; + state.nextPaymentDate = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; + } + state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; + state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; + + if (paymentParams.validateBalances) + verifyLoanStatus(state); + + totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += paymentComponents.trackedInterestPart(); + totalFeesPaid += serviceFee; + ++totalPaymentsMade; + + currentTrueState = nextTrueState; + } + validateBorrowerBalance(); + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + auto const initialInterestDue = initialState.totalValue - + (initialState.principalOutstanding + initialState.managementFeeOutstanding); + if (paymentParams.validateBalances) + { + // Make sure all the payments add up + BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT(totalInterestPaid == initialInterestDue); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + } + + if (showStepBalances) + { + auto const loanSle = env.le(loanKeylet); + if (!BEAST_EXPECT(loanSle)) + { + // No reason for this not to exist + return; + } + log << currencyLabel << " Total amounts paid: " + << "\n\tTotal value: " << totalPaid.trackedValueDelta + << " (initial: " << truncate(initialState.totalValue) + << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta) + << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta + << " (initial: " << truncate(initialState.principalOutstanding) << ", error: " + << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta) + << ")\n\tInterest: " << totalInterestPaid + << " (initial: " << truncate(initialInterestDue) + << ", error: " << truncate(initialInterestDue - totalInterestPaid) + << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta + << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: " + << truncate( + initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta) + << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl; + } + } + + void + runLoan( + AssetType assetType, + BrokerParameters const& brokerParams, + LoanParameters const& loanParams, + FeatureBitset features) + { + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + Env env(*this, features); + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + PaymentParameters{.showStepBalances = true}); + } + + /** + * Runs through the complete lifecycle of a loan + * + * 1. Create a loan. + * 2. Test a bunch of transaction failure conditions. + * 3. Use the `toEndOfLife` callback to take the loan to 0. How that is done + * depends on the callback. e.g. Default, Early payoff, make all the + * normal payments, etc. + * 4. Delete the loan. The loan will alternate between being deleted by the + * lender and the borrower. + */ + void + lifecycle( + std::string const& caseLabel, + char const* label, + jtx::Env& env, + Number const& loanAmount, + int interestExponent, + jtx::Account const& lender, + jtx::Account const& borrower, + jtx::Account const& evan, + BrokerInfo const& broker, + jtx::Account const& pseudoAcct, + std::uint32_t flags, + // The end of life callback is expected to take the loan to 0 payments + // remaining, one way or another + std::function + toEndOfLife) + { + auto const [keylet, loanSequence] = [&]() { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + { + // will be invalid + return std::make_pair(keylet::loan(broker.brokerID), std::uint32_t(0)); + } + + // Broker has no loans + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + + // 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); + }(); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet); + + // No loans yet + verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0); + + if (!BEAST_EXPECT(loanSequence != 0)) + return; + + testcase << caseLabel << " " << label; + + using namespace jtx; + using namespace loan; + using namespace std::chrono_literals; + + auto applyExponent = [interestExponent, this](TenthBips32 value) mutable { + BEAST_EXPECT(value > TenthBips32(0)); + while (interestExponent > 0) + { + auto const oldValue = value; + value *= 10; + --interestExponent; + BEAST_EXPECT(value / 10 == oldValue); + } + while (interestExponent < 0) + { + auto const oldValue = value; + value /= 10; + ++interestExponent; + BEAST_EXPECT(value * 10 == oldValue); + } + return value; + }; + + auto const borrowerOwnerCount = env.ownerCount(borrower); + + auto const loanSetFee = env.current()->fees().base * 2; + LoanParameters const loanParams{ + .account = borrower, + .counter = lender, + .counterpartyExplicit = false, + .principalRequest = loanAmount, + .setFee = loanSetFee, + .originationFee = 1, + .serviceFee = 2, + .lateFee = 3, + .closeFee = 4, + .overFee = applyExponent(percentageToTenthBips(5) / 10), + .interest = applyExponent(percentageToTenthBips(12)), + // 2.4% + .lateInterest = applyExponent(percentageToTenthBips(24) / 10), + .closeInterest = applyExponent(percentageToTenthBips(36) / 10), + .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10), + .payTotal = 12, + .payInterval = 600, + .gracePd = 60, + .flags = flags, + }; + Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value(); + auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value(); + auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value(); + auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value(); + auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value(); + + auto const borrowerStartbalance = env.balance(borrower, broker.asset); + + auto createJtx = loanParams(env, broker); + // Successfully create a Loan + env(createJtx); + + env.close(); + + auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count(); + + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); + } + + { + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = 2 * env.current()->fees().base; + } + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount - + adjustment.value()); + } + + auto const loanFlags = + createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0); + + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + // log << "loan after create: " << to_string(loan->getJson()) + // << std::endl; + BEAST_EXPECT( + loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment)); + BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence); + BEAST_EXPECT(loan->at(sfBorrower) == borrower.id()); + BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID); + BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount); + BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount); + BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount); + BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount); + BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee); + BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest); + BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest); + BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest); + BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest); + BEAST_EXPECT(loan->at(sfStartDate) == startDate); + BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval); + BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd); + BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0); + BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval); + BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal); + BEAST_EXPECT( + loan->at(sfLoanScale) >= + (broker.asset.integral() + ? 0 + : std::max(broker.vaultScale(env), principalRequestAmount.exponent()))); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount); + } + + auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); + + auto const loanProperties = computeLoanProperties( + env.current()->rules(), + broker.asset.raw(), + state.principalOutstanding, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + broker.params.managementFeeRate, + state.loanScale); + + verifyLoanStatus( + 0, + startDate + *loanParams.payInterval, + *loanParams.payTotal, + state.loanScale, + loanProperties.loanState.valueOutstanding, + principalRequestAmount, + loanProperties.loanState.managementFeeDue, + loanProperties.periodicPayment, + loanFlags | 0); + + // Manage the loan + // no-op + env(manage(lender, keylet.key, 0)); + { + // no flags + auto jt = manage(lender, keylet.key, 0); + jt.removeMember(sfFlags.getName()); + env(jt); + } + // Only the lender can manage + env(manage(evan, keylet.key, 0), Ter(tecNO_PERMISSION)); + // unknown flags + env(manage(lender, keylet.key, tfLoanManageMask), Ter(temINVALID_FLAG)); + // combinations of flags are not allowed + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), Ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), Ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), Ter(temINVALID_FLAG)); + env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault), + Ter(temINVALID_FLAG)); + // invalid loan ID + env(manage(lender, broker.brokerID, tfLoanImpair), Ter(tecNO_ENTRY)); + // Loan is unimpaired, can't unimpair it again + env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); + // Loan is unimpaired, it can go into default, but only after it's past + // due + env(manage(lender, keylet.key, tfLoanDefault), Ter(tecTOO_SOON)); + + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible + env(manage(lender, keylet.key, tfLoanImpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); + // Unimpair the loan + env(manage(lender, keylet.key, tfLoanUnimpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION)); + + auto const nextDueDate = startDate + *loanParams.payInterval; + + env.close(); + + verifyLoanStatus( + 0, + nextDueDate, + *loanParams.payTotal, + loanProperties.loanScale, + loanProperties.loanState.valueOutstanding, + principalRequestAmount, + loanProperties.loanState.managementFeeDue, + loanProperties.periodicPayment, + loanFlags | 0); + + // Can't delete the loan yet. It has payments remaining. + env(del(lender, keylet.key), Ter(tecHAS_OBLIGATIONS)); + + if (BEAST_EXPECT(toEndOfLife)) + toEndOfLife(keylet, verifyLoanStatus); + env.close(); + + // Verify the loan is at EOL + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); + } + auto const borrowerStartingBalance = env.balance(borrower, broker.asset); + + // Try to delete the loan broker with an active loan + env(loan_broker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS)); + // Ensure the above tx doesn't get ordered after the LoanDelete and + // delete our broker! + env.close(); + + // Test failure cases + env(del(lender, keylet.key, tfLoanOverpayment), Ter(temINVALID_FLAG)); + env(del(evan, keylet.key), Ter(tecNO_PERMISSION)); + env(del(lender, broker.brokerID), Ter(tecNO_ENTRY)); + + // Delete the loan + // Either the borrower or the lender can delete the loan. Alternate + // between who does it across tests. + static unsigned kDeleteCounter = 0; + auto const deleter = ((++kDeleteCounter % 2) != 0u) ? lender : borrower; + env(del(deleter, keylet.key)); + env.close(); + + PrettyAmount adjustment = broker.asset(0); + if (deleter == borrower) + { + // Need to account for fees if the loan is in XRP + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + } + + // No loans left + verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0); + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartingBalance.value() - adjustment); + BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); + + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0); + } + } + + static std::string + getCurrencyLabel(Asset const& asset) + { + if (asset.native()) + return "XRP"; + if (asset.holds()) + return "IOU"; + if (asset.holds()) + return "MPT"; + return "Unknown"; + } + + /** + * Wrapper to run a series of lifecycle tests for a given asset and loan + * amount + * + * Will be used in the future to vary the loan parameters. For now, it is + * only called once. + * + * Tests a bunch of LoanSet failure conditions before lifecycle. + */ + template + void + testCaseWrapper( + jtx::Env& env, + jtx::MPTTester& mptt, + std::array const& assets, + BrokerInfo const& broker, + Number const& loanAmount, + int interestExponent) + { + using namespace jtx; + using namespace lending; + + auto const& asset = broker.asset.raw(); + auto const currencyLabel = getCurrencyLabel(asset); + auto const caseLabel = [&]() { + std::stringstream ss; + ss << "Lifecycle: " << loanAmount << " " << currencyLabel + << " Scale interest to: " << interestExponent << " "; + return ss.str(); + }(); + testcase << caseLabel; + + using namespace loan; + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + + Account const issuer{"issuer"}; + // For simplicity, lender will be the sole actor for the vault & + // brokers. + Account const lender{"lender"}; + // Borrower only wants to borrow + Account const borrower{"borrower"}; + // Evan will attempt to be naughty + Account const evan{"evan"}; + // Do not fund alice + Account const alice{"alice"}; + + Number const principalRequest = broker.asset(loanAmount).value(); + Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0); + BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10); + Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value(); + Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value(); + Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value(); + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + + auto const pseudoAcct = brokerPseudoAccount(env, broker, lender); + + auto const baseFee = env.current()->fees().base; + + auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); + // Try some failure cases + // flags are checked first + env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + + // field length validation + // sfData: good length, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kData(std::string(kMaxDataPayloadLength, 'X')), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfData: too long + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kData(std::string(kMaxDataPayloadLength + 1, 'Y')), + loanSetFee, + Ter(temINVALID)); + + // field range validation + // sfOverpaymentFee: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kOverpaymentFee(kMaxOverpaymentFee), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfOverpaymentFee: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kOverpaymentFee(kMaxOverpaymentFee + 1), + loanSetFee, + Ter(temINVALID)); + + // sfInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kInterestRate(kMaxInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kInterestRate(kMaxInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + // sfInterestRate: too small + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfLateInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kLateInterestRate(kMaxLateInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kLateInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfLateInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kLateInterestRate(kMaxLateInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + // sfLateInterestRate: too small + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kLateInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfCloseInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kCloseInterestRate(kMaxCloseInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kCloseInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfCloseInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kCloseInterestRate(kMaxCloseInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kCloseInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfOverpaymentInterestRate: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kOverpaymentInterestRate(kMaxOverpaymentInterestRate), + loanSetFee, + Ter(tefBAD_AUTH)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kOverpaymentInterestRate(TenthBips32(0)), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfOverpaymentInterestRate: too big + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kOverpaymentInterestRate(kMaxOverpaymentInterestRate + 1), + loanSetFee, + Ter(temINVALID)); + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kOverpaymentInterestRate(TenthBips32(-1)), + loanSetFee, + Ter(temINVALID)); + + // sfPaymentTotal: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kPaymentTotal(LoanSet::kMinPaymentTotal), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfPaymentTotal: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kPaymentTotal(LoanSet::kMinPaymentTotal - 1), + loanSetFee, + Ter(temINVALID)); + + // sfPaymentInterval: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kPaymentInterval(LoanSet::kMinPaymentInterval), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfPaymentInterval: too small (there is no max) + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kPaymentInterval(LoanSet::kMinPaymentInterval - 1), + loanSetFee, + Ter(temINVALID)); + + // sfGracePeriod: good value, bad account + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, borrower), + kPaymentInterval(LoanSet::kMinPaymentInterval * 2), + kGracePeriod(LoanSet::kMinPaymentInterval * 2), + loanSetFee, + Ter(tefBAD_AUTH)); + // sfGracePeriod: larger than paymentInterval + env(set(evan, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + kPaymentInterval(LoanSet::kMinPaymentInterval * 2), + kGracePeriod(LoanSet::kMinPaymentInterval * 3), + loanSetFee, + Ter(temINVALID)); + + // insufficient fee - single sign + env(set(borrower, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, lender), + Ter(telINSUF_FEE_P)); + // insufficient fee - multisign + env(signers(lender, 2, {{evan, 1}, {borrower, 1}})); + env(signers(borrower, 2, {{evan, 1}, {lender, 1}})); + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(evan, lender), + Msig(sfCounterpartySignature, evan, borrower), + Fee(env.current()->fees().base * 5 - 1), + Ter(telINSUF_FEE_P)); + // Bad multisign signatures for borrower (Account) + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(alice, issuer), + Msig(sfCounterpartySignature, evan, borrower), + Fee(env.current()->fees().base * 5), + Ter(tefBAD_SIGNATURE)); + // Bad multisign signatures for issuer (Counterparty) + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(evan, lender), + Msig(sfCounterpartySignature, alice, issuer), + Fee(env.current()->fees().base * 5 - 1), + Ter(tefBAD_SIGNATURE)); + env(signers(lender, kNone)); + env(signers(borrower, kNone)); + // multisign sufficient fee, but no signers set up + env(set(borrower, broker.brokerID, principalRequest), + kCounterparty(lender), + Msig(evan, lender), + Msig(sfCounterpartySignature, evan, borrower), + Fee(env.current()->fees().base * 5), + Ter(tefNOT_MULTI_SIGNING)); + // not the broker owner, no counterparty, not signed by broker + // owner + env(set(borrower, broker.brokerID, principalRequest), + Sig(sfCounterpartySignature, evan), + loanSetFee, + Ter(tefBAD_AUTH)); + // not the broker owner, counterparty is borrower + env(set(evan, broker.brokerID, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + loanSetFee, + Ter(tecNO_PERMISSION)); + // not a LoanBroker object, no counterparty + env(set(lender, badKeylet.key, principalRequest), + Sig(sfCounterpartySignature, evan), + loanSetFee, + Ter(temBAD_SIGNER)); + // not a LoanBroker object, counterparty is valid + env(set(lender, badKeylet.key, principalRequest), + kCounterparty(borrower), + Sig(sfCounterpartySignature, borrower), + loanSetFee, + Ter(tecNO_ENTRY)); + // borrower doesn't exist + env(set(lender, broker.brokerID, principalRequest), + kCounterparty(alice), + Sig(sfCounterpartySignature, alice), + loanSetFee, + Ter(terNO_ACCOUNT)); + + // Request more funds than the vault has available + env(set(evan, broker.brokerID, totalVaultRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecINSUFFICIENT_FUNDS)); + + // Request more funds than the broker's first-loss capital can + // cover. + env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecINSUFFICIENT_FUNDS)); + + // Frozen trust line / locked MPT issuance + // XRP can not be frozen, but run through the loop anyway to test + // the tecLIMIT_EXCEEDED case + { + auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + if (!BEAST_EXPECT(brokerSle)) + return; + + auto const vaultPseudo = [&]() { + auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID))); + if (!BEAST_EXPECT(vaultSle)) + { + // This will be wrong, but the test has failed anyway. + return Account{lender}; + } + auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount)); + return vaultPseudo; + }(); + + auto const [freeze, deepfreeze, unfreeze, expectedResult] = + [&]() -> std::tuple< + std::function, + std::function, + std::function, + TER> { + // Freeze / lock the asset + std::function const empty; + if (broker.asset.native()) + { + // XRP can't be frozen + return std::make_tuple(empty, empty, empty, tesSUCCESS); + } + if (broker.asset.holds()) + { + auto freeze = [&](Account const& holder) { + env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze)); + }; + auto deepfreeze = [&](Account const& holder) { + env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze)); + }; + auto unfreeze = [&](Account const& holder) { + env(trust( + issuer, holder[iouCurrency_](0), tfClearFreeze | tfClearDeepFreeze)); + }; + return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN); + } + + auto freeze = [&](Account const& holder) { + mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock}); + }; + auto unfreeze = [&](Account const& holder) { + mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock}); + }; + return std::make_tuple(freeze, empty, unfreeze, tecLOCKED); + }(); + + // Try freezing the accounts that can't be frozen + if (freeze) + { + for (auto const& account : {vaultPseudo, evan}) + { + // Freeze the account + freeze(account); + + // Try to create a loan with a frozen line + env(set(evan, broker.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(expectedResult)); + + // Unfreeze the account + BEAST_EXPECT(unfreeze); + unfreeze(account); + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, broker.brokerID, debtMaximumRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecLIMIT_EXCEEDED)); + } + } + + // Deep freeze the borrower, which prevents them from receiving + // funds + if (deepfreeze) + { + // Make sure evan has a trust line that so the issuer can + // freeze it. (Don't need to do this for the borrower, + // because LoanSet will create a line to the borrower + // automatically.) + env(trust(evan, issuer[iouCurrency_](100'000))); + + for (auto const& account : {// these accounts can't be frozen, which deep freeze + // implies + vaultPseudo, + evan, + // these accounts can't be deep frozen + lender}) + { + // Freeze evan + deepfreeze(account); + + // Try to create a loan with a deep frozen line + env(set(evan, broker.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(expectedResult)); + + // Unfreeze evan + BEAST_EXPECT(unfreeze); + unfreeze(account); + + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, broker.brokerID, debtMaximumRequest + 1), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecLIMIT_EXCEEDED)); + } + } + } + + // Finally! Create a loan + + auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) { + if (auto const brokerSle = env.le(keylet::loanBroker(brokerID)); + BEAST_EXPECT(brokerSle)) + { + auto const available = brokerSle->at(sfCoverAvailable); + BEAST_EXPECT(available == expected); + return available; + } + return Number{}; + }; + auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) { + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT( + state.loanScale >= + (broker.asset.integral() + ? 0 + : std::max( + broker.vaultScale(env), state.principalOutstanding.exponent()))); + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + auto const defaultAmount = roundToAsset( + broker.asset, + std::min( + tenthBipsOfValue( + tenthBipsOfValue( + brokerSle->at(sfDebtTotal), broker.params.coverRateMin), + broker.params.coverRateLiquidation), + state.totalValue - state.managementFeeOutstanding), + state.loanScale); + return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); + } + return std::make_pair(Number{}, AccountID{}); + }; + auto replenishCover = [&env, &coverAvailable]( + BrokerInfo const& broker, + AccountID const& brokerAcct, + Number const& startingCoverAvailable, + Number const& amountToBeCovered) { + coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered); + env(loan_broker::coverDeposit( + brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered})); + coverAvailable(broker.brokerID, startingCoverAvailable); + env.close(); + }; + + auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) { + return [&, impair, baseFlag]( + Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Default the loan + + // Initialize values with the current state + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); + + auto const& broker = verifyLoanStatus.broker; + auto const startingCoverAvailable = coverAvailable( + broker.brokerID, broker.asset(broker.params.coverDeposit).number()); + + if (impair) + { + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible + env(manage(lender, loanKeylet.key, tfLoanImpair), + canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED)); + + if (canImpair) + { + state.flags |= tfLoanImpair; + state.nextPaymentDate = env.now().time_since_epoch().count(); + + // Once the loan is impaired, it can't be impaired again + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + } + verifyLoanStatus(state); + } + + auto const nextDueDate = tp{d{state.nextPaymentDate}}; + + // Can't default the loan yet. The grace period hasn't + // expired + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON)); + + // Let some time pass so that the loan can be + // defaulted + env.close(nextDueDate + 60s); + + auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker); + + // Default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // The LoanBroker just lost some of it's first-loss capital. + // Replenish it. + replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered); + + state.flags |= tfLoanDefault; + state.paymentRemaining = 0; + state.totalValue = 0; + state.principalOutstanding = 0; + state.managementFeeOutstanding = 0; + state.nextPaymentDate = 0; + verifyLoanStatus(state); + + // Once a loan is defaulted, it can't be managed + env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + // Can't make a payment on it either + env(pay(borrower, loanKeylet.key, broker.asset(300)), Ter(tecKILLED)); + }; + }; + + auto singlePayment = [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus, + LoanState& state, + STAmount const& payoffAmount, + std::uint32_t numPayments, + std::uint32_t baseFlag, + std::uint32_t txFlags) { + // toEndOfLife + // + verifyLoanStatus(state); + + // Send some bogus pay transactions + env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags), + Ter(temINVALID)); + // broker.asset(80) is less than a single payment, but all these + // checks fail before that matters + env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), Ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), Ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), Ter(tecNO_PERMISSION)); + + // TODO: Write a general "isFlag" function? See STObject::isFlag. + // Maybe add a static overloaded member? + if (!(state.flags & lsfLoanOverpayment)) + { + // If the loan does not allow overpayments, send a payment that + // tries to make an overpayment. Do not include `txFlags`, so we + // don't end up duplicating the next test transaction. + // + // fixCleanup3_1_3 gates tfLoanOverpayment as a valid flag: + // with fix on → preflight passes, apply returns tecNO_PERMISSION; + // with fix off → preflight rejects the flag, returns temINVALID_FLAG. + bool const hasFix313 = env.current()->rules().enabled(fixCleanup3_1_3); + STAmount const overpayAmount{broker.asset, state.periodicPayment * Number{15, -1}}; + XRPAmount const overpayFee{ + baseFee * (Number{15, -1} / kLoanPaymentsPerFeeIncrement + 1)}; + env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), + Fee(overpayFee), + Ter(hasFix313 ? TER{tecNO_PERMISSION} : TER{temINVALID_FLAG})); + + if (hasFix313) + { + env.disableFeature(fixCleanup3_1_3); + env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment), + Fee(overpayFee), + Ter(temINVALID_FLAG)); + env.enableFeature(fixCleanup3_1_3); + } + } + // Try to send a payment marked as multiple mutually exclusive + // payment types. Do not include `txFlags`, so we don't duplicate + // the prior test transaction. + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanFullPayment), + Ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanOverpayment), + Ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanOverpayment | tfLoanFullPayment), + Ter(temINVALID_FLAG)); + env(pay(borrower, + loanKeylet.key, + broker.asset(state.periodicPayment * 2), + tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment), + Ter(temINVALID_FLAG)); + + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), Ter(tecWRONG_ASSET)); + } + + // Amount doesn't cover a single payment + env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags), + Ter(tecINSUFFICIENT_PAYMENT)); + + // Get the balance after these failed transactions take + // fees + auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + // Try to pay a little extra to show that it's _not_ + // taken + auto const transactionAmount = payoffAmount + broker.asset(10); + + // Send a transaction that tries to pay more than the borrowers's + // balance + XRPAmount const badFee{ + baseFee * + (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment / + kLoanPaymentsPerFeeIncrement + + 1)}; + env(pay(borrower, + loanKeylet.key, + STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2}, + txFlags), + Fee(badFee), + Ter(tecINSUFFICIENT_FUNDS)); + + XRPAmount const goodFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; + env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), Fee(goodFee)); + + env.close(); + + // log << env.meta()->getJson() << std::endl; + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = badFee + goodFee; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + state.totalValue = 0; + state.managementFeeOutstanding = 0; + state.previousPaymentDate = + state.nextPaymentDate + (state.paymentInterval * (numPayments - 1)); + state.nextPaymentDate = 0; + verifyLoanStatus(state); + + verifyLoanStatus.checkPayment( + state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); + }; + + auto fullPayment = [&](std::uint32_t baseFlag) { + return [&, baseFlag]( + Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + // Full payoff amount will consist of + // 1. principal outstanding (1000) + // 2. accrued interest (at 12%) + // 3. prepayment penalty (closeInterest at 3.6%) + // 4. close payment fee (4) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; + BEAST_EXPECT( + periodicRate == Number(2283105022831050228ULL, -24, Number::Normalized{})); + STAmount const principalOutstanding{broker.asset, state.principalOutstanding}; + STAmount const accruedInterest{ + broker.asset, state.principalOutstanding * periodicRate * loanAge / interval}; + BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19))); + STAmount const prepaymentPenalty{ + broker.asset, state.principalOutstanding * Number(36, -3)}; + BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); + STAmount const closePaymentFee = broker.asset(4); + auto const payoffAmount = roundToScale( + principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee, + state.loanScale); + BEAST_EXPECT( + payoffAmount == + roundToAsset( + broker.asset, + broker.asset(Number(1040000114155251, -12)).number(), + state.loanScale)); + + // The terms of this loan actually make the early payoff + // more expensive than just making payments + BEAST_EXPECT( + payoffAmount > + state.paymentRemaining * (state.periodicPayment + broker.asset(2).value())); + + singlePayment( + loanKeylet, + verifyLoanStatus, + state, + payoffAmount, + 1, + baseFlag, + tfLoanFullPayment); + }; + }; + + auto combineAllPayments = [&](std::uint32_t baseFlag) { + return + [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + env.close(); + + BEAST_EXPECT( + STAmount(broker.asset, state.periodicPayment) == + broker.asset(Number(8333457002039338267, -17))); + + // Make all the payments in one transaction + // service fee is 2 + auto const startingPayments = state.paymentRemaining; + STAmount const payoffAmount = [&]() { + NumberRoundModeGuard const mg(Number::RoundingMode::Upward); + auto const rawPayoff = + startingPayments * (state.periodicPayment + broker.asset(2).value()); + STAmount payoffAmount{broker.asset, rawPayoff}; + BEAST_EXPECTS( + payoffAmount == broker.asset(Number(1024014840244721, -12)), + to_string(payoffAmount)); + BEAST_EXPECT(payoffAmount > state.principalOutstanding); + + payoffAmount = roundToScale(payoffAmount, state.loanScale); + + return payoffAmount; + }(); + + auto const totalPayoffValue = + state.totalValue + startingPayments * broker.asset(2).value(); + STAmount const totalPayoffAmount{broker.asset, totalPayoffValue}; + + BEAST_EXPECTS( + totalPayoffAmount == payoffAmount, + "Payoff amount: " + to_string(payoffAmount) + + ". Total Value: " + to_string(totalPayoffAmount)); + + singlePayment( + loanKeylet, + verifyLoanStatus, + state, + payoffAmount, + state.paymentRemaining, + baseFlag, + 0); + }; + }; + + // There are a lot of fields that can be set on a loan, but most + // of them only affect the "math" when a payment is made. The + // only one that really affects behavior is the + // `tfLoanOverpayment` flag. + lifecycle( + caseLabel, + "Loan overpayment allowed - Impair and Default", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultImmediately(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Impair and Default", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultImmediately(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Default without Impair", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + defaultImmediately(lsfLoanOverpayment, false)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Default without Impair", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + defaultImmediately(0, false)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Pay off immediately", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + fullPayment(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Pay off immediately", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + fullPayment(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Combine all payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + combineAllPayments(0)); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Combine all payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + combineAllPayments(lsfLoanOverpayment)); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Make payments", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + // Draw and make multiple payments + auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == 0); + env.close(); + + verifyLoanStatus(state); + + env.close(state.startDate + 20s); + auto const loanAge = (env.now() - state.startDate).count(); + BEAST_EXPECT(loanAge == 30); + + // Periodic payment amount will consist of + // 1. principal outstanding (1000) + // 2. interest interest rate (at 12%) + // 3. payment interval (600s) + // 4. loan service fee (2) + // Calculate these values without the helper functions + // to verify they're working correctly The numbers in + // the below BEAST_EXPECTs may not hold across assets. + Number const interval = state.paymentInterval; + auto const periodicRate = interval * Number(12, -2) / kSecondsInYear; + BEAST_EXPECT( + periodicRate == Number(2283105022831050228, -24, Number::Normalized{})); + STAmount const roundedPeriodicPayment{ + broker.asset, + roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)}; + + testcase << currencyLabel << " Payment components: " + << "Payments remaining, rawInterest, rawPrincipal, " + "rawMFee, trackedValueDelta, trackedPrincipalDelta, " + "trackedInterestDelta, trackedMgmtFeeDelta, special"; + + auto const serviceFee = broker.asset(2); + + BEAST_EXPECT( + roundedPeriodicPayment == + roundToScale( + broker.asset( + Number(8333457002039338267, -17), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + // 83334570.01162141 + // Include the service fee + STAmount const totalDue = roundToScale( + roundedPeriodicPayment + serviceFee, + state.loanScale, + Number::RoundingMode::Upward); + // Only check the first payment since the rounding + // may drift as payments are made + BEAST_EXPECT( + totalDue == + roundToScale( + broker.asset( + Number(8533457002039338267, -17), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + + { + auto const raw = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + auto const rounded = constructLoanState( + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding); + testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining + << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", " + << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", " + << rounded.principalOutstanding << ", " << rounded.interestDue << ", " + << rounded.managementFeeDue; + } + + // Try to pay a little extra to show that it's _not_ + // taken + STAmount const transactionAmount = + STAmount{broker.asset, totalDue} + broker.asset(10); + // Only check the first payment since the rounding + // may drift as payments are made + BEAST_EXPECT( + transactionAmount == + roundToScale( + broker.asset(Number(9533457002039400, -14), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + + auto const initialState = state; + xrpl::detail::PaymentComponents totalPaid{ + .trackedValueDelta = 0, + .trackedPrincipalDelta = 0, + .trackedManagementFeeDelta = 0}; + Number totalInterestPaid = 0; + std::size_t totalPaymentsMade = 0; + + xrpl::LoanState currentTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + while (state.paymentRemaining > 0) + { + // Compute the expected principal amount + auto const paymentComponents = xrpl::detail::computePaymentComponents( + env.current()->rules(), + broker.asset.raw(), + state.loanScale, + state.totalValue, + state.principalOutstanding, + state.managementFeeOutstanding, + state.periodicPayment, + periodicRate, + state.paymentRemaining, + broker.params.managementFeeRate); + + BEAST_EXPECTS( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedValueDelta <= roundedPeriodicPayment, + "Delta: " + to_string(paymentComponents.trackedValueDelta) + + ", periodic payment: " + to_string(roundedPeriodicPayment)); + + xrpl::LoanState const nextTrueState = computeTheoreticalLoanState( + env.current()->rules(), + state.periodicPayment, + periodicRate, + state.paymentRemaining - 1, + broker.params.managementFeeRate); + xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState; + + testcase << currencyLabel << " Payment components: " << state.paymentRemaining + << ", " << deltas.interest << ", " << deltas.principal << ", " + << deltas.managementFee << ", " << paymentComponents.trackedValueDelta + << ", " << paymentComponents.trackedPrincipalDelta << ", " + << paymentComponents.trackedInterestPart() << ", " + << paymentComponents.trackedManagementFeeDelta << ", " + << [&]() -> char const* { + if (paymentComponents.specialCase == + ::xrpl::detail::PaymentSpecialCase::Final) + return "final"; + if (paymentComponents.specialCase == + ::xrpl::detail::PaymentSpecialCase::Extra) + return "extra"; + return "none"; + }(); + + auto const totalDueAmount = STAmount{ + broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()}; + + // Due to the rounding algorithms to keep the interest and + // principal in sync with "true" values, the computed amount + // may be a little less than the rounded fixed payment + // amount. For integral types, the difference should be < 3 + // (1 unit for each of the interest and management fee). For + // IOUs, the difference should be after the 8th digit. + Number const diff = totalDue - totalDueAmount; + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + diff == beast::kZero || + (diff > beast::kZero && + ((broker.asset.integral() && (static_cast(diff) < 3)) || + (state.loanScale - diff.exponent() > 13)))); + + BEAST_EXPECT( + paymentComponents.trackedValueDelta == + paymentComponents.trackedPrincipalDelta + + paymentComponents.trackedInterestPart() + + paymentComponents.trackedManagementFeeDelta); + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedValueDelta <= roundedPeriodicPayment); + + BEAST_EXPECT( + state.paymentRemaining < 12 || + roundToAsset( + broker.asset, + deltas.principal, + state.loanScale, + Number::RoundingMode::Upward) == + roundToScale( + broker.asset( + Number(8333228691531218890, -17), Number::RoundingMode::Upward), + state.loanScale, + Number::RoundingMode::Upward)); + BEAST_EXPECT( + paymentComponents.trackedPrincipalDelta >= beast::kZero && + paymentComponents.trackedPrincipalDelta <= state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final || + paymentComponents.trackedPrincipalDelta == state.principalOutstanding); + BEAST_EXPECT( + paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final || + (state.periodicPayment.exponent() - + (deltas.principal + deltas.interest + deltas.managementFee - + state.periodicPayment) + .exponent()) > 14); + + auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset); + + if (canImpairLoan(env, broker, state)) + { + // Making a payment will unimpair the loan + env(manage(lender, loanKeylet.key, tfLoanImpair)); + } + + env.close(); + + // Make the payment + env(pay(borrower, loanKeylet.key, transactionAmount)); + + env.close(); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.native()) + { + adjustment = env.current()->fees().base; + } + + // Check the result + verifyLoanStatus.checkPayment( + state.loanScale, + borrower, + borrowerBalanceBeforePayment, + totalDueAmount, + adjustment); + + --state.paymentRemaining; + state.previousPaymentDate = state.nextPaymentDate; + if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final) + { + state.paymentRemaining = 0; + state.nextPaymentDate = 0; + } + else + { + state.nextPaymentDate += state.paymentInterval; + } + state.principalOutstanding -= paymentComponents.trackedPrincipalDelta; + state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta; + state.totalValue -= paymentComponents.trackedValueDelta; + + verifyLoanStatus(state); + + totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta; + totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta; + totalPaid.trackedManagementFeeDelta += + paymentComponents.trackedManagementFeeDelta; + totalInterestPaid += paymentComponents.trackedInterestPart(); + ++totalPaymentsMade; + + currentTrueState = nextTrueState; + } + + // Loan is paid off + BEAST_EXPECT(state.paymentRemaining == 0); + BEAST_EXPECT(state.principalOutstanding == 0); + + // Make sure all the payments add up + BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue); + BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding); + BEAST_EXPECT( + totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding); + // This is almost a tautology given the previous checks, but + // check it anyway for completeness. + BEAST_EXPECT( + totalInterestPaid == + initialState.totalValue - + (initialState.principalOutstanding + + initialState.managementFeeOutstanding)); + BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining); + + // Can't impair or default a paid off loan + env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION)); + env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION)); + }); + +#if LOAN_TODO + // TODO + + /* + LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also + borrower) tries to do the payment. Here's the scenario: Create a XRP + loan with loan broker as borrower, loan origination fee and loan service + fee. Loan broker makes the first payment with periodic payment and loan + service fee. + */ + + auto time = [&](std::string label, std::function timed) { + if (!BEAST_EXPECT(timed)) + return; + + using clock_type = std::chrono::steady_clock; + using duration_type = std::chrono::milliseconds; + + auto const start = clock_type::now(); + timed(); + auto const duration = + std::chrono::duration_cast(clock_type::now() - start); + + log << label << " took " << duration.count() << "ms" << std::endl; + + return duration; + }; + + lifecycle( + caseLabel, + "timing", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { + // Estimate optimal values for kLoanPaymentsPerFeeIncrement and + // kLoanMaximumPaymentsPerTransaction. + using namespace loan; + + auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet); + auto const serviceFee = broker.asset(2).value(); + + STAmount const totalDue{ + broker.asset, + roundPeriodicPayment( + broker.asset, state.periodicPayment + serviceFee, state.loanScale)}; + + // Make a single payment + time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); }); + env.close(); + + // Make all but the final payment + auto const numPayments = (state.paymentRemaining - 2); + STAmount const bigPayment{broker.asset, totalDue * numPayments}; + XRPAmount const bigFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)}; + time("ten payments", [&]() { + env(pay(borrower, loanKeylet.key, bigPayment), Fee(bigFee)); + }); + env.close(); + + time("final payment", [&]() { + // Make the final payment + env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1})); + }); + env.close(); + }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Explicit overpayment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment prohibited - Late payment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Late payment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + + lifecycle( + caseLabel, + "Loan overpayment allowed - Late payment and overpayment", + env, + loanAmount, + interestExponent, + lender, + borrower, + evan, + broker, + pseudoAcct, + tfLoanOverpayment, + [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; }); + +#endif + } +}; + +} // namespace xrpl::test diff --git a/src/test/app/lending/LoanValidation_test.cpp b/src/test/app/lending/LoanValidation_test.cpp new file mode 100644 index 0000000000..d2985c4c30 --- /dev/null +++ b/src/test/app/lending/LoanValidation_test.cpp @@ -0,0 +1,558 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::test { + +class LoanValidation_test : public LoanTestBase +{ +private: + void + testDisabled() + { + testcase("Disabled"); + // Lending Protocol depends on Single Asset Vault (SAV). Test + // combinations of the two amendments. + // Single Asset Vault depends on MPTokensV1, but don't test every combo + // of that. + using namespace jtx; + auto failAll = [this](FeatureBitset features) { + Env env(*this, features); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + + auto const keylet = keylet::loanBroker(alice, env.seq(alice)); + + using namespace std::chrono_literals; + using namespace loan; + + // counter party signature is optional on LoanSet. Confirm that by + // sending transaction without one. + auto setTx = env.jt(set(alice, keylet.key, Number(10000)), Ter(temDISABLED)); + env(setTx); + + // All loan transactions are disabled. + // 1. LoanSet + setTx = env.jt(setTx, Sig(sfCounterpartySignature, bob), Ter(temDISABLED)); + 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)); + // Other Loan transactions are disabled, too. + // 2. LoanDelete + env(del(alice, loanKeylet.key), Ter(temDISABLED)); + // 3. LoanManage + env(manage(alice, loanKeylet.key, tfLoanImpair), Ter(temDISABLED)); + // 4. LoanPay + env(pay(alice, loanKeylet.key, XRP(500)), Ter(temDISABLED)); + }; + failAll(all_ - featureMPTokensV1); + failAll(all_ - featureSingleAssetVault - featureLendingProtocol); + failAll(all_ - featureSingleAssetVault); + failAll(all_ - featureLendingProtocol); + } + + void + testInvalidLoanSet() + { + testcase("Invalid LoanSet"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Account const sponsor{"sponsor"}; + auto const iou = issuer["IOU"]; + + auto testWrapper = [&](auto&& test) { + Env env(*this); + env.fund(XRP(1'000), lender, issuer, borrower, sponsor); + env(trust(lender, iou(10'000'000))); + env(pay(issuer, lender, iou(5'000'000))); + BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const debtMaximumRequest = brokerInfo.asset(1'000).value(); + test(env, brokerInfo, loanSetFee, debtMaximumRequest); + }; + + // preflight: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::Fee const& loanSetFee, + Number const& debtMaximumRequest) { + for (auto const sponsorFlags : {spfSponsorReserve, spfSponsorReserve | spfSponsorFee}) + { + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + sponsor::As(sponsor, sponsorFlags), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(temINVALID_FLAG)); + } + + // first temBAD_SIGNER: TODO + // invalid grace period + { + // zero grace period + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kGracePeriod(0), + loanSetFee, + Ter(temINVALID)); + + // grace period less than default minimum + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kGracePeriod(LoanSet::kDefaultGracePeriod - 1), + loanSetFee, + Ter(temINVALID)); + + // grace period greater than payment interval + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + kPaymentInterval(120), + kGracePeriod(121), + loanSetFee, + Ter(temINVALID)); + } + // empty/zero broker ID + { + auto jv = set(borrower, uint256{}, debtMaximumRequest); + + auto testZeroBrokerID = [&](std::string const& id, std::uint32_t flags = 0) { + // empty broker ID + jv[sfLoanBrokerID] = id; + env(jv, + Sig(sfCounterpartySignature, lender), + loanSetFee, + Txflags(flags), + Ter(temINVALID)); + }; + // empty broker ID + testZeroBrokerID(std::string("")); + // zero broker ID + // needs a flag to distinguish the parsed STTx from the prior + // test + testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig); + } + + // preflightCheckSigningKey() failure: + // can it happen? the signature is checked before transactor + // executes + + JTx const tx = env.jt( + set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee); + STTx local = *(tx.stx); + auto counterpartySig = local.getFieldObject(sfCounterpartySignature); + auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey); + badPubKey[20] ^= 0xAA; + counterpartySig.setFieldVL(sfSigningPubKey, badPubKey); + local.setFieldObject(sfCounterpartySignature, counterpartySig); + json::Value jvResult; + jvResult[jss::tx_blob] = strHex(local.getSerializer().slice()); + auto res = env.rpc("json", "submit", to_string(jvResult))["result"]; + BEAST_EXPECT( + res[jss::error] == "invalidTransaction" && + res[jss::error_exception] == + "fails local checks: Counterparty: Invalid signature."); + }); + + // preclaim: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::Fee const& loanSetFee, + Number const& debtMaximumRequest) { + // canAddHoldingFailure (IOU only, if MPT doesn't have + // MPTCanTransfer set, then can't create Vault/LoanBroker, + // and LoanSet will fail with different error + env(fclear(issuer, asfDefaultRipple)); + env.close(); + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(terNO_RIPPLE)); + }); + + // doApply: + testWrapper([&](Env& env, + BrokerInfo const& brokerInfo, + jtx::Fee const& loanSetFee, + Number const& debtMaximumRequest) { + auto const amt = + env.balance(borrower) - accountReserve(*env.current(), borrower.id(), env.journal); + env(pay(borrower, issuer, amt)); + + // tecINSUFFICIENT_RESERVE + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecINSUFFICIENT_RESERVE)); + + // addEmptyHolding failure + env(pay(issuer, borrower, amt)); + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + Ter(tecFROZEN)); + }); + } + + void + testInvalidLoanDelete() + { + testcase("Invalid LoanDelete"); + using namespace jtx; + using namespace loan; + + // preflight: temINVALID, LoanID == zero + { + Account const alice{"alice"}; + Env env(*this); + env.fund(XRP(1'000), alice); + env.close(); + env(del(alice, beast::kZero), Ter(temINVALID)); + } + } + + void + testInvalidLoanManage() + { + testcase("Invalid LoanManage"); + using namespace jtx; + using namespace loan; + + // preflight: temINVALID, LoanID == zero + { + Account const alice{"alice"}; + Env env(*this); + env.fund(XRP(1'000), alice); + env.close(); + env(manage(alice, beast::kZero, tfLoanDefault), Ter(temINVALID)); + } + } + + void + testInvalidLoanPay() + { + testcase("Invalid LoanPay"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + auto const iou = issuer["IOU"]; + + // preclaim + Env env(*this); + env.fund(XRP(1'000), lender, issuer, borrower); + env(trust(lender, iou(10'000'000))); + env(pay(issuer, lender, iou(5'000'000))); + BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); + + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee); + + env.close(); + + std::uint32_t const loanSequence = 1; + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence); + + env(fset(issuer, asfGlobalFreeze)); + env.close(); + + // preclaim: tecFROZEN + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); + env.close(); + + env(fclear(issuer, asfGlobalFreeze)); + env.close(); + + auto const pseudoBroker = [&]() -> std::optional { + if (auto brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID)); + BEAST_EXPECT(brokerSle)) + { + return Account{"pseudo", brokerSle->at(sfAccount)}; + } + + return std::nullopt; + }(); + if (!pseudoBroker) + return; + + // Lender and pseudoaccount must both be frozen + env(trust(issuer, lender["IOU"](1'000), lender, tfSetFreeze | tfSetDeepFreeze)); + env(trust( + issuer, (*pseudoBroker)["IOU"](1'000), *pseudoBroker, tfSetFreeze | tfSetDeepFreeze)); + env.close(); + + // preclaim: tecFROZEN due to deep frozen + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN)); + env.close(); + + // Only one needs to be unfrozen + env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze)); + env.close(); + + // The payment is late by this point + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecEXPIRED)); + env.close(); + env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment)); + env.close(); + + // preclaim: tecKILLED + // note that tecKILLED in loanMakePayment() + // doesn't happen because of the preclaim check. + env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecKILLED)); + } + + void + testRequireAuth() + { + testcase("Require Auth - Implicit Pseudo-account authorization"); + using namespace jtx; + using namespace loan; + Account const lender{"lender"}; + Account const issuer{"issuer"}; + Account const borrower{"borrower"}; + Env env(*this); + + env.fund(XRP(100'000), issuer, lender, borrower); + env.close(); + + auto asset = MPTTester({ + .env = env, + .issuer = issuer, + .holders = {lender, borrower}, + .flags = kMptDexFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock, + .authHolder = true, + }); + + env(pay(issuer, lender, asset(5'000'000))); + BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)}; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value(); + + auto forUnauthAuth = [&](auto&& doTx) { + for (auto const flag : {tfMPTUnauthorize, 0u}) + { + asset.authorize({.account = issuer, .holder = borrower, .flags = flag}); + env.close(); + doTx(flag == 0); + env.close(); + } + }; + + // Can't create a loan if the borrower is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); + env(set(borrower, brokerInfo.brokerID, debtMaximumRequest), + Sig(sfCounterpartySignature, lender), + loanSetFee, + err); + }); + + static constexpr std::uint32_t kLoanSequence = 1; + auto const loanKeylet = keylet::loan(brokerInfo.brokerID, kLoanSequence); + + // Can't loan pay if the borrower is not authorized + forUnauthAuth([&](bool authorized) { + auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS); + env(pay(borrower, loanKeylet.key, debtMaximumRequest), err); + }); + } + + void + testLimitExceeded() + { + testcase("RIPD-4125 - overpayment"); + + using namespace jtx; + + Account const issuer("issuer"); + Account const lender("lender"); + Account const borrower("borrower"); + + BrokerParameters const brokerParams{ + .vaultDeposit = 100'000, + .debtMax = 0, + .coverRateMin = TenthBips32{0}, + .managementFeeRate = TenthBips16{0}, + .coverRateLiquidation = TenthBips32{0}}; + LoanParameters const loanParams{ + .account = lender, + .counter = borrower, + .principalRequest = Number{200000, -6}, + .interest = TenthBips32{50000}, + .payTotal = 3, + .payInterval = 200, + .gracePd = 60, + .flags = tfLoanOverpayment, + }; + + auto const assetType = AssetType::XRP; + + Env env(*this, makeConfig(), all_, nullptr, beast::Severity::Warning); + + auto loanResult = + createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower); + + if (BEAST_EXPECT(loanResult); !loanResult.has_value()) + return; + + auto broker = std::get(*loanResult); + auto loanKeylet = std::get(*loanResult); + auto pseudoAcct = std::get(*loanResult); + + VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); + + auto const state = getCurrentState(env, broker, loanKeylet); + + env(loan::pay( + borrower, + loanKeylet.key, + STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1}, + tfLoanOverpayment)); + env.close(); + + PaymentParameters const paymentParams{ + .showStepBalances = false, + .validateBalances = true, + }; + + makeLoanPayments( + env, + broker, + loanParams, + loanKeylet, + verifyLoanStatus, + issuer, + lender, + borrower, + paymentParams); + } + + void + testWrongMaxDebtBehavior(FeatureBitset features) + { + // From FIND-003 + testcase << "Wrong Max Debt Behavior"; + + using namespace jtx; + using namespace std::chrono_literals; + Env env(*this, features); + + Account const issuer{"issuer"}; + Account const lender{"lender"}; + + BrokerParameters const brokerParams{.debtMax = 0}; + env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender)); + env.close(); + + PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; + + BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)}; + + if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0); + } + + using namespace loan; + + auto const loanSetFee = Fee(env.current()->fees().base * 2); + Number const principalRequest{1, 3}; + + auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee)); + + json::Value counterpartyJson{json::ValueType::Object}; + counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; + counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey]; + if (!BEAST_EXPECT(!createJson.isMember(jss::Signers))) + counterpartyJson[sfSigners] = createJson[sfSigners]; + + createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)); + env(createJson); + + env.close(); + } + + void + runAmendmentIndependent() + { + testDisabled(); + testInvalidLoanSet(); + testInvalidLoanDelete(); + testInvalidLoanManage(); + testInvalidLoanPay(); + testRequireAuth(); + testLimitExceeded(); + } + + // Tests run under each entry in amendmentCombinations(). + void + runAmendmentSensitive(FeatureBitset features) + { + testWrongMaxDebtBehavior(features); + } + +public: + void + run() override + { + runAmendmentIndependent(); + for (auto const& features : jtx::amendmentCombinations( + {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_)) + runAmendmentSensitive(features); + } +}; + +BEAST_DEFINE_TESTSUITE(LoanValidation, tx, xrpl); + +} // namespace xrpl::test diff --git a/src/test/app/lending/Loan_test.cpp b/src/test/app/lending/Loan_test.cpp new file mode 100644 index 0000000000..717387665e --- /dev/null +++ b/src/test/app/lending/Loan_test.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include +#include +#include + +namespace xrpl::test { + +/** + * Aggregator: running this suite ("Loan") reruns every topical Loan/Lending + * suite in one invocation. Each member suite below remains independently + * runnable under its own name. Declared manual so an unfiltered full test + * run doesn't execute every case twice. + */ +class Loan_test : public beast::unit_test::Suite +{ + void + run() override + { + static constexpr std::array kMembers{ + "LendingHelpers", + "LoanBroker", + "LoanCashBasis", + "LoanCoverFreezeAuth", + "LoanInvariants", + "LoanLifecycle", + "LoanMisc", + "LoanPay", + "LoanRounding", + "LoanSecurity", + "LoanSet", + "LoanValidation", + }; + + for (auto const& info : beast::unit_test::globalSuites()) + { + if (std::ranges::find(kMembers, info.name()) != kMembers.end()) + info.run(runner()); + } + } +}; + +BEAST_DEFINE_TESTSUITE_MANUAL(Loan, tx, xrpl); + +} // namespace xrpl::test From a75488e5ff1420b6312db2edfce2a3691ccf8b6c Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 4 Aug 2026 17:17:41 +0100 Subject: [PATCH 72/86] docs: Add a fix for `command not found: nix` on macOS (#7951) --- docs/build/nix_troubleshooting.md | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/build/nix_troubleshooting.md b/docs/build/nix_troubleshooting.md index ae5cb8059a..fa766c0ee9 100644 --- a/docs/build/nix_troubleshooting.md +++ b/docs/build/nix_troubleshooting.md @@ -3,6 +3,78 @@ Common issues encountered when using the [Nix development shell](./nix.md), and how to resolve them. +## `command not found: nix` after a macOS update + +If a shell suddenly can't find `nix` at all: + +``` +$ nix develop +zsh: command not found: nix +``` + +then Nix is almost certainly still installed — only the shell hook that puts it +on your `PATH` is gone. Confirm that first: + +```bash +ls -l /nix/var/nix/profiles/default/bin/nix +``` + +If that exists, the installation is fine and this is purely a `PATH` problem. + +### Why it happens + +The installer does not touch your dotfiles. Instead it sources a setup script +from the Nix store by editing **system-wide** rc files: + +| Shell | File the installer edits | +| ----- | ------------------------------------- | +| bash | `/etc/bashrc`, `/etc/bash.bashrc` | +| zsh | `/etc/zshrc` | +| fish | `$__fish_sysconf_dir/conf.d/nix.fish` | + +macOS manages `/etc/zshrc`, so an OS update can replace it with the vendor copy +and silently drop the Nix block. `/etc/bashrc` and the fish file usually survive, +which is why the breakage often shows up in zsh only. You can verify this by +diffing against the backup the installer left behind: + +```bash +diff /etc/zshrc /etc/zshrc.backup-before-nix +``` + +If they are identical, the Nix snippet was wiped. This is upstream issue +[NixOS/nix#3616](https://github.com/NixOS/nix/issues/3616). + +### Fix + +To unblock the current shell: + +```bash +. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh +``` + +For a permanent fix, add the snippet to your **user** rc file rather than +restoring `/etc/zshrc` — user dotfiles are not clobbered by OS updates: + +```bash +cat >>~/.zshrc <<'EOF' + +# Nix +if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then + . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' +fi +# End Nix +EOF +``` + +The scripts guard against double-sourcing via `__ETC_PROFILE_NIX_SOURCED`, so +this is safe even if a system-wide hook is later restored. + +> [!NOTE] +> `/etc/zshrc` and `~/.zshrc` are only read by **interactive** zsh. If the +> snippet is present but `zsh -c '…'`, a script, or an IDE terminal still can't +> find `nix`, that shell is non-interactive — put the snippet in `~/.zshenv` +> instead. + ## Git worktrees If `nix develop` fails with an error like: 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 73/86] 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 74/86] 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 41d6bb5f736459d31bc3207c7ab77f720aaaed63 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Wed, 5 Aug 2026 00:27:23 +0100 Subject: [PATCH 75/86] build: Fix build on macOS 15 and Nix environment (#7953) --- BUILD.md | 2 ++ CMakeLists.txt | 19 +++++++++++++++++++ conan.lock | 3 ++- conan/profiles/default | 20 ++++++++++++++++++++ conanfile.py | 2 ++ sanitizers/suppressions/ubsan.supp | 4 ++++ src/libxrpl/json/json_reader.cpp | 12 ++++++++++-- 7 files changed, 59 insertions(+), 3 deletions(-) diff --git a/BUILD.md b/BUILD.md index a15c94edc9..edc52fe3b7 100644 --- a/BUILD.md +++ b/BUILD.md @@ -42,6 +42,8 @@ Our Linux CI tooling is distro-independent and uses a Nix-based environment, so ### macOS Many `xrpld` engineers use macOS for development. +The minimum supported version is macOS 15 (Sequoia). +CI testing is done in macOS 26 (Tahoe), but the build defaults `CMAKE_OSX_DEPLOYMENT_TARGET` to 15. ### Windows diff --git a/CMakeLists.txt b/CMakeLists.txt index f2e8fb3ae5..b7e1c0cad0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,23 @@ if(DEFINED CMAKE_MODULE_PATH) endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +# Must be set before project() because project() consumes it when configuring the compiler and SDK. +# A user-provided -DCMAKE_OSX_DEPLOYMENT_TARGET still takes precedence. +# +# CMAKE_SYSTEM_NAME can't be used before project(), so CMAKE_HOST_SYSTEM_NAME is used instead. +# +# When CMAKE_OSX_DEPLOYMENT_TARGET is bumped to >=26.0, FastFloat dependency won't be needed anymore +if( + CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" + AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET +) + set(CMAKE_OSX_DEPLOYMENT_TARGET + "15.0" + CACHE STRING + "Minimum macOS deployment version" + ) +endif() + project(xrpl) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 23) @@ -87,6 +104,7 @@ include(deps/Boost) add_subdirectory(external/antithesis-sdk) find_package(date REQUIRED) find_package(ed25519 REQUIRED) +find_package(FastFloat REQUIRED) find_package(gRPC REQUIRED) find_package(LibArchive REQUIRED) find_package(lz4 REQUIRED) @@ -102,6 +120,7 @@ target_link_libraries( xrpl_libs INTERFACE ed25519::ed25519 + FastFloat::fast_float lz4::lz4 mpt-crypto::mpt-crypto OpenSSL::Crypto diff --git a/conan.lock b/conan.lock index c6a4070c77..0e1461ba9c 100644 --- a/conan.lock +++ b/conan.lock @@ -20,6 +20,7 @@ "jemalloc/5.3.1#1fc58d55316041f10fbc1e8a2eae632a%1776700028.228", "gtest/1.17.0#5224b3b3ff3b4ce1133cbdd27d53ee7d%1782392402.791979", "grpc/1.81.1#f729f6d75992d20f9c72828e9142d62f%1783945160.094135", + "fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1782494504.298", "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562", "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492", "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654", @@ -34,7 +35,7 @@ "protobuf/6.33.5#ff253ead763bd8d9904a52979cd21e81%1782392410.233933", "nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1782395690.33162", "msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649", - "m4/1.4.19#34c4bbc3eeebe98ca6edf2f52d602e7d%1777282960.259", + "m4/1.4.19#1727f439cf74e83826ec96d0b4904eee%1784541921.659", "cmake/4.3.3#840cf00ea09777e05c2050a50a82c722%1782392418.696091", "b2/5.4.2#ffd6084a119587e70f11cd45d1a386e2%1782392402.624226", "automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56", diff --git a/conan/profiles/default b/conan/profiles/default index 6534f8092b..f2d93213ac 100644 --- a/conan/profiles/default +++ b/conan/profiles/default @@ -5,6 +5,13 @@ {% 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 #} +{# dependency objects target a newer macOS than the binary and the linker warns. #} +{# Keep at or below CMAKE_OSX_DEPLOYMENT_TARGET in CMakeLists.txt. #} +{% set min_macos_version = "15.0" %} +{% endif %} [settings] os={{ os }} @@ -18,6 +25,9 @@ compiler.runtime=static {% else %} compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }} {% endif %} +{% if os == "Macos" %} +os.version={{ min_macos_version }} +{% endif %} [conf] {# The Boost recipe builds with b2, which doesn't use Conan's toolchain files. #} @@ -41,3 +51,13 @@ tools.build:compiler_executables={'c':'{{ cc_exe }}','cpp':'{{ cxx_exe }}'} {# More info: https://docs.conan.io/2/reference/extensions/binary_compatibility.html #} user.package:cppstd_version=23 tools.info.package_id:confs+=["user.package:cppstd_version"] + +{% if os == "Macos" %} +[buildenv] +{# os.version adds -mmacosx-version-min to compiler command lines, #} +{# but Boost.Context's b2 assembly (.S) rule ignores it, #} +{# so those objects keep the host SDK version and still warn at link time. #} +{# clang's assembler honors this env var regardless, pinning them. #} +{# Scoped to boost/* since it is the only gap. #} +boost/*:MACOSX_DEPLOYMENT_TARGET={{ min_macos_version }} +{% endif %} diff --git a/conanfile.py b/conanfile.py index f883761f0e..b9fa513d67 100644 --- a/conanfile.py +++ b/conanfile.py @@ -29,6 +29,7 @@ class Xrpl(ConanFile): requires = [ "ed25519/2015.03", + "fast_float/8.2.10", "grpc/1.81.1", "libarchive/3.8.7", "nudb/2.0.9", @@ -211,6 +212,7 @@ class Xrpl(ConanFile): "boost::thread", "date::date", "ed25519::ed25519", + "fast_float::fast_float", "grpc::grpc++", "libarchive::libarchive", "lz4::lz4", diff --git a/sanitizers/suppressions/ubsan.supp b/sanitizers/suppressions/ubsan.supp index cb93a617aa..56f2c77204 100644 --- a/sanitizers/suppressions/ubsan.supp +++ b/sanitizers/suppressions/ubsan.supp @@ -102,6 +102,10 @@ undefined:nudb # Snappy compression library intentional overflows unsigned-integer-overflow:snappy.cc +# fast_float parses floats with a SWAR trick (parse_eight_digits_unrolled) that +# multiplies eight packed digits modulo 2^64; the wraparound is by design. +unsigned-integer-overflow:fast_float + # Abseil intentional overflows in hashing, RNG and time arithmetic. # Matched at library scope (like boost above): the wraparound is by design # across many absl files (hash mixing, raw_hash_set probing, duration math, diff --git a/src/libxrpl/json/json_reader.cpp b/src/libxrpl/json/json_reader.cpp index f9134e6629..8598f94491 100644 --- a/src/libxrpl/json/json_reader.cpp +++ b/src/libxrpl/json/json_reader.cpp @@ -3,9 +3,11 @@ #include #include +#include // IWYU pragma: keep +#include + #include #include -#include #include #include #include @@ -605,8 +607,14 @@ Reader::decodeNumber(Token& token) bool Reader::decodeDouble(Token& token) { + // Sanity check to avoid buffer overflow exploits. + if (token.end < token.start) + { + return addError("Unable to parse token length", token); + } + double value = 0; - auto const [ptr, ec] = std::from_chars(token.start, token.end, value); + auto const [ptr, ec] = fast_float::from_chars(token.start, token.end, value); // Reject anything from_chars could not turn into a finite double: // - ec != std::errc{}: no valid conversion, or an out-of-range magnitude From 8d7524f03b6298fb0d366d23036688b23042defd Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 5 Aug 2026 13:54:44 -0400 Subject: [PATCH 76/86] fix: Use consistent endianness serializing MPT STIssue sequence (#7429) Co-authored-by: Ed Hennis Co-authored-by: David Fuelling --- src/libxrpl/protocol/STIssue.cpp | 10 ++++++ src/test/protocol/STIssue_test.cpp | 54 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/libxrpl/protocol/STIssue.cpp b/src/libxrpl/protocol/STIssue.cpp index 10403d2c50..ba32c1214c 100644 --- a/src/libxrpl/protocol/STIssue.cpp +++ b/src/libxrpl/protocol/STIssue.cpp @@ -11,6 +11,8 @@ #include #include +#include + #include #include #include @@ -45,6 +47,10 @@ STIssue::STIssue(SerialIter& sit, SField const& name) : STBase{name} { MPTID mptID; std::uint32_t sequence = sit.get32(); + // MPTID stores the sequence in canonical big-endian bytes. STIssue + // ledger bytes are the legacy LE-host encoding, so convert the + // native get32() value to LE bytes before copying into the MPTID. + sequence = boost::endian::native_to_little(sequence); static_assert(MPTID::size() == sizeof(sequence) + sizeof(currencyOrAccount)); memcpy(mptID.data(), &sequence, sizeof(sequence)); memcpy( @@ -100,6 +106,10 @@ STIssue::add(Serializer& s) const s.addBitString(noAccount()); std::uint32_t sequence = 0; memcpy(&sequence, issue.getMptID().data(), sizeof(sequence)); + // The MPTID bytes are canonical big-endian. Interpret those bytes + // as the legacy LE-host value so add32() writes the preserved + // STIssue wire bytes on every host endian. + sequence = boost::endian::little_to_native(sequence); s.add32(sequence); }); } diff --git a/src/test/protocol/STIssue_test.cpp b/src/test/protocol/STIssue_test.cpp index b7cc944e6b..41517b38f3 100644 --- a/src/test/protocol/STIssue_test.cpp +++ b/src/test/protocol/STIssue_test.cpp @@ -7,17 +7,22 @@ #include #include +#include #include #include #include #include +#include #include +#include #include #include #include #include #include +#include +#include #include namespace xrpl::test { @@ -273,6 +278,54 @@ public: } } + void + testMPTSerialization() + { + testcase("MPT serialization"); + using namespace jtx; + Account const alice{"alice"}; + + // 0x01020304 pins canonical MPTID bytes 01 02 03 04 and + // preserved STIssue wire bytes 04 03 02 01 on BE and LE. + auto const sequences = std::to_array({0x00000001, 0x01020304, 0xa1b2c3d4}); + + for (auto const vector : sequences) + { + MPTID const mptID = makeMptID(vector, alice); + MPTIssue const issue{mptID}; + STIssue const stIssue(sfAsset, Asset{issue}); + + Serializer actual; + stIssue.add(actual); + + // STIssue preserves the existing little-endian validator ledger bytes. + Serializer expected; + expected.addBitString(alice.id()); + expected.addBitString(noAccount()); + { + std::array const bytes{ + static_cast(vector), + static_cast(vector >> 8), + static_cast(vector >> 16), + static_cast(vector >> 24)}; + expected.addRaw(bytes.data(), bytes.size()); + } + + BEAST_EXPECTS(strHex(actual) == strHex(expected), strHex(actual)); + + // Decoding the preserved wire format must recover the canonical MPTID. + SerialIter iter(expected.slice()); + STIssue const decoded(iter, sfAsset); + BEAST_EXPECT(decoded.holds()); + BEAST_EXPECT(decoded.value().get().getMptID() == mptID); + + // A decoded ledger value must serialize back to the same bytes. + Serializer roundTrip; + decoded.add(roundTrip); + BEAST_EXPECTS(strHex(roundTrip) == strHex(expected), strHex(roundTrip)); + } + } + void run() override { @@ -283,6 +336,7 @@ public: testNoAccountIssuer(); testXrpAccountIssuerRpc(); testXrpAccountIssuer(); + testMPTSerialization(); } }; From 5bf24c40456dfe0fa33a3ecf315fa0454ab26424 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 6 Aug 2026 12:04:41 +0100 Subject: [PATCH 77/86] ci: Use a separate benchmark filter (#7919) --- .github/scripts/strategy-matrix/generate.py | 9 +++++++++ .github/scripts/strategy-matrix/linux.json | 3 ++- .../workflows/reusable-build-test-config.yml | 17 +++++++++++++---- .github/workflows/reusable-build-test.yml | 1 + 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index c783f32fb7..3797e5881d 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -33,6 +33,10 @@ def get_cmake_args(build_type: str, extra_args: str) -> str: # Every config must declare 'minimal'. Minimal configs form the reduced matrix # built for pull requests by default; the full matrix adds the rest. Packaging # configs declare it too, but packaging is gated in the workflow, not by it. +# +# Configs may also opt into 'benchmark' to smoke-run the benchmarks. Note that +# the flag applies to every entry a config expands into, so only set it on +# configs that expand to a single combination. @dataclasses.dataclass @@ -43,6 +47,7 @@ class LinuxConfig: build_type: list[str] arch: list[str] minimal: bool + benchmark: bool = False # if true, smoke-run the benchmarks after testing sanitizers: list[str] = dataclasses.field(default_factory=list) suffix: str = "" extra_cmake_args: str = "" @@ -81,6 +86,7 @@ class PlatformConfig: build_type: list[str] minimal: bool build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug) + benchmark: bool = False # if true, smoke-run the benchmarks after testing extra_cmake_args: str = "" def __post_init__(self) -> None: @@ -125,6 +131,7 @@ class MatrixEntry: cmake_args: str cmake_target: str build_only: bool + benchmark: bool build_type: str architecture: Architecture sanitizers: str @@ -193,6 +200,7 @@ def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]: cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args), cmake_target="all", build_only=False, + benchmark=cfg.benchmark, build_type=build_type, architecture=arch_info, sanitizers=sanitizer, @@ -245,6 +253,7 @@ def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry] cmake_args=get_cmake_args(build_type, cfg.extra_cmake_args), cmake_target="install" if is_windows else "all", build_only=cfg.build_only, + benchmark=cfg.benchmark, build_type=build_type, architecture=Architecture(platform=pf.platform, runner=pf.runner), sanitizers="", diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 9510212344..159c76b6c2 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -14,7 +14,8 @@ "compiler": ["clang"], "build_type": ["Release"], "arch": ["amd64"], - "minimal": true + "minimal": true, + "benchmark": true }, { diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 3023f70cdf..e21067cc5f 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -3,6 +3,12 @@ name: Build and test configuration on: workflow_call: inputs: + benchmark: + description: "Whether to smoke-run the benchmarks after testing." + required: false + type: boolean + default: false + build_only: description: 'Whether to only build or to build and test the code ("true", "false").' required: true @@ -328,11 +334,14 @@ jobs: # Smoke-run every benchmark module with a single repetition to confirm the # benchmarks still build and execute. This is a correctness check, not a - # performance measurement, so it is skipped for instrumented builds - # (sanitizers/coverage/voidstar), where it would be slow and meaningless, - # and on Windows, where the `install` target does not build them. + # performance measurement, so there is nothing to gain from repeating it + # across configurations: it is opted into by a single config in the + # strategy matrix (see the 'benchmark' flag in the JSON files), which + # keeps it off instrumented builds (sanitizers/coverage/voidstar), where + # it would be slow and meaningless, off Debug builds, where it is much + # slower, and off Windows, where the `install` target does not build them. - name: Run the benchmarks - if: ${{ !inputs.build_only && runner.os != 'Windows' && env.SANITIZERS_ENABLED == 'false' && env.COVERAGE_ENABLED != 'true' && env.VOIDSTAR_ENABLED != 'true' }} + if: ${{ inputs.benchmark }} working-directory: ${{ env.BUILD_DIR }} run: | rc=0 diff --git a/.github/workflows/reusable-build-test.yml b/.github/workflows/reusable-build-test.yml index 4b64c53521..5368274a16 100644 --- a/.github/workflows/reusable-build-test.yml +++ b/.github/workflows/reusable-build-test.yml @@ -40,6 +40,7 @@ jobs: fail-fast: ${{ github.event_name == 'merge_group' }} matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }} with: + benchmark: ${{ matrix.benchmark }} build_only: ${{ matrix.build_only }} build_type: ${{ matrix.build_type }} ccache_enabled: ${{ inputs.ccache_enabled }} From cb425647a45d12d4e038bbb8cdd41accbdfea607 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 6 Aug 2026 14:25:28 +0100 Subject: [PATCH 78/86] ci: Generate protocol_autogen only once in CI (#7918) --- .github/workflows/on-pr.yml | 7 ++ .github/workflows/on-trigger.yml | 4 + .../workflows/reusable-build-test-config.yml | 32 +------- .github/workflows/reusable-check-autogen.yml | 76 +++++++++++++++++++ BUILD.md | 12 ++- cmake/XrplProtocolAutogen.cmake | 27 +++---- cmake/codegen/CMakeLists.txt | 21 +++++ include/xrpl/protocol_autogen/README.md | 10 +++ 8 files changed, 146 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/reusable-check-autogen.yml create mode 100644 cmake/codegen/CMakeLists.txt diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 1cd97305da..0a4e4b1f49 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -82,6 +82,7 @@ jobs: .github/scripts/strategy-matrix/** .github/workflows/reusable-build-test-config.yml .github/workflows/reusable-build-test.yml + .github/workflows/reusable-check-autogen.yml .github/workflows/reusable-clang-tidy.yml .github/workflows/reusable-package.yml .github/workflows/reusable-strategy-matrix.yml @@ -126,6 +127,11 @@ jobs: outputs: go: ${{ steps.go.outputs.go == 'true' }} + check-autogen: + needs: should-run + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-check-autogen.yml + check-levelization: needs: should-run if: ${{ needs.should-run.outputs.go == 'true' }} @@ -200,6 +206,7 @@ jobs: passed: if: failure() || cancelled() needs: + - check-autogen - check-levelization - check-rename - clang-tidy diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index b8899cec72..73f918d528 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -20,6 +20,7 @@ on: - ".github/scripts/strategy-matrix/**" - ".github/workflows/reusable-build-test-config.yml" - ".github/workflows/reusable-build-test.yml" + - ".github/workflows/reusable-check-autogen.yml" - ".github/workflows/reusable-clang-tidy.yml" - ".github/workflows/reusable-package.yml" - ".github/workflows/reusable-strategy-matrix.yml" @@ -67,6 +68,9 @@ defaults: shell: bash jobs: + check-autogen: + uses: ./.github/workflows/reusable-check-autogen.yml + clang-tidy: uses: ./.github/workflows/reusable-clang-tidy.yml permissions: diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index e21067cc5f..548fde8b5e 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -176,9 +176,9 @@ jobs: .. # Export the sanitizer options before any instrumented binary runs. The - # protocol code-gen and build steps below invoke instrumented dependency - # tools (protoc, grpc), so setting UBSAN_OPTIONS here lets the UBSan - # suppression list silence their diagnostics too, not just at test time. + # build step below invokes instrumented dependency tools (protoc, grpc), + # so setting UBSAN_OPTIONS here lets the UBSan suppression list silence + # their diagnostics too, not just at test time. # GITHUB_WORKSPACE (not the github.workspace context) is used so the path # resolves correctly inside the container job. - name: Set sanitizer options @@ -196,32 +196,6 @@ jobs: echo "UBSAN_OPTIONS=include=${SUPP}/runtime-ubsan-options.txt:suppressions=${SUPP}/ubsan.supp" >>${GITHUB_ENV} echo "LSAN_OPTIONS=include=${SUPP}/runtime-lsan-options.txt:suppressions=${SUPP}/lsan.supp" >>${GITHUB_ENV} - - name: Check protocol autogen files are up-to-date - working-directory: ${{ env.BUILD_DIR }} - env: - MESSAGE: | - - The generated protocol wrapper classes are out of date. - - This typically happens when the macro files or generator scripts - have changed but the generated files were not regenerated. - - To fix this: - 1. Run: cmake --build . --target setup_code_gen - 2. Run: cmake --build . --target code_gen - 3. Commit and push the regenerated files - run: | - set -e - cmake --build . --target setup_code_gen - cmake --build . --target code_gen - DIFF=$(git -C .. status --porcelain -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen) - if [ -n "${DIFF}" ]; then - echo "::error::Generated protocol files are out of date" - git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen - echo "${MESSAGE}" - exit 1 - fi - - name: Build the binary working-directory: ${{ env.BUILD_DIR }} env: diff --git a/.github/workflows/reusable-check-autogen.yml b/.github/workflows/reusable-check-autogen.yml new file mode 100644 index 0000000000..bb77ea85a9 --- /dev/null +++ b/.github/workflows/reusable-check-autogen.yml @@ -0,0 +1,76 @@ +# This workflow checks that the generated protocol wrapper classes are +# up-to-date with the macro files and generator scripts they are produced from, +# see more info in include/xrpl/protocol_autogen/README.md. +name: Check autogen + +# This workflow can only be triggered by other workflows. +on: workflow_call + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-autogen + cancel-in-progress: true + +defaults: + run: + shell: bash + +env: + BUILD_DIR: build/codegen + +jobs: + autogen: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + + # Code generation is pure Python, so the standalone project below offers + # the same targets as the main build without needing its dependencies or + # a compiler, which keeps this job down to a few seconds. + - name: Configure CMake + run: cmake -S cmake/codegen -B "${BUILD_DIR}" + + - name: Install code generation dependencies + run: cmake --build "${BUILD_DIR}" --target setup_code_gen + + - name: Generate code + run: cmake --build "${BUILD_DIR}" --target code_gen + + - name: Check for differences + env: + MESSAGE: | + + The generated protocol wrapper classes are out of date. + + This typically happens when the macro files or generator scripts + have changed but the generated files were not regenerated. + + Run the following from the repository root, then commit and push + the regenerated files. This needs neither the dependencies nor a + compiler. See include/xrpl/protocol_autogen/README.md for more info. + + cmake -S cmake/codegen -B build/codegen + cmake --build build/codegen --target setup_code_gen + cmake --build build/codegen --target code_gen + + In an already configured build directory, the 'setup_code_gen' and + 'code_gen' targets do the same thing. + run: | + # Record untracked files in the index without staging their contents, + # so that classes generated for a newly added transaction or ledger + # entry type show up in the diff below rather than silently as an + # empty one. + git add --intent-to-add . + DIFF=$(git status --porcelain) + if [ -n "${DIFF}" ]; then + # Print the differences to give the contributor a hint about what to + # expect when running code generation on their own machine. + git diff + echo "${MESSAGE}" + exit 1 + fi diff --git a/BUILD.md b/BUILD.md index edc52fe3b7..238c10e17c 100644 --- a/BUILD.md +++ b/BUILD.md @@ -247,7 +247,17 @@ cmake --build . --target setup_code_gen # create venv and install dependencies cmake --build . --target code_gen # regenerate code ``` -The regenerated files should be committed alongside your changes. +The same targets are also available as a standalone project, which does not +need the dependencies to be configured first: + +``` +cmake -S cmake/codegen -B build/codegen +cmake --build build/codegen --target setup_code_gen +cmake --build build/codegen --target code_gen +``` + +The regenerated files should be committed alongside your changes. CI verifies +that they are up-to-date. ## Coverage report diff --git a/cmake/XrplProtocolAutogen.cmake b/cmake/XrplProtocolAutogen.cmake index dd9ef6a9a4..33af560113 100644 --- a/cmake/XrplProtocolAutogen.cmake +++ b/cmake/XrplProtocolAutogen.cmake @@ -2,21 +2,22 @@ Protocol Autogen - Code generation for protocol wrapper classes #]===================================================================] +# The repository root, derived from the location of this file rather than from +# the including project, so that the targets below can also be offered on their +# own by cmake/codegen/CMakeLists.txt. +get_filename_component(XRPL_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) + set(CODEGEN_VENV_DIR - "${CMAKE_CURRENT_SOURCE_DIR}/.venv" + "${XRPL_ROOT}/.venv" CACHE PATH "Path to a Python virtual environment for code generation. A venv will be created here by setup_code_gen and used to run generation scripts." ) # Directory paths -set(MACRO_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl/protocol/detail") -set(AUTOGEN_HEADER_DIR - "${CMAKE_CURRENT_SOURCE_DIR}/include/xrpl/protocol_autogen" -) -set(AUTOGEN_TEST_DIR - "${CMAKE_CURRENT_SOURCE_DIR}/src/tests/libxrpl/protocol_autogen" -) -set(SCRIPTS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake/scripts/codegen") +set(MACRO_DIR "${XRPL_ROOT}/include/xrpl/protocol/detail") +set(AUTOGEN_HEADER_DIR "${XRPL_ROOT}/include/xrpl/protocol_autogen") +set(AUTOGEN_TEST_DIR "${XRPL_ROOT}/src/tests/libxrpl/protocol_autogen") +set(SCRIPTS_DIR "${XRPL_ROOT}/cmake/scripts/codegen") # Input macro files set(TRANSACTIONS_MACRO "${MACRO_DIR}/transactions.macro") @@ -114,14 +115,14 @@ if(CODEGEN_VENV_DIR) setup_code_gen COMMAND ${Python3_EXECUTABLE} -m venv "${CODEGEN_VENV_DIR}" COMMAND ${CODEGEN_PYTHON} -m pip install -r "${REQUIREMENTS_FILE}" - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + WORKING_DIRECTORY "${XRPL_ROOT}" COMMENT "Creating venv and installing code generation dependencies..." ) else() add_custom_target( setup_code_gen COMMAND ${Python3_EXECUTABLE} -m pip install -r "${REQUIREMENTS_FILE}" - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + WORKING_DIRECTORY "${XRPL_ROOT}" COMMENT "Installing code generation dependencies..." ) endif() @@ -139,8 +140,8 @@ add_custom_target( -DSFIELDS_MACRO=${SFIELDS_MACRO} -DAUTOGEN_HEADER_DIR=${AUTOGEN_HEADER_DIR} -DAUTOGEN_TEST_DIR=${AUTOGEN_TEST_DIR} -P - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/XrplProtocolAutogenRun.cmake" - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/XrplProtocolAutogenRun.cmake" + WORKING_DIRECTORY "${XRPL_ROOT}" COMMENT "Running protocol code generation..." SOURCES ${ALL_INPUT_FILES} ) diff --git a/cmake/codegen/CMakeLists.txt b/cmake/codegen/CMakeLists.txt new file mode 100644 index 0000000000..f697d67dd8 --- /dev/null +++ b/cmake/codegen/CMakeLists.txt @@ -0,0 +1,21 @@ +#[===================================================================[ + Protocol Autogen - Standalone project + + Exposes the 'setup_code_gen' and 'code_gen' targets on their own, without + configuring the rest of xrpl. Code generation is pure Python, so this needs + neither the dependencies nor a compiler, which makes it usable in CI and by + contributors who only want to regenerate the protocol wrapper classes: + + cmake -S cmake/codegen -B build/codegen + cmake --build build/codegen --target setup_code_gen + cmake --build build/codegen --target code_gen + + The targets are identical to the ones offered by the top-level build, since + both come from cmake/XrplProtocolAutogen.cmake. +#]===================================================================] + +cmake_minimum_required(VERSION 3.16) + +project(xrpl_codegen LANGUAGES NONE) + +include("${CMAKE_CURRENT_LIST_DIR}/../XrplProtocolAutogen.cmake") diff --git a/include/xrpl/protocol_autogen/README.md b/include/xrpl/protocol_autogen/README.md index 608ffed085..ed649a05fc 100644 --- a/include/xrpl/protocol_autogen/README.md +++ b/include/xrpl/protocol_autogen/README.md @@ -23,6 +23,16 @@ By default, `CODEGEN_VENV_DIR` points to `.venv` in the project root. The `setup_code_gen` target creates a venv there and installs the required packages. The `code_gen` target then uses the venv's Python interpreter to run generation. +Generation is pure Python, so the same targets are also available as a +standalone project that needs neither the dependencies nor a compiler. This is +what CI uses, and it is handy if you only want to regenerate these files: + +```bash +cmake -S cmake/codegen -B build/codegen +cmake --build build/codegen --target setup_code_gen +cmake --build build/codegen --target code_gen +``` + ### Python Dependencies The code generation requires the following Python packages (installed by `setup_code_gen`): From 00a178fb92ca49521b937ae1a99d863765ea8a90 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 6 Aug 2026 17:34:39 +0100 Subject: [PATCH 79/86] 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 80/86] 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 81/86] 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 82/86] 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 83/86] 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 84/86] 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 8a3363752f7db77796e8d07261506eeef18267a9 Mon Sep 17 00:00:00 2001 From: pwang200 <354723+pwang200@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:41:56 -0400 Subject: [PATCH 85/86] trace host function refactor (#7920) --- include/xrpl/tx/wasm/HostFunc.h | 31 +- include/xrpl/tx/wasm/HostFuncImpl.h | 16 +- include/xrpl/tx/wasm/HostFuncWrapper.h | 16 +- include/xrpl/tx/wasm/WasmCommon.h | 12 + src/libxrpl/tx/wasm/HostFuncImpl.cpp | 7 + src/libxrpl/tx/wasm/HostFuncImplTrace.cpp | 72 -- src/libxrpl/tx/wasm/HostFuncWrapper.cpp | 492 ++++++----- src/libxrpl/tx/wasm/WasmVM.cpp | 8 +- src/test/app/HostFuncImpl_test.cpp | 314 +++++-- src/test/app/TestHostFunctions.h | 53 +- src/test/app/Wasm_test.cpp | 14 +- .../all_host_functions/Cargo.lock | 77 +- .../all_host_functions/Cargo.toml | 3 +- .../all_host_functions/src/lib.rs | 93 +- .../wasm_fixtures/codecov_tests/Cargo.lock | 77 +- .../wasm_fixtures/codecov_tests/Cargo.toml | 3 +- .../codecov_tests/src/host_bindings_loose.rs | 11 +- .../wasm_fixtures/codecov_tests/src/lib.rs | 428 +++++---- src/test/app/wasm_fixtures/fixtures.cpp | 816 +++++++++--------- 19 files changed, 1250 insertions(+), 1293 deletions(-) delete mode 100644 src/libxrpl/tx/wasm/HostFuncImplTrace.cpp diff --git a/include/xrpl/tx/wasm/HostFunc.h b/include/xrpl/tx/wasm/HostFunc.h index f318610488..96953fbf90 100644 --- a/include/xrpl/tx/wasm/HostFunc.h +++ b/include/xrpl/tx/wasm/HostFunc.h @@ -400,34 +400,11 @@ public: return std::unexpected(HostFunctionError::Unimplemented); } - [[nodiscard]] [[nodiscard]] virtual std::expected - trace(std::string_view const& msg, Slice const& data, bool asHex) const + // A no-op rather than Unimplemented: trace only writes to the local log. + // trace_wrap has already rendered the guest's buffer into `data`. + virtual void + trace(std::string_view const& msg, std::string_view const& data) const { - return std::unexpected(HostFunctionError::Unimplemented); - } - - [[nodiscard]] [[nodiscard]] virtual std::expected - traceNum(std::string_view const& msg, int64_t data) const - { - return std::unexpected(HostFunctionError::Unimplemented); - } - - [[nodiscard]] [[nodiscard]] virtual std::expected - traceAccount(std::string_view const& msg, AccountID const& account) const - { - return std::unexpected(HostFunctionError::Unimplemented); - } - - [[nodiscard]] [[nodiscard]] virtual std::expected - traceFloat(std::string_view const& msg, Slice const& data) const - { - return std::unexpected(HostFunctionError::Unimplemented); - } - - [[nodiscard]] [[nodiscard]] virtual std::expected - traceAmount(std::string_view const& msg, STAmount const& amount) const - { - return std::unexpected(HostFunctionError::Unimplemented); } [[nodiscard]] [[nodiscard]] virtual std::expected diff --git a/include/xrpl/tx/wasm/HostFuncImpl.h b/include/xrpl/tx/wasm/HostFuncImpl.h index 37ee4af5a8..569b151e29 100644 --- a/include/xrpl/tx/wasm/HostFuncImpl.h +++ b/include/xrpl/tx/wasm/HostFuncImpl.h @@ -241,20 +241,8 @@ public: std::expected getNFTSequence(uint256 const& nftId) const override; - std::expected - trace(std::string_view const& msg, Slice const& data, bool asHex) const override; - - std::expected - traceNum(std::string_view const& msg, int64_t data) const override; - - std::expected - traceAccount(std::string_view const& msg, AccountID const& account) const override; - - std::expected - traceFloat(std::string_view const& msg, Slice const& data) const override; - - std::expected - traceAmount(std::string_view const& msg, STAmount const& amount) const override; + void + trace(std::string_view const& msg, std::string_view const& data) const override; std::expected floatFromInt(int64_t x, int32_t mode) const override; diff --git a/include/xrpl/tx/wasm/HostFuncWrapper.h b/include/xrpl/tx/wasm/HostFuncWrapper.h index 1d04d7202a..4884c750f1 100644 --- a/include/xrpl/tx/wasm/HostFuncWrapper.h +++ b/include/xrpl/tx/wasm/HostFuncWrapper.h @@ -190,21 +190,11 @@ wasm_trap_t* getNFTTransferFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST); using getNFTSequence_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); wasm_trap_t* getNFTSequence_wrap(WASM_SECONDARY_CB_PARAMS_LIST); -using trace_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, int32_t); +// trace(msg_ptr, msg_len, data_type, data_ptr, data_len); data_type is a +// TraceDataType. +using trace_proto = void(uint8_t const*, int32_t, int32_t, uint8_t const*, int32_t); wasm_trap_t* trace_wrap(WASM_SECONDARY_CB_PARAMS_LIST); -using traceNum_proto = int32_t(uint8_t const*, int32_t, int64_t); -wasm_trap_t* traceNum_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using traceAccount_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t); -wasm_trap_t* traceAccount_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using traceFloat_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t); -wasm_trap_t* traceFloat_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using traceAmount_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t); -wasm_trap_t* traceAmount_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - using floatFromInt_proto = int32_t(int64_t, uint8_t*, int32_t, int32_t); wasm_trap_t* floatFromInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST); diff --git a/include/xrpl/tx/wasm/WasmCommon.h b/include/xrpl/tx/wasm/WasmCommon.h index 3e55777b7c..f73ca7c2d2 100644 --- a/include/xrpl/tx/wasm/WasmCommon.h +++ b/include/xrpl/tx/wasm/WasmCommon.h @@ -33,6 +33,18 @@ std::string_view inline constexpr hfErrInternal = "HfInternal"; std::string_view inline constexpr hfErrOutOfGas = "HfOutOfGas"; std::string_view inline constexpr wasmiTrapOutOfFuel = "OutOfFuel"; +// Guest ABI, mirrored in the wasm stdlib: append only, never renumber. Starts at +// 1 so a zeroed data_type is rejected rather than treated as Int64. +enum class TraceDataType : std::int32_t { + Int64 = 1, + Uint64, + Xfloat, + Account, + Amount, + AsHex, // raw bytes, hex-encoded by the host before printing + AsText, // bytes printed verbatim as text +}; + enum class HostFunctionError : int32_t { Unimplemented = -1, FieldNotFound = -2, diff --git a/src/libxrpl/tx/wasm/HostFuncImpl.cpp b/src/libxrpl/tx/wasm/HostFuncImpl.cpp index 2067062deb..c82690099a 100644 --- a/src/libxrpl/tx/wasm/HostFuncImpl.cpp +++ b/src/libxrpl/tx/wasm/HostFuncImpl.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace xrpl { @@ -49,4 +50,10 @@ WasmHostFunctionsImpl::computeSha512HalfHash(Slice const& data) const return hash; } +void +WasmHostFunctionsImpl::trace(std::string_view const& msg, std::string_view const& data) const +{ + log(msg, [&data] { return data; }); +} + } // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostFuncImplTrace.cpp b/src/libxrpl/tx/wasm/HostFuncImplTrace.cpp deleted file mode 100644 index 4e4058ea39..0000000000 --- a/src/libxrpl/tx/wasm/HostFuncImplTrace.cpp +++ /dev/null @@ -1,72 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - -#ifdef _DEBUG -// #define DEBUG_OUTPUT 1 -#endif - -namespace xrpl { - -std::expected -WasmHostFunctionsImpl::trace(std::string_view const& msg, Slice const& data, bool asHex) const -{ - if (!asHex) - { - log(msg, [&data] { - return std::string_view(reinterpret_cast(data.data()), data.size()); - }); - } - else - { - log(msg, [&data] { - std::string hex; - hex.reserve(data.size() * 2); - boost::algorithm::hex(data.begin(), data.end(), std::back_inserter(hex)); - return hex; - }); - } - - return 0; -} - -std::expected -WasmHostFunctionsImpl::traceNum(std::string_view const& msg, int64_t data) const -{ - log(msg, [data] { return data; }); - return 0; -} - -std::expected -WasmHostFunctionsImpl::traceAccount(std::string_view const& msg, AccountID const& account) const -{ - log(msg, [&account] { return toBase58(account); }); - return 0; -} - -std::expected -WasmHostFunctionsImpl::traceFloat(std::string_view const& msg, Slice const& data) const -{ - log(msg, [&data] { return wasm_float::floatToString(data); }); - return 0; -} - -std::expected -WasmHostFunctionsImpl::traceAmount(std::string_view const& msg, STAmount const& amount) const -{ - log(msg, [&amount] { return amount.getFullText(); }); - return 0; -} - -} // namespace xrpl diff --git a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp index ea5caec6cc..a6cd5fc1e3 100644 --- a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp +++ b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,8 @@ #include #include +#include + #include #include @@ -31,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -544,8 +548,8 @@ HostFuncMain_wrap(WASM_CB_PARAMS_LIST) wasm_trap_t* getLedgerSqn_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int const index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t const index = 0; + auto& runtime = hf.getRT(); return returnResult(runtime, params, results, hf.getLedgerSqn(), index); } @@ -553,8 +557,8 @@ getLedgerSqn_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getParentLedgerTime_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int const index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t const index = 0; + auto& runtime = hf.getRT(); return returnResult(runtime, params, results, hf.getParentLedgerTime(), index); } @@ -562,8 +566,8 @@ getParentLedgerTime_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getParentLedgerHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int const index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t const index = 0; + auto& runtime = hf.getRT(); return returnResult(runtime, params, results, hf.getParentLedgerHash(), index); } @@ -571,8 +575,8 @@ getParentLedgerHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getBaseFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int const index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t const index = 0; + auto& runtime = hf.getRT(); return returnResult(runtime, params, results, hf.getBaseFee(), index); } @@ -580,8 +584,8 @@ getBaseFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* isAmendmentEnabled_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const slice = getDataSlice(runtime, params, index); if (!slice) @@ -605,8 +609,8 @@ isAmendmentEnabled_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* cacheLedgerObj_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const id = getDataUInt256(runtime, params, index); if (!id) @@ -622,8 +626,8 @@ cacheLedgerObj_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getTxField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const fname = getDataSField(runtime, params, index); if (!fname) @@ -635,8 +639,8 @@ getTxField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getCurrentLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const fname = getDataSField(runtime, params, index); if (!fname) @@ -648,8 +652,8 @@ getCurrentLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const cache = getDataInt32(runtime, params, index); if (!cache) @@ -665,8 +669,8 @@ getLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getTxNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const locator = getDataLocator(runtime, params, index); if (!locator) @@ -678,8 +682,8 @@ getTxNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getCurrentLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const locator = getDataLocator(runtime, params, index); if (!locator) @@ -692,8 +696,8 @@ getCurrentLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const cache = getDataInt32(runtime, params, index); if (!cache) @@ -710,8 +714,8 @@ getLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getTxArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const fname = getDataSField(runtime, params, index); if (!fname) @@ -723,8 +727,8 @@ getTxArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getCurrentLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const fname = getDataSField(runtime, params, index); if (!fname) @@ -736,8 +740,8 @@ getCurrentLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const cache = getDataInt32(runtime, params, index); if (!cache) @@ -753,8 +757,8 @@ getLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getTxNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const locator = getDataLocator(runtime, params, index); if (!locator) @@ -766,8 +770,8 @@ getTxNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getCurrentLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const locator = getDataLocator(runtime, params, index); if (!locator) @@ -779,8 +783,8 @@ getCurrentLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const cache = getDataInt32(runtime, params, index); if (!cache) @@ -797,8 +801,8 @@ getLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* updateData_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const bytes = getDataSlice(runtime, params, index); if (!bytes) @@ -810,8 +814,8 @@ updateData_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* checkSignature_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const message = getDataSlice(runtime, params, index); if (!message) @@ -832,8 +836,8 @@ checkSignature_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* computeSha512HalfHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const bytes = getDataSlice(runtime, params, index); if (!bytes) @@ -845,8 +849,8 @@ computeSha512HalfHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* accountKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -858,8 +862,8 @@ accountKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* ammKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const issue1 = getDataAsset(runtime, params, index); if (!issue1) @@ -876,8 +880,8 @@ ammKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* checkKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -893,8 +897,8 @@ checkKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* credentialKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const subj = getDataAccountID(runtime, params, index); if (!subj) @@ -915,8 +919,8 @@ credentialKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* delegateKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -933,8 +937,8 @@ delegateKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* depositPreauthKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -951,8 +955,8 @@ depositPreauthKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* didKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -964,8 +968,8 @@ didKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* escrowKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -981,8 +985,8 @@ escrowKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* trustLineKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc1 = getDataAccountID(runtime, params, index); if (!acc1) @@ -1007,8 +1011,8 @@ trustLineKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* mptokenIssuanceKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1025,8 +1029,8 @@ mptokenIssuanceKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* mptokenKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const slice = getDataSlice(runtime, params, index); if (!slice) @@ -1046,8 +1050,8 @@ mptokenKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* nftokenOfferKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1064,8 +1068,8 @@ nftokenOfferKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* offerKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1081,8 +1085,8 @@ offerKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* oracleKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1098,8 +1102,8 @@ oracleKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* paychannelKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1124,8 +1128,8 @@ paychannelKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* permissionedDomainKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1142,8 +1146,8 @@ permissionedDomainKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* signerListKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1155,8 +1159,8 @@ signerListKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* ticketKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1172,8 +1176,8 @@ ticketKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* vaultKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1189,8 +1193,8 @@ vaultKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getNFT_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const acc = getDataAccountID(runtime, params, index); if (!acc) @@ -1206,8 +1210,8 @@ getNFT_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getNFTIssuer_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const nftId = getDataUInt256(runtime, params, index); if (!nftId) @@ -1219,8 +1223,8 @@ getNFTIssuer_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getNFTTaxon_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const nftId = getDataUInt256(runtime, params, index); if (!nftId) @@ -1232,8 +1236,8 @@ getNFTTaxon_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getNFTFlags_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const nftId = getDataUInt256(runtime, params, index); if (!nftId) @@ -1245,8 +1249,8 @@ getNFTFlags_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getNFTTransferFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const nftId = getDataUInt256(runtime, params, index); if (!nftId) @@ -1258,8 +1262,8 @@ getNFTTransferFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* getNFTSequence_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t index = 0; + auto& runtime = hf.getRT(); auto const nftId = getDataUInt256(runtime, params, index); if (!nftId) @@ -1268,124 +1272,150 @@ getNFTSequence_wrap(WASM_SECONDARY_CB_PARAMS_LIST) return returnResult(runtime, params, results, hf.getNFTSequence(*nftId), index); } +// log() ignores the journal under DEBUG_OUTPUT, so the gate must not either. +static inline bool +traceActive([[maybe_unused]] HostFunctions const& hf) +{ +#ifdef DEBUG_OUTPUT + return true; +#else + return hf.getJournal().active(beast::Severity::Trace); +#endif +} + +// Not getDataUnsigned: that branches on pointer alignment, and trace must cost +// the same regardless of how the guest laid out its buffer. +template +static std::optional +traceInt(Slice const& data) +{ + static_assert(std::is_integral_v); + if (data.size() != sizeof(T)) + return std::nullopt; + + T x; + memcpy(&x, data.data(), sizeof(T)); + return adjustWasmEndianess(x); +} + +// std::nullopt means the buffer does not match the type. May throw. +static std::optional +traceFormat(TraceDataType type, Slice const& data) +{ + switch (type) + { + case TraceDataType::Int64: + if (auto const x = traceInt(data)) + return std::to_string(*x); + return std::nullopt; + + case TraceDataType::Uint64: + if (auto const x = traceInt(data)) + return std::to_string(*x); + return std::nullopt; + + case TraceDataType::Xfloat: + return wasm_float::floatToString(data); + + case TraceDataType::Account: + // Not getDataAccountID: it charges the transfer limit. + if (data.size() != AccountID::size()) + return std::nullopt; + return toBase58(AccountID::fromVoid(data.data())); + + case TraceDataType::Amount: { + auto serialIter = SerialIter(data); + STAmount const amount(serialIter, sfGeneric); // may throw + return amount.getFullText(); + } + + case TraceDataType::AsHex: { + std::string hex; + hex.reserve(data.size() * 2); + boost::algorithm::hex(data.begin(), data.end(), std::back_inserter(hex)); + return hex; + } + + case TraceDataType::AsText: + // An empty Slice has a null data(), which std::string may not take. + if (data.empty()) + return std::string(); + return std::string(reinterpret_cast(data.data()), data.size()); + } + + return std::nullopt; // unknown data_type +} + +// trace's only effect is this node's local log, so nothing observable may depend +// on the log level: gas is charged in mainCheck before this runs, no transfer +// limit is charged, and errors are logged rather than trapped. wasm_trap_t* trace_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + if (!traceActive(hf)) + return nullptr; - auto const msg = getDataString(runtime, params, index); - if (!msg) - return hfResult(results, msg.error()); - - auto const data = getDataSlice(runtime, params, index); - if (!data) - return hfResult(results, data.error()); - - if (msg->size() + data->size() > kMaxWasmDataLength) - return hfResult(results, HostFunctionError::DataFieldTooLarge); - - auto const asHex = getDataInt32(runtime, params, index); - if (!asHex) - return hfResult(results, asHex.error()); // LCOV_EXCL_LINE - - if (*asHex != 0 && *asHex != 1) - return hfResult(results, HostFunctionError::InvalidParams); - - return returnResult(runtime, params, results, hf.trace(*msg, *data, *asHex != 0), index); -} - -wasm_trap_t* -traceNum_wrap(WASM_SECONDARY_CB_PARAMS_LIST) -{ - int index = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); - - auto const msg = getDataString(runtime, params, index); - if (!msg) - return hfResult(results, msg.error()); - - auto const number = getDataInt64(runtime, params, index); - if (!number) - return hfResult(results, number.error()); // LCOV_EXCL_LINE - - return returnResult(runtime, params, results, hf.traceNum(*msg, *number), index); -} - -wasm_trap_t* -traceAccount_wrap(WASM_SECONDARY_CB_PARAMS_LIST) -{ - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); - - auto const msg = getDataString(runtime, params, i); - if (!msg) - return hfResult(results, msg.error()); - - auto const account = getDataAccountID(runtime, params, i); - if (!account) - return hfResult(results, account.error()); - - return returnResult(runtime, params, results, hf.traceAccount(*msg, *account), i); -} - -wasm_trap_t* -traceFloat_wrap(WASM_SECONDARY_CB_PARAMS_LIST) -{ - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); - - auto const msg = getDataString(runtime, params, i); - if (!msg) - return hfResult(results, msg.error()); - - auto const number = getDataSlice(runtime, params, i); - if (!number) - return hfResult(results, number.error()); - - return returnResult(runtime, params, results, hf.traceFloat(*msg, *number), i); -} - -wasm_trap_t* -traceAmount_wrap(WASM_SECONDARY_CB_PARAMS_LIST) -{ - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); - - auto const msg = getDataString(runtime, params, i); - if (!msg) - return hfResult(results, msg.error()); - - auto const amountSliceOpt = getDataSlice(runtime, params, i); - if (!amountSliceOpt) - return hfResult(results, amountSliceOpt.error()); - - auto const amountSlice = amountSliceOpt.value(); - auto serialIter = SerialIter(amountSlice); - - std::optional amount; try { - amount = STAmount(serialIter, sfGeneric); - } - catch (std::exception const&) - { - amount = std::nullopt; - } + int32_t index = 0; + auto& runtime = hf.getRT(); - if (!amount) - { - return hfResult(results, HostFunctionError::InvalidParams); - } + auto const msg = getDataString(runtime, params, index); + if (!msg) + { + hf.getJournal().trace() << "WasmTrace: invalid message"; + return nullptr; + } - return returnResult(runtime, params, results, hf.traceAmount(*msg, *amount), i); + auto const type = getDataInt32(runtime, params, index); + // LCOV_EXCL_START + if (!type) + { + hf.getJournal().trace() << "WasmTrace: invalid data type"; + return nullptr; + } + // LCOV_EXCL_STOP + + auto const data = getDataSlice(runtime, params, index); + if (!data) + { + hf.getJournal().trace() << "WasmTrace: invalid data"; + return nullptr; + } + + if (msg->size() + data->size() > kMaxWasmDataLength) + { + hf.getJournal().trace() << "WasmTrace: message and data too long"; + return nullptr; + } + + auto const text = traceFormat(static_cast(*type), *data); + if (!text) + { + hf.getJournal().trace() << "WasmTrace: data does not match the data type"; + return nullptr; + } + + hf.trace(*msg, *text); + } + catch (std::exception const& e) + { + hf.getJournal().trace() << "WasmTrace: error: " << e.what(); + } + // LCOV_EXCL_START + catch (...) + { + hf.getJournal().trace() << "WasmTrace: unknown error"; + } + // LCOV_EXCL_STOP + return nullptr; } wasm_trap_t* floatFromInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataInt64(runtime, params, i); if (!x) @@ -1403,8 +1433,8 @@ floatFromInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatFromUint_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataUInt64(runtime, params, i); if (!x) @@ -1422,8 +1452,8 @@ floatFromUint_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatFromSTAmount_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1454,8 +1484,8 @@ floatFromSTAmount_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatFromSTNumber_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1486,8 +1516,8 @@ floatFromSTNumber_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatToInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1505,8 +1535,8 @@ floatToInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatToMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1519,8 +1549,8 @@ floatToMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatFromMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const mant = getDataInt64(runtime, params, i); if (!mant) @@ -1542,8 +1572,8 @@ floatFromMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatCompare_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1559,8 +1589,8 @@ floatCompare_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatAdd_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1582,8 +1612,8 @@ floatAdd_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatSubtract_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1605,8 +1635,8 @@ floatSubtract_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatMultiply_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1628,8 +1658,8 @@ floatMultiply_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatDivide_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1651,8 +1681,8 @@ floatDivide_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatRoot_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1674,8 +1704,8 @@ floatRoot_wrap(WASM_SECONDARY_CB_PARAMS_LIST) wasm_trap_t* floatPower_wrap(WASM_SECONDARY_CB_PARAMS_LIST) { - int i = 0; - WasmRuntimeWrapper& runtime = hf.getRT(); + int32_t i = 0; + auto& runtime = hf.getRT(); auto const x = getDataSlice(runtime, params, i); if (!x) @@ -1757,7 +1787,7 @@ testGetDataIncrement() values[0] = WASM_I32_VAL(42); - int index = 0; + int32_t index = 0; auto const result = getDataInt32(runtime, ¶ms, index); if (!result || result.value() != 42 || index != 1) return false; @@ -1769,7 +1799,7 @@ testGetDataIncrement() values[0] = WASM_I64_VAL(1234); - int index = 0; + int32_t index = 0; auto const result = getDataInt64(runtime, ¶ms, index); if (!result || result.value() != 1234 || index != 1) return false; @@ -1781,7 +1811,7 @@ testGetDataIncrement() values[0] = WASM_I32_VAL(sfAccount.getCode()); - int index = 0; + int32_t index = 0; auto const result = getDataSField(runtime, ¶ms, index); if (!result || result.value().get() != sfAccount || index != 1) return false; @@ -1794,7 +1824,7 @@ testGetDataIncrement() values[0] = WASM_I32_VAL(0); values[1] = WASM_I32_VAL(3); - int index = 0; + int32_t index = 0; auto const result = getDataSlice(runtime, ¶ms, index); if (!result || result.value() != Slice(buffer.data(), 3) || index != 2) return false; @@ -1807,7 +1837,7 @@ testGetDataIncrement() values[0] = WASM_I32_VAL(0); values[1] = WASM_I32_VAL(5); - int index = 0; + int32_t index = 0; auto const result = getDataString(runtime, ¶ms, index); if (!result || result.value() != std::string_view(reinterpret_cast(buffer.data()), 5) || @@ -1826,7 +1856,7 @@ testGetDataIncrement() values[1] = WASM_I32_VAL(AccountID::size()); memcpy(&buffer[0], id.data(), AccountID::size()); - int index = 0; + int32_t index = 0; auto const result = getDataAccountID(runtime, ¶ms, index); if (!result || result.value() != id || index != 2) return false; @@ -1842,7 +1872,7 @@ testGetDataIncrement() values[1] = WASM_I32_VAL(Hash::size()); memcpy(&buffer[0], h1.data(), Hash::size()); - int index = 0; + int32_t index = 0; auto const result = getDataUInt256(runtime, ¶ms, index); if (!result || result.value() != h1 || index != 2) return false; @@ -1858,7 +1888,7 @@ testGetDataIncrement() values[1] = WASM_I32_VAL(Currency::size()); memcpy(&buffer[0], c.data(), Currency::size()); - int index = 0; + int32_t index = 0; auto const result = getDataCurrency(runtime, ¶ms, index); if (!result || result.value() != c || index != 2) return false; diff --git a/src/libxrpl/tx/wasm/WasmVM.cpp b/src/libxrpl/tx/wasm/WasmVM.cpp index 7eda08f42b..ed87f7c4ac 100644 --- a/src/libxrpl/tx/wasm/WasmVM.cpp +++ b/src/libxrpl/tx/wasm/WasmVM.cpp @@ -79,13 +79,9 @@ setCommonHostFunctions(HostFunctions& hfs, ImportVec& i) WASM_IMPORT_FUNC2(i, getNFTTaxon, "nft_taxon", hfs, 60); WASM_IMPORT_FUNC2(i, getNFTFlags, "nft_flags", hfs, 60); WASM_IMPORT_FUNC2(i, getNFTTransferFee, "nft_xfer_fee", hfs, 60); - WASM_IMPORT_FUNC2(i, getNFTSequence, "nft_serial", hfs, 60); + WASM_IMPORT_FUNC2(i, getNFTSequence, "nft_serial", hfs, 60); - WASM_IMPORT_FUNC (i, trace, hfs, 500); - WASM_IMPORT_FUNC2(i, traceNum, "trace_num", hfs, 500); - WASM_IMPORT_FUNC2(i, traceAccount, "trace_acct", hfs, 500); - WASM_IMPORT_FUNC2(i, traceFloat, "trace_xfloat", hfs, 500); - WASM_IMPORT_FUNC2(i, traceAmount, "trace_amt", hfs, 500); + WASM_IMPORT_FUNC (i, trace, hfs, 30); WASM_IMPORT_FUNC2(i, floatFromInt, "float_from_int", hfs, 100); WASM_IMPORT_FUNC2(i, floatFromUint, "float_from_uint", hfs, 130); diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp index 8311aae638..8c0bf1ab60 100644 --- a/src/test/app/HostFuncImpl_test.cpp +++ b/src/test/app/HostFuncImpl_test.cpp @@ -360,6 +360,13 @@ ww(E&& e, P&& params, P&& result, Args... args) return HostFuncMain_wrap(std::forward(e), params.get(), result.get()); // NOLINT } +// ww() packs only integral args as wasm params, so the scoped enum needs widening. +constexpr int32_t +traceDataTypeToInt(TraceDataType t) +{ + return static_cast(t); +} + constexpr int64_t min64 = std::numeric_limits::min(); constexpr int64_t max64 = std::numeric_limits::max(); constexpr int32_t floatSize = 12; @@ -3440,32 +3447,45 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string data = "abc"; auto const slice = Slice(data.data(), data.size()); - // hfs.trace(msg, slice, false); + // AsText: data printed verbatim (was trace with as_hex = 0) { vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, slice.data(), slice.size()); - WasmValVec params(5), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace"), params, result, 0, msg.size(), 256, slice.size(), 0); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::AsText), + 256, + slice.size()); - if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0)) + if (BEAST_EXPECT(!trap)) { auto const messages = sink.messages().str(); BEAST_EXPECT(messages.contains(msg)); + BEAST_EXPECT(messages.contains(data)); } } - // hfs.trace(msg, slice, true); + // AsHex: host hex-encodes data (was trace with as_hex = 1) { vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, slice.data(), slice.size()); - WasmValVec params(5), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace"), params, result, 0, msg.size(), 256, slice.size(), 1); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::AsHex), + 256, + slice.size()); - if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0)) + if (BEAST_EXPECT(!trap)) { auto const messages = sink.messages().str(); std::string hex; @@ -3475,6 +3495,41 @@ struct HostFuncImpl_test : public beast::unit_test::Suite BEAST_EXPECT(messages.contains(hex)); } } + + // Unknown data_type: logged as invalid, never a trap + { + vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); + vrt.setBytes(256, slice.data(), slice.size()); + WasmValVec params(5), result(0); + auto* trap = + ww(&import.at("trace"), params, result, 0, msg.size(), 9999, 256, slice.size()); + BEAST_EXPECT(!trap); + } + + // msg and data each fit, but their combined size exceeds + // kMaxWasmDataLength, so nothing is logged + { + std::string const longMsg(kMaxWasmDataLength, 'x'); + vrt.setBytes(0, reinterpret_cast(longMsg.data()), longMsg.size()); + vrt.setBytes(2048, slice.data(), slice.size()); + WasmValVec params(5), result(0); + auto* trap = + ww(&import.at("trace"), + params, + result, + 0, + longMsg.size(), + traceDataTypeToInt(TraceDataType::AsText), + 2048, + slice.size()); + + if (BEAST_EXPECT(!trap)) + { + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.contains("message and data too long")); + BEAST_EXPECT(!messages.contains(longMsg)); + } + } } { @@ -3496,15 +3551,20 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string data = "abc"; auto const slice = Slice(data.data(), data.size()); - // hfs.trace(msg, slice, false); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, slice.data(), slice.size()); - WasmValVec params(5), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace"), params, result, 0, msg.size(), 256, slice.size(), 0); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::AsText), + 256, + slice.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); auto const messages = sink.messages().str(); BEAST_EXPECT(messages.empty()); } @@ -3531,19 +3591,53 @@ struct HostFuncImpl_test : public beast::unit_test::Suite hfs.setRT(vrt); std::string const msg = "trace number"; - int64_t const num = 123456789; - // hfs.traceNum(msg, num); - vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); - WasmValVec params(3), result(1); - auto* trap = ww(&import.at("trace_num"), params, result, 0, msg.size(), num); + // adjustWasmEndianess is its own inverse, so writing the adjusted value + // lets the wrapper's adjustment recover it on either endianness. + auto const traceNum = [&](TraceDataType type, auto value) { + auto const wire = adjustWasmEndianess(value); + vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); + vrt.setBytes(256, reinterpret_cast(&wire), sizeof(wire)); + WasmValVec params(5), result(0); + auto* trap = + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(type), + 256, + sizeof(wire)); - if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0)) + if (BEAST_EXPECT(!trap)) + { + auto const messages = sink.messages().str(); + BEAST_EXPECT(messages.contains(msg)); + BEAST_EXPECT(messages.contains(std::to_string(value))); + } + }; + + traceNum(TraceDataType::Int64, int64_t{123456789}); + traceNum(TraceDataType::Int64, int64_t{-42}); + // Above int64 max -- unreachable through the old trace_num + traceNum(TraceDataType::Uint64, std::numeric_limits::max()); + + // Wrong buffer length for the type: logged as invalid, no trap { - auto const messages = sink.messages().str(); - BEAST_EXPECT(messages.contains(msg)); - BEAST_EXPECT(messages.contains(std::to_string(num))); + std::int32_t const tooShort = 7; + vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); + vrt.setBytes(256, reinterpret_cast(&tooShort), sizeof(tooShort)); + WasmValVec params(5), result(0); + auto* trap = + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Int64), + 256, + sizeof(tooShort)); + BEAST_EXPECT(!trap); } } @@ -3563,15 +3657,22 @@ struct HostFuncImpl_test : public beast::unit_test::Suite hfs.setRT(vrt); std::string const msg = "trace number"; - int64_t const num = 123456789; + auto const wire = adjustWasmEndianess(int64_t{123456789}); - // hfs.traceNum(msg, num); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); - WasmValVec params(3), result(1); - auto* trap = ww(&import.at("trace_num"), params, result, 0, msg.size(), num); + vrt.setBytes(256, reinterpret_cast(&wire), sizeof(wire)); + WasmValVec params(5), result(0); + auto* trap = + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Int64), + 256, + sizeof(wire)); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); auto const messages = sink.messages().str(); BEAST_EXPECT(messages.empty()); } @@ -3600,15 +3701,20 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string const msg = "trace account"; auto const& accountId = env.master.id(); - // hfs.traceAccount(msg, env.master.id()); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, accountId.data(), accountId.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_acct"), params, result, 0, msg.size(), 256, accountId.size()); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Account), + 256, + accountId.size()); - if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0)) + if (BEAST_EXPECT(!trap)) { auto const messages = sink.messages().str(); BEAST_EXPECT(messages.contains(msg)); @@ -3634,15 +3740,20 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string msg = "trace account"; auto const& accountId = env.master.id(); - // hfs.traceAccount(msg, env.master.id()); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, accountId.data(), accountId.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_acct"), params, result, 0, msg.size(), 256, accountId.size()); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Account), + 256, + accountId.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); auto const messages = sink.messages().str(); BEAST_EXPECT(messages.empty()); } @@ -3671,22 +3782,21 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string const msg = "trace amount"; STAmount const amount = XRP(12345); { - // hfs.traceAmount(msg, amount); Bytes amountBytes = toBytes(amount); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, amountBytes.data(), amountBytes.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_amt"), + ww(&import.at("trace"), params, result, 0, msg.size(), + traceDataTypeToInt(TraceDataType::Amount), 256, amountBytes.size()); - if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0)) + if (BEAST_EXPECT(!trap)) { auto const messages = sink.messages().str(); BEAST_EXPECT(messages.contains(msg)); @@ -3700,22 +3810,21 @@ struct HostFuncImpl_test : public beast::unit_test::Suite env.close(); STAmount const iouAmount = env.master["USD"](100); { - // hfs.traceAmount(msg, iouAmount); Bytes amountBytes = toBytes(iouAmount); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, amountBytes.data(), amountBytes.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_amt"), + ww(&import.at("trace"), params, result, 0, msg.size(), + traceDataTypeToInt(TraceDataType::Amount), 256, amountBytes.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); } // MPT amount @@ -3724,22 +3833,21 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Asset const mptAsset = Asset(mptId); STAmount const mptAmount(mptAsset, 123456); - // hfs.traceAmount(msg, mptAmount); Bytes amountBytes = toBytes(mptAmount); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, amountBytes.data(), amountBytes.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_amt"), + ww(&import.at("trace"), params, result, 0, msg.size(), + traceDataTypeToInt(TraceDataType::Amount), 256, amountBytes.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); } } @@ -3761,16 +3869,21 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string const msg = "trace amount"; STAmount const amount = XRP(12345); - // hfs.traceAmount(msg, amount); Bytes amountBytes = toBytes(amount); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, amountBytes.data(), amountBytes.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_amt"), params, result, 0, msg.size(), 256, amountBytes.size()); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Amount), + 256, + amountBytes.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); auto const messages = sink.messages().str(); BEAST_EXPECT(messages.empty()); } @@ -3882,33 +3995,37 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string const msg = "trace float"; { - // hfs.traceFloat(msg, makeSlice(invalid)); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, reinterpret_cast(invalid.data()), invalid.size()); - WasmValVec params(4), result(1); - auto* trap = ww( - &import.at("trace_xfloat"), params, result, 0, msg.size(), 256, invalid.size()); - - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); - } - - { - // hfs.traceFloat(msg, makeSlice(floatMaxExp)); - vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); - vrt.setBytes(256, floatMaxExp.data(), floatMaxExp.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_xfloat"), + ww(&import.at("trace"), params, result, 0, msg.size(), + traceDataTypeToInt(TraceDataType::Xfloat), + 256, + invalid.size()); + + BEAST_EXPECT(!trap); + } + + { + vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); + vrt.setBytes(256, floatMaxExp.data(), floatMaxExp.size()); + WasmValVec params(5), result(0); + auto* trap = + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Xfloat), 256, floatMaxExp.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); } } @@ -3929,15 +4046,20 @@ struct HostFuncImpl_test : public beast::unit_test::Suite std::string const msg = "trace float"; - // hfs.traceFloat(msg, makeSlice(invalid)); vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size()); vrt.setBytes(256, reinterpret_cast(invalid.data()), invalid.size()); - WasmValVec params(4), result(1); + WasmValVec params(5), result(0); auto* trap = - ww(&import.at("trace_xfloat"), params, result, 0, msg.size(), 256, invalid.size()); + ww(&import.at("trace"), + params, + result, + 0, + msg.size(), + traceDataTypeToInt(TraceDataType::Xfloat), + 256, + invalid.size()); - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + BEAST_EXPECT(!trap); auto const messages = sink.messages().str(); BEAST_EXPECT(messages.empty()); } @@ -6181,18 +6303,26 @@ struct HostFuncImpl_test : public beast::unit_test::Suite reinterpret_cast("dummy"), 5); // Empty data slice for trace { - WasmValVec params(5), result(1); - // trace(msg_ptr, msg_len, data_ptr, data_len, asHex) - auto* trap = ww(&import.at("trace"), params, result, 0, testMsg.size(), 100, 5, 0); + WasmValVec params(5), result(0); + // trace(msg_ptr, msg_len, data_type, data_ptr, data_len) -- returns nothing + auto* trap = + ww(&import.at("trace"), + params, + result, + 0, + testMsg.size(), + traceDataTypeToInt(TraceDataType::AsText), + 100, + 5); - // Should succeed even though message is >10 bytes, because trace only uses slices - // (no transfer limit check in getDataSlice) - BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) && - BEAST_EXPECT(result[0].of.i32 == 0); + // Should not trap even though the message is >10 bytes, because trace only reads + // slices (no transfer limit check in getDataSlice) and never charges the limit. + BEAST_EXPECT(!trap); } - // setData should return when transfer limit is exhausted - // After trace consumed overhead (1024 bytes), we have 10 - 1024 = negative limit left + // setData should return OutOfTransferLimit when the transfer limit is exhausted. + // trace left the limit untouched, so the next getTransferLimit() overhead (1024) + // takes 1034 down to 10 -- not enough for the 32-byte hash copy. { WasmValVec params(2), result(1); auto* trap = ww(&import.at("parent_ldgr_hash"), params, result, 500, 32); diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index a3ded89f33..5feeb25783 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -17,11 +17,8 @@ #include #include -#include - #include #include -#include #include #include @@ -382,54 +379,10 @@ public: #endif } - [[nodiscard]] std::expected - trace(std::string_view const& msg, Slice const& data, bool asHex) const override + void + trace(std::string_view const& msg, std::string_view const& data) const override { - if (!asHex) - { - log(msg, [&data] { - return std::string_view(reinterpret_cast(data.data()), data.size()); - }); - } - else - { - log(msg, [&data] { - std::string hex; - hex.reserve(data.size() * 2); - boost::algorithm::hex(data.begin(), data.end(), std::back_inserter(hex)); - return hex; - }); - } - - return 0; - } - - [[nodiscard]] std::expected - traceNum(std::string_view const& msg, int64_t data) const override - { - log(msg, [data] { return data; }); - return 0; - } - - [[nodiscard]] std::expected - traceAccount(std::string_view const& msg, AccountID const& account) const override - { - log(msg, [&account] { return toBase58(account); }); - return 0; - } - - [[nodiscard]] std::expected - traceFloat(std::string_view const& msg, Slice const& data) const override - { - log(msg, [&data] { return wasm_float::floatToString(data); }); - return 0; - } - - [[nodiscard]] std::expected - traceAmount(std::string_view const& msg, STAmount const& amount) const override - { - log(msg, [&amount] { return amount.getFullText(); }); - return 0; + log(msg, [&data] { return data; }); } [[nodiscard]] std::expected diff --git a/src/test/app/Wasm_test.cpp b/src/test/app/Wasm_test.cpp index 3a9f541153..c2a493de42 100644 --- a/src/test/app/Wasm_test.cpp +++ b/src/test/app/Wasm_test.cpp @@ -217,7 +217,7 @@ struct Wasm_test : public beast::unit_test::Suite auto re = engine.run( allHostFuncWasm, hfs, 1'000'000, escrowFunctionName, {}, imp, env.journal); - checkResult(re, 1, 27'617); + checkResult(re, 1, 30'760); env.close(); } @@ -239,7 +239,7 @@ struct Wasm_test : public beast::unit_test::Suite auto re = engine.run( allHostFuncWasm, hfs, 1'000'000, escrowFunctionName, {}, imp, env.journal); - checkResult(re, 1, 70'877); + checkResult(re, 1, 48'580); env.close(); } @@ -280,7 +280,7 @@ struct Wasm_test : public beast::unit_test::Suite { TestHostFunctions hfs(env); auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {}); - checkResult(re, 1, 70'877); + checkResult(re, 1, 48'580); } { @@ -304,7 +304,7 @@ struct Wasm_test : public beast::unit_test::Suite TestHostFunctions hfs(env); auto re = runEscrowWasm( allHFWasm, hfs, std::numeric_limits::max(), escrowFunctionName, {}); - checkResult(re, 1, 70'877); + checkResult(re, 1, 48'580); } { // fail because trying to access nonexistent field @@ -322,7 +322,7 @@ struct Wasm_test : public beast::unit_test::Suite FieldNotFoundHostFunctions hfs(env); auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {}); - checkResult(re, -201, 29'502); + checkResult(re, -201, 28'329); } { // fail because trying to allocate more than MAX_PAGES memory @@ -340,7 +340,7 @@ struct Wasm_test : public beast::unit_test::Suite OversizedFieldHostFunctions hfs(env); auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {}); - checkResult(re, -201, 29'502); + checkResult(re, -201, 28'329); } } @@ -356,7 +356,7 @@ struct Wasm_test : public beast::unit_test::Suite auto const codecovWasm = hexToBytes(kCodecovTestsWasmHex); TestHostFunctions hfs(env); - auto const allowance = 204'624; + auto const allowance = 125'667; auto re = runEscrowWasm(codecovWasm, hfs, allowance, escrowFunctionName, {}); checkResult(re, 1, allowance); diff --git a/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock b/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock index 48771d1506..5240e9b0f0 100644 --- a/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock +++ b/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock @@ -6,16 +6,17 @@ version = 4 name = "all_host_functions" version = "0.1.0" dependencies = [ - "xrpl-wasm-stdlib", + "xrpl-common-stdlib", + "xrpl-escrow-stdlib", ] [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -34,42 +35,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] name = "digest" -version = "0.10.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "hybrid-array" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", - "version_check", ] [[package]] @@ -98,9 +104,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", @@ -109,9 +115,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -146,26 +152,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +name = "xrpl-common-stdlib" +version = "0.8.0" +source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9" +dependencies = [ + "xrpl-macros", +] + +[[package]] +name = "xrpl-escrow-stdlib" +version = "0.1.0" +source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9" +dependencies = [ + "xrpl-common-stdlib", +] [[package]] name = "xrpl-macros" version = "0.1.0" -source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#9822d645870908a79d87a57b0244caa6359cb9cf" +source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9" dependencies = [ "bs58", + "proc-macro2", "quote", "sha2", "syn", ] - -[[package]] -name = "xrpl-wasm-stdlib" -version = "0.8.0" -source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#9822d645870908a79d87a57b0244caa6359cb9cf" -dependencies = [ - "xrpl-macros", -] diff --git a/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml b/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml index fb0c44562a..71ad3ba9c6 100644 --- a/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml +++ b/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml @@ -10,7 +10,8 @@ edition = "2024" crate-type = ["cdylib"] [dependencies] -xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-wasm-stdlib", branch = "renames" } +xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-common-stdlib", branch = "error-and-trace" } +xrpl-escrow = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-escrow-stdlib", branch = "error-and-trace" } [profile.dev] panic = "abort" diff --git a/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs b/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs index a40aa91d6a..586e950e39 100644 --- a/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs +++ b/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs @@ -25,10 +25,11 @@ extern crate std; // -700 to -799: Data Update Functions (1 function) // -use xrpl_std::core::current_tx::escrow_finish::EscrowFinish; -use xrpl_std::core::current_tx::traits::TransactionCommonFields; +use xrpl_escrow::current_tx::escrow_finish::EscrowFinish; +use xrpl_std::current_tx::traits::TransactionCommonFields; use xrpl_std::host; -use xrpl_std::host::trace::{trace, trace_account_buf, trace_data, trace_num, DataRepr}; +use xrpl_std::host::trace::TraceDataType; +use xrpl_std::host::trace::{trace, trace_acct_buf, trace_hex, trace_num}; use xrpl_std::sfield; #[unsafe(no_mangle)] @@ -131,7 +132,7 @@ fn test_ledger_header_functions() -> i32 { ); return -103; // Parent ledger hash test failed - should be exactly 32 bytes } - let _ = trace_data("Parent ledger hash:", &hash_buffer, DataRepr::AsHex); + let _ = trace_hex("Parent ledger hash:", &hash_buffer); let _ = trace("SUCCESS: Ledger header functions"); 0 @@ -160,7 +161,7 @@ fn test_transaction_data_functions() -> i32 { ); return -201; // Basic transaction field test failed } - let _ = trace_account_buf("Transaction Account:", &account_buffer); + let _ = trace_acct_buf("Transaction Account:", &account_buffer); // Test with Fee field (XRP amount - 8 bytes in new serialized format) // New format: XRP amounts are always 8 bytes (positive: value | cPositive flag, negative: just value) @@ -181,11 +182,7 @@ fn test_transaction_data_functions() -> i32 { return -202; // Fee field test failed - XRP amounts should be exactly 8 bytes } let _ = trace_num("Transaction Fee length:", fee_len as i64); - let _ = trace_data( - "Transaction Fee (serialized XRP amount):", - &fee_buffer, - DataRepr::AsHex, - ); + let _ = trace_hex("Transaction Fee (serialized XRP amount):", &fee_buffer); // Test with Sequence field (required, 4 bytes uint32) let mut seq_buffer = [0u8; 4]; @@ -204,7 +201,7 @@ fn test_transaction_data_functions() -> i32 { ); return -203; // Sequence field test failed } - let _ = trace_data("Transaction Sequence:", &seq_buffer, DataRepr::AsHex); + let _ = trace_hex("Transaction Sequence:", &seq_buffer); // NOTE: get_tx_field2() through get_tx_field6() have been deprecated. // Use get_tx_field() with appropriate parameters for all transaction field access. @@ -231,11 +228,7 @@ fn test_transaction_data_functions() -> i32 { // Expected - locator may not match transaction structure } else { let _ = trace_num("Nested field length:", nested_result as i64); - let _ = trace_data( - "Nested field:", - &nested_buffer[..nested_result as usize], - DataRepr::AsHex, - ); + let _ = trace_hex("Nested field:", &nested_buffer[..nested_result as usize]); } // Test 2.3: get_tx_array_len() - Get array length @@ -288,20 +281,18 @@ fn test_current_ledger_object_functions() -> i32 { "Current object balance length (XRP amount):", balance_result as i64, ); - let _ = trace_data( + let _ = trace_hex( "Current object balance (serialized XRP amount):", &balance_buffer, - DataRepr::AsHex, ); } else { let _ = trace_num( "Current object balance length (non-XRP amount):", balance_result as i64, ); - let _ = trace_data( + let _ = trace_hex( "Current object balance:", &balance_buffer[..balance_result as usize], - DataRepr::AsHex, ); } @@ -321,7 +312,7 @@ fn test_current_ledger_object_functions() -> i32 { current_account_result as i64, ); } else { - let _ = trace_account_buf("Current ledger object account:", ¤t_account_buffer); + let _ = trace_acct_buf("Current ledger object account:", ¤t_account_buffer); } // Test 3.2: get_current_ledger_obj_nested_field() - Nested field access @@ -345,10 +336,9 @@ fn test_current_ledger_object_functions() -> i32 { ); } else { let _ = trace_num("Current nested field length:", current_nested_result as i64); - let _ = trace_data( + let _ = trace_hex( "Current nested field:", ¤t_nested_buffer[..current_nested_result as usize], - DataRepr::AsHex, ); } @@ -505,20 +495,18 @@ fn test_any_ledger_object_functions() -> i32 { "Cached object balance length (XRP amount):", cached_balance_result as i64, ); - let _ = trace_data( + let _ = trace_hex( "Cached object balance (serialized XRP amount):", &cached_balance_buffer, - DataRepr::AsHex, ); } else { let _ = trace_num( "Cached object balance length (non-XRP amount):", cached_balance_result as i64, ); - let _ = trace_data( + let _ = trace_hex( "Cached object balance:", &cached_balance_buffer[..cached_balance_result as usize], - DataRepr::AsHex, ); } @@ -544,10 +532,9 @@ fn test_any_ledger_object_functions() -> i32 { ); } else { let _ = trace_num("Cached nested field length:", cached_nested_result as i64); - let _ = trace_data( + let _ = trace_hex( "Cached nested field:", &cached_nested_buffer[..cached_nested_result as usize], - DataRepr::AsHex, ); } @@ -604,7 +591,7 @@ fn test_keylet_generation_functions() -> i32 { ); return -501; // Account keylet generation failed } - let _ = trace_data("Account keylet:", &accountroot_id_buffer, DataRepr::AsHex); + let _ = trace_hex("Account keylet:", &accountroot_id_buffer); // Test 5.2: credential_keylet() - Generate keylet for credential let mut credential_keylet_buffer = [0u8; 32]; @@ -628,10 +615,9 @@ fn test_keylet_generation_functions() -> i32 { ); // This is expected to fail due to unusual parameter types } else { - let _ = trace_data( + let _ = trace_hex( "Credential keylet:", &credential_keylet_buffer[..credential_keylet_result as usize], - DataRepr::AsHex, ); } @@ -654,7 +640,7 @@ fn test_keylet_generation_functions() -> i32 { let _ = trace_num("ERROR: escrow_keylet failed:", escrow_keylet_result as i64); return -503; // Escrow keylet generation failed } - let _ = trace_data("Escrow keylet:", &escrow_keylet_buffer, DataRepr::AsHex); + let _ = trace_hex("Escrow keylet:", &escrow_keylet_buffer); // Test 5.4: oracle_keylet() - Generate keylet for oracle let mut oracle_keylet_buffer = [0u8; 32]; @@ -675,7 +661,7 @@ fn test_keylet_generation_functions() -> i32 { let _ = trace_num("ERROR: oracle_keylet failed:", oracle_keylet_result as i64); return -504; // Oracle keylet generation failed } - let _ = trace_data("Oracle keylet:", &oracle_keylet_buffer, DataRepr::AsHex); + let _ = trace_hex("Oracle keylet:", &oracle_keylet_buffer); let _ = trace("SUCCESS: Keylet generation functions"); 0 @@ -702,8 +688,8 @@ fn test_utility_functions() -> i32 { let _ = trace_num("ERROR: compute_sha512_half failed:", hash_result as i64); return -601; // SHA512 half computation failed } - let _ = trace_data("Input data:", test_data, DataRepr::AsHex); - let _ = trace_data("SHA512 half hash:", &hash_output, DataRepr::AsHex); + let _ = trace_hex("Input data:", test_data); + let _ = trace_hex("SHA512 half hash:", &hash_output); // Test 6.2: get_nft() - NFT data retrieval let escrow_finish = EscrowFinish; @@ -729,46 +715,25 @@ fn test_utility_functions() -> i32 { // This is expected - test account likely doesn't own the dummy NFT } else { let _ = trace_num("NFT data length:", nft_result as i64); - let _ = trace_data( - "NFT data:", - &nft_buffer[..nft_result as usize], - DataRepr::AsHex, - ); + let _ = trace_hex("NFT data:", &nft_buffer[..nft_result as usize]); } // Test 6.3: trace() - Debug logging with data let trace_message = b"Test trace message"; let trace_data_payload = b"payload"; - let trace_result = unsafe { + unsafe { host::trace( trace_message.as_ptr(), trace_message.len(), + TraceDataType::AsHex as i32, trace_data_payload.as_ptr(), trace_data_payload.len(), - 1, // as_hex = true ) }; - if trace_result < 0 { - let _ = trace_num("ERROR: trace() failed:", trace_result as i64); - return -603; // Trace function failed - } - let _ = trace_num("Trace function bytes written:", trace_result as i64); - // Test 6.4: trace_num() - Debug logging with number let test_number = 42i64; - let trace_num_result = trace_num("Test number trace", test_number); - - use xrpl_std::host::Result; - match trace_num_result { - Result::Ok(_) => { - let _ = trace_num("Trace_num function succeeded", 0); - } - Result::Err(_) => { - let _ = trace_num("ERROR: trace_num() failed:", -604); - return -604; // Trace number function failed - } - } + trace_num("Test number trace", test_number); let _ = trace("SUCCESS: Utility functions"); 0 @@ -789,11 +754,7 @@ fn test_data_update_functions() -> i32 { return -701; // Data update failed } - let _ = trace_data( - "Successfully updated ledger entry with:", - update_payload, - DataRepr::AsHex, - ); + let _ = trace_hex("Successfully updated ledger entry with:", update_payload); let _ = trace("SUCCESS: Data update functions"); 0 } diff --git a/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock b/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock index d7d91db071..899f278196 100644 --- a/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock +++ b/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock @@ -4,11 +4,11 @@ version = 4 [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -30,46 +30,52 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "codecov_tests" version = "0.0.1" dependencies = [ - "xrpl-wasm-stdlib", + "xrpl-common-stdlib", + "xrpl-escrow-stdlib", ] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] name = "digest" -version = "0.10.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer", + "const-oid", "crypto-common", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "hybrid-array" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", - "version_check", ] [[package]] @@ -98,9 +104,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", @@ -109,9 +115,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -146,26 +152,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +name = "xrpl-common-stdlib" +version = "0.8.0" +source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9" +dependencies = [ + "xrpl-macros", +] + +[[package]] +name = "xrpl-escrow-stdlib" +version = "0.1.0" +source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9" +dependencies = [ + "xrpl-common-stdlib", +] [[package]] name = "xrpl-macros" version = "0.1.0" -source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#9822d645870908a79d87a57b0244caa6359cb9cf" +source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9" dependencies = [ "bs58", + "proc-macro2", "quote", "sha2", "syn", ] - -[[package]] -name = "xrpl-wasm-stdlib" -version = "0.8.0" -source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#9822d645870908a79d87a57b0244caa6359cb9cf" -dependencies = [ - "xrpl-macros", -] diff --git a/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml b/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml index 1cc49ac490..1e388a5154 100644 --- a/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml +++ b/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml @@ -15,4 +15,5 @@ opt-level = 's' panic = "abort" [dependencies] -xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-wasm-stdlib", branch = "renames" } +xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-common-stdlib", branch = "error-and-trace" } +xrpl-escrow = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-escrow-stdlib", branch = "error-and-trace" } diff --git a/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs b/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs index 6204dff0a2..7b42f747a8 100644 --- a/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs +++ b/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs @@ -43,5 +43,14 @@ unsafe extern "C" { out_buff_len: usize, ) -> i32; - pub fn trace_num(msg_read_ptr: i32, msg_read_len: i32, number: i64) -> i32; + // Same wasm functype as the real binding, so this is not a second import of + // host_lib.trace. Loose i32 pointers exercise the out-of-bounds path. + #[link_name = "trace"] + pub fn trace_loose( + msg_read_ptr: i32, + msg_read_len: i32, + data_type: i32, + data_read_ptr: i32, + data_read_len: i32, + ); } diff --git a/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs b/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs index 59f16155d2..02b38f633e 100644 --- a/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs +++ b/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs @@ -4,19 +4,20 @@ extern crate std; use core::panic; -use xrpl_std::core::current_tx::escrow_finish::{get_current_escrow_finish, EscrowFinish}; -use xrpl_std::core::current_tx::traits::TransactionCommonFields; -use xrpl_std::core::keylets; -use xrpl_std::core::locator::Locator; -use xrpl_std::core::types::blob::DEFAULT_BLOB_SIZE; -use xrpl_std::core::types::issue::Issue; -use xrpl_std::core::types::issue::XrpIssue; -use xrpl_std::core::types::mpt_id::MptId; +use xrpl_escrow::current_tx::escrow_finish::{EscrowFinish, get_current_escrow_finish}; +use xrpl_std::current_tx::traits::TransactionCommonFields; +use xrpl_std::fields::locator::Locator; use xrpl_std::host; use xrpl_std::host::error_codes; +use xrpl_std::host::trace::TraceDataType; use xrpl_std::host::trace::{trace, trace_num as trace_number}; +use xrpl_std::ledger_entry_ids; use xrpl_std::sfield; -use xrpl_std::types::XRPL_CONTRACT_DATA_SIZE; +use xrpl_std::types::blob::DEFAULT_BLOB_SIZE; +use xrpl_std::types::contract_data::XRPL_CONTRACT_DATA_SIZE; +use xrpl_std::types::issue::Issue; +use xrpl_std::types::issue::XrpIssue; +use xrpl_std::types::mpt_id::MptId; mod host_bindings_loose; include!("host_bindings_loose.rs"); @@ -91,7 +92,7 @@ pub extern "C" fn escrow_finish() -> i32 { ); let tx: EscrowFinish = get_current_escrow_finish(); let account = tx.get_account().unwrap_or_panic(); // get_tx_field under the hood - let keylet = keylets::accountroot_id(&account).unwrap_or_panic(); // accountroot_id under the hood + let keylet = ledger_entry_ids::accountroot_id(&account).unwrap_or_panic(); // accountroot_id under the hood check_result( unsafe { host::cache_le(keylet.as_ptr(), keylet.len(), 0) }, 1, @@ -243,44 +244,35 @@ pub extern "C" fn escrow_finish() -> i32 { ) }); let message = "testing trace"; - check_result( - unsafe { - host::trace_acct( - message.as_ptr(), - message.len(), - account.0.as_ptr(), - account.0.len(), - ) - }, - 0, - "trace_acct", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Account as i32, + account.0.as_ptr(), + account.0.len(), + ) + }; let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F]; // 95 drops of XRP - check_result( - unsafe { - host::trace_amt( - message.as_ptr(), - message.len(), - amount.as_ptr(), - amount.len(), - ) - }, - 0, - "trace_amt", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Amount as i32, + amount.as_ptr(), + amount.len(), + ) + }; let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // 0 drops of XRP - check_result( - unsafe { - host::trace_amt( - message.as_ptr(), - message.len(), - amount.as_ptr(), - amount.len(), - ) - }, - 0, - "trace_amt_zero", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Amount as i32, + amount.as_ptr(), + amount.len(), + ) + }; // ######################################## // Step #2: Test set_data edge cases @@ -589,18 +581,17 @@ pub extern "C" fn escrow_finish() -> i32 { ) }); - // string - check_result( - unsafe { - host_bindings_loose::trace_num( - locator.as_ptr() as i32 + 1_000_000_000, - locator.len() as i32, - 42, - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_num_oob_str", - ); + // Out-of-bounds message pointer; nothing to assert on now that trace is void. + let num_bytes = 42i64.to_le_bytes(); + unsafe { + host_bindings_loose::trace_loose( + locator.as_ptr() as i32 + 1_000_000_000, + locator.len() as i32, + TraceDataType::Int64 as i32, + num_bytes.as_ptr() as i32, + num_bytes.len() as i32, + ) + }; // ######################################## // Step #4: Test other host function edge cases @@ -798,44 +789,34 @@ pub extern "C" fn escrow_finish() -> i32 { "mptoken_id_too_big_slice_mptid", ) }); - check_result( - unsafe { - host::trace( - message.as_ptr(), - message.len(), - locator.as_ptr().wrapping_add(1_000_000_000), - locator.len(), - 0, - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_oob_slice", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::AsText as i32, + locator.as_ptr().wrapping_add(1_000_000_000), + locator.len(), + ) + }; let float: [u8; 8] = [0xD4, 0x83, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00]; - check_result( - unsafe { - host::trace_xfloat( - message.as_ptr(), - message.len(), - float.as_ptr().wrapping_add(1_000_000_000), - float.len(), - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_xfloat_oob_slice", - ); - check_result( - unsafe { - host::trace_amt( - message.as_ptr(), - message.len(), - locator.as_ptr().wrapping_add(1_000_000_000), - locator.len(), - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_amt_oob_slice", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Xfloat as i32, + float.as_ptr().wrapping_add(1_000_000_000), + float.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Amount as i32, + locator.as_ptr().wrapping_add(1_000_000_000), + locator.len(), + ) + }; check_result( unsafe { host::float_cmp( @@ -1607,129 +1588,116 @@ pub extern "C" fn escrow_finish() -> i32 { "nft_uri_wrong_size_account_id", ) }); - check_result( - unsafe { - host::trace_acct( - message.as_ptr(), - message.len(), - locator.as_ptr(), - locator.len(), - ) - }, - error_codes::INVALID_PARAMS, - "trace_acct_wrong_size_account_id", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Account as i32, + locator.as_ptr(), + locator.len(), + ) + }; // invalid Currency was already tested above // invalid string - check_result( - unsafe { - host::trace( - message.as_ptr().wrapping_add(1_000_000_000), - message.len(), - uint256.as_ptr(), - uint256.len(), - 0, - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_oob_string", - ); - check_result( - unsafe { - host::trace_xfloat( - message.as_ptr().wrapping_add(1_000_000_000), - message.len(), - float.as_ptr(), - float.len(), - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_xfloat_oob_string", - ); - check_result( - unsafe { - host::trace_acct( - message.as_ptr().wrapping_add(1_000_000_000), - message.len(), - account.0.as_ptr(), - account.0.len(), - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_acct_oob_string", - ); - check_result( - unsafe { - host::trace_amt( - message.as_ptr().wrapping_add(1_000_000_000), - message.len(), - amount.as_ptr(), - amount.len(), - ) - }, - error_codes::POINTER_OUT_OF_BOUNDS, - "trace_amt_oob_string", - ); + unsafe { + host::trace( + message.as_ptr().wrapping_add(1_000_000_000), + message.len(), + TraceDataType::AsText as i32, + uint256.as_ptr(), + uint256.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr().wrapping_add(1_000_000_000), + message.len(), + TraceDataType::Xfloat as i32, + float.as_ptr(), + float.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr().wrapping_add(1_000_000_000), + message.len(), + TraceDataType::Account as i32, + account.0.as_ptr(), + account.0.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr().wrapping_add(1_000_000_000), + message.len(), + TraceDataType::Amount as i32, + amount.as_ptr(), + amount.len(), + ) + }; // trace too large - check_result( - unsafe { - host::trace( - locator.as_ptr(), - locator.len(), - locator.as_ptr(), - long_len, - 0, - ) - }, - error_codes::DATA_FIELD_TOO_LARGE, - "trace_too_long", - ); - check_result( - unsafe { host::trace_num(locator.as_ptr(), long_len, 1) }, - error_codes::DATA_FIELD_TOO_LARGE, - "trace_num_too_long", - ); - check_result( - unsafe { host::trace_xfloat(message.as_ptr(), long_len, float.as_ptr(), float.len()) }, - error_codes::DATA_FIELD_TOO_LARGE, - "trace_xfloat_too_long", - ); - check_result( - unsafe { - host::trace_acct( - message.as_ptr(), - long_len, - account.0.as_ptr(), - account.0.len(), - ) - }, - error_codes::DATA_FIELD_TOO_LARGE, - "trace_acct_too_long", - ); - check_result( - unsafe { host::trace_amt(message.as_ptr(), long_len, amount.as_ptr(), amount.len()) }, - error_codes::DATA_FIELD_TOO_LARGE, - "trace_amt_too_long", - ); + unsafe { + host::trace( + locator.as_ptr(), + locator.len(), + TraceDataType::AsText as i32, + locator.as_ptr(), + long_len, + ) + }; + let too_long_num = 1i64.to_le_bytes(); + unsafe { + host::trace( + locator.as_ptr(), + long_len, + TraceDataType::Int64 as i32, + too_long_num.as_ptr(), + too_long_num.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr(), + long_len, + TraceDataType::Xfloat as i32, + float.as_ptr(), + float.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr(), + long_len, + TraceDataType::Account as i32, + account.0.as_ptr(), + account.0.len(), + ) + }; + unsafe { + host::trace( + message.as_ptr(), + long_len, + TraceDataType::Amount as i32, + amount.as_ptr(), + amount.len(), + ) + }; // trace amount errors - check_result( - unsafe { - host::trace_amt( - message.as_ptr(), - message.len(), - locator.as_ptr(), - locator.len(), - ) - }, - error_codes::INVALID_PARAMS, - "trace_amt_wrong_length", - ); + unsafe { + host::trace( + message.as_ptr(), + message.len(), + TraceDataType::Amount as i32, + locator.as_ptr(), + locator.len(), + ) + }; // other misc errors @@ -1749,34 +1717,28 @@ pub extern "C" fn escrow_finish() -> i32 { "mptoken_id_mptid_wrong_length", ) }); - check_result( - unsafe { - host::trace( - message.as_ptr(), - message.len(), - locator.as_ptr(), - locator.len(), - 2, - ) - }, - error_codes::INVALID_PARAMS, - "trace_invalid_as_hex", - ); + // Unknown data_type: the host logs "invalid arguments" and returns. + unsafe { + host::trace( + message.as_ptr(), + message.len(), + 99, + locator.as_ptr(), + locator.len(), + ) + }; // ensure that the Slice index desync issue is fixed let empty: &[u8] = b""; - check_result( - unsafe { - host::trace_acct( - empty.as_ptr(), - empty.len(), - account.0.as_ptr(), - account.0.len(), - ) - }, - 0, - "trace_acct_check_desync", - ); + unsafe { + host::trace( + empty.as_ptr(), + empty.len(), + TraceDataType::Account as i32, + account.0.as_ptr(), + account.0.len(), + ) + }; 1 // <-- If we get here, finish the escrow. } diff --git a/src/test/app/wasm_fixtures/fixtures.cpp b/src/test/app/wasm_fixtures/fixtures.cpp index 6ff902717e..363da88f8d 100644 --- a/src/test/app/wasm_fixtures/fixtures.cpp +++ b/src/test/app/wasm_fixtures/fixtures.cpp @@ -20,178 +20,184 @@ extern std::string const kLedgerSqnWasmHex = "7565"; extern std::string const kAllHostFunctionsWasmHex = - "0061736d0100000001540c60027f7f017f60037f7f7f017f60047f7f7f7f017f60017f017f60067f7f7f7f7f7f017f" - "60037f7f7f0060057f7f7f7f7f017f60037f7f7e017f60087f7f7f7f7f7f7f7f017f60017f0060027f7f006000017f" - "02dc041a08686f73745f6c69620874785f6669656c64000108686f73745f6c69620974726163655f6e756d00070868" - "6f73745f6c6962057472616365000608686f73745f6c69620a6c6467725f696e646578000008686f73745f6c696210" - "706172656e745f6c6467725f74696d65000008686f73745f6c696210706172656e745f6c6467725f68617368000008" - "686f73745f6c69620874785f696e6e6572000208686f73745f6c69620a74785f6172725f6c656e000308686f73745f" - "6c69621074785f696e6e65725f6172725f6c656e000008686f73745f6c69620d686f6d655f6c655f6669656c640001" - "08686f73745f6c69620d686f6d655f6c655f696e6e6572000208686f73745f6c69620f686f6d655f6c655f6172725f" - "6c656e000308686f73745f6c696215686f6d655f6c655f696e6e65725f6172725f6c656e000008686f73745f6c6962" - "0863616368655f6c65000108686f73745f6c69620d63726564656e7469616c5f6964000808686f73745f6c69620965" - "7363726f775f6964000408686f73745f6c6962096f7261636c655f6964000408686f73745f6c69620b736861353132" - "5f68616c66000208686f73745f6c6962076e66745f757269000408686f73745f6c6962087365745f64617461000008" - "686f73745f6c6962086c655f6669656c64000208686f73745f6c6962086c655f696e6e6572000608686f73745f6c69" - "620a6c655f6172725f6c656e000008686f73745f6c6962106c655f696e6e65725f6172725f6c656e000108686f7374" - "5f6c69620e6163636f756e74726f6f745f6964000208686f73745f6c69620a74726163655f616363740002030c0b09" - "0a05050b05000101030005030100110619037f01418080c0000b7f0041af99c0000b7f0041b099c0000b073504066d" - "656d6f727902000d657363726f775f66696e697368001e0a5f5f646174615f656e6403010b5f5f686561705f626173" - "6503020a911e0b990101027f230041306b220124002000027f418180202001411c6a4114100022024114470440417f" - "20022002417f4e1b210241010c010b200020012f001c3b0001200041036a2001411e6a2d00003a0000200120012900" - "233703082001200141286a29000037000d200128001f21022000410d6a200129000d37000020002001290308370208" - "41000b3a000020002002360204200141306a24000b460020012d00004101460440418080c000410b20013402041001" - "000b20002001290001370000200041106a200141116a280000360000200041086a200141096a2900003700000b1900" - "200241094f0440000b20002002360204200020013602000b1900200241214f0440000b200020023602042000200136" - "02000ba91b01097f230041b0036b22002400418b80c000411b41014100410010021a41a680c0004119410141004100" - "10021a41e780c000412b41014100410010021a2000410036027002400240024002400240024002400240200041f000" - "6a220741041003220141004a0440419281c00041172000280270220141187420014180fe0371410874722001410876" - "4180fe037120014118767272ad10011a200041003602900120004190016a220341041004220141004c0d0141a981c0" - "004113200028029001220141187420014180fe03714108747220014108764180fe037120014118767272ad10011a20" - "0041c8016a22024200370300200041c0016a22054200370300200041b8016a22044200370300200042003703b00120" - "0041b0016a22064120100522014120470d0241bc81c000411320064120410110021a41cf81c0004120410141004100" - "10021a41dc82c000412e41014100410010021a200041a0016a410036020020004198016a4200370300200042003703" - "90014181802020034114100022014114470d03418a83c00041142003101f2000420037034841888018200041c8006a" - "22034108100022014108470d04419e83c0004117420810011a41b583c000412820034108410110021a200041003602" - "3041848008200041306a22034104100022014104470d0541dd83c000411520034104410110021a200041f4006a4100" - "36000020004100360071200041013a0070200242003703002005420037030020044200370300200042003703b00102" - "4020074108200641201006220141004e044041f283c00041142001ad10011a200041286a20062001101d418684c000" - "410d2000280228200028022c410110021a0c010b419384c00041292001ac10011a0b41bc84c00041154183803c1007" - "ac10011a41d184c00041134189803c1007ac10011a0240200041f0006a41081008220141004e044041e484c0004114" - "2001ad10011a0c010b41f884c000412d2001ac10011a0b41a585c000412341014100410010021a41de86c000413341" - "014100410010021a2000420037034841828018200041c8006a220141081009220341004c0d06200341084604404191" - "87c000412b420810011a41bc87c000412f20014108410110021a0c080b41eb87c000412f2003ad10011a200041206a" - "200041c8006a2003101c419a88c000411720002802202000280224410110021a0c070b41bf82c000411d2001ac1001" - "1a419b7f21020c070b419a82c00041252001ac10011a419a7f21020c060b41ef81c000412b2001ac10011a41997f21" - "020c050b41b486c000412a2001ac10011a41b77e21020c040b41f385c00041c1002001ac10011a41b67e21020c030b" - "41c885c000412b2001ac10011a41b57e21020c020b41b188c00041c5002003ac10011a0b200041a0016a4100360200" - "20004198016a4200370300200042003703900102404181802020004190016a220341141009220141004a044041f688" - "c000411e2003101f0c010b419489c00041332001ac10011a0b200041f4006a41003600002000410036007120004101" - "3a0070200041c8016a4200370300200041c0016a4200370300200041b8016a4200370300200042003703b001024020" - "0041f0006a4108200041b0016a22014120100a220341004e044041c789c000411c2003ad10011a200041186a200120" - "03101d41e389c00041152000280218200028021c410110021a0c010b41f889c00041392003ac10011a0b41b18ac000" - "41244183803c100bac10011a0240200041f0006a4108100c220141004e044041d58ac000411c2001ad10011a0c010b" - "41f18ac000413d2001ac10011a0b41ae8bc000412841014100410010021a41d68bc000412f41014100410010021a20" - "0041b0016a2203101a200041f0006a22012003101b200041a8016a4200370300200041a0016a420037030020004198" - "016a4200370300200042003703900102400240024002400240200120004190016a2203102022014120460440200341" - "204100100d220441004a044041858cc00041232004ad10011a200042003703302004200041306a2201410810212203" - "41004c0d022003410846044041a88cc000412a420810011a41d28cc000412e20014108410110021a0c060b41808dc0" - "00412e2003ad10011a200041106a200041306a2003101c41ae8dc000411620002802102000280214410110021a0c05" - "0b41e68fc000413c2004ac10011a200041c8016a4200370300200041c0016a4200370300200041b8016a4200370300" - "200042003703b0014101200041b0016a4120102122014100480d020c030b41ba92c000412e2001ac10011a41ef7c21" - "020c050b41c48dc000412b2003ac10011a0c020b41a290c00041c1002001ac10011a0b200041cc006a410036000020" - "004100360049200041013a00484101200041c8006a200041b0016a10222201410048044041e390c00041352001ac10" - "011a0b4101102322014100480440419891c00041322001ac10011a0b4101200041c8006a10242201410048044041ca" - "91c00041392001ac10011a0b418392c000413741014100410010021a0c010b200041cc006a41003600002000410036" - "0049200041013a0048200041c8016a4200370300200041c0016a4200370300200041b8016a42003703002000420037" - "03b00102402004200041c8006a200041b0016a22011022220341004e044041ef8dc000411b2003ad10011a20004108" - "6a20012003101d418a8ec00041142000280208200028020c410110021a0c010b419e8ec00041312003ac10011a0b41" - "cf8ec000412320041023ac10011a02402004200041c8006a1024220141004e044041f28ec000411b2001ad10011a0c" - "010b418d8fc00041352001ac10011a0b41c28fc000412441014100410010021a0b41e892c000412f41014100410010" - "021a200041b0016a2201101a200041306a22042001101b200041e0006a4200370300200041d8006a42003703002000" - "41d0006a420037030020004200370348024002400240024002402004200041c8006a22031020220141204604404197" - "93c000410f20034120410110021a20004188016a420037030020004180016a4200370300200041f8006a4200370300" - "200042003703700240200441142004411441a693c0004109200041f0006a22014120100e220341004a044020002001" - "2003101d41ae93c000411220002802002000280204410110021a0c010b41c093c000413c2003ac10011a0b200041a8" - "016a22064200370300200041a0016a2202420037030020004198016a22054200370300200042003703900120004180" - "808cc07e360268200041306a22034114200041e8006a410420004190016a22084120100f22014120470d0141fc93c0" - "00410e20084120410110021a200041c8016a4200370300200041c0016a4200370300200041b8016a42003703002000" - "42003703b001200041808080d00236026c20034114200041ec006a4104200041b0016a22044120101022014120470d" - "02418a94c000410e20044120410110021a419894c000412441014100410010021a419195c000412541014100410010" - "021a20004188016a420037030020004180016a4200370300200041f8006a42003703002000420037037041b695c000" - "4117200041f0006a22034120101122014120470d0341cd95c000410b41b695c0004117410110021a41d895c0004111" - "20034120410110021a2004101a200041c8006a22072004101b20064200370300200242003703002005420037030020" - "0042003703900102404100200422026b410371220320026a220520024d0d0020030440200321010340200241003a00" - "00200241016a2102200141016b22010d000b0b200341016b4107490d000340200241003a0000200241076a41003a00" - "00200241066a41003a0000200241056a41003a0000200241046a41003a0000200241036a41003a0000200241026a41" - "003a0000200241016a41003a0000200241086a22022005470d000b0b200541800220036b2201417c716a220220054b" - "0440034020054100360200200541046a22052002490d000b0b024020022001410371220120026a22034f0d00200122" - "0504400340200241003a0000200241016a2102200541016b22050d000b0b200141016b4107490d000340200241003a" - "0000200241076a41003a0000200241066a41003a0000200241056a41003a0000200241046a41003a0000200241036a" - "41003a0000200241026a41003a0000200241016a41003a0000200241086a22022003470d000b0b0240200741142008" - "412020044180021012220141004a044041e995c00041102001ad10011a20014181024f0d0641f995c0004109200420" - "01410110021a0c010b418296c000412e2001ac10011a0b41b096c000411241c296c00041074101100222014100480d" - "0541c996c000411d2001ad10011a41e696c0004111422a1001410048044041ad97c000411a42a47b10011a41a47b21" - "020c070b41f796c000411c420010011a41012102419397c000411a41014100410010021a41ff97c000412941014100" - "410010021a41a898c000412810132201412846044041d098c000412741a898c0004128410110021a41f798c000411e" - "41014100410010021a41bf80c000412841014100410010021a0c070b419599c000411a2001ac10011a41c37a21020c" - "060b41f494c000411d2001ac10011a418b7c21020c050b41d894c000411c2001ac10011a41897c21020c040b41bc94" - "c000411c2001ac10011a41887c21020c030b41dd97c00041222001ac10011a41a77b21020c020b000b41c797c00041" - "162001ac10011a41a57b21020b200041b0036a240020020b0d00200020012002411410191a0b0c0020004114200141" - "2010180b0e002000418280182001200210140b0e002000200141082002412010150b0a0020004183803c10160b0a00" - "20002001410810170b0bb9190100418080c0000baf196572726f725f636f64653d3d3d3d20484f53542046554e4354" - "494f4e532054455354203d3d3d54657374696e6720323620686f73742066756e6374696f6e73535543434553533a20" - "416c6c20686f73742066756e6374696f6e20746573747320706173736564212d2d2d2043617465676f727920313a20" - "4c6564676572204865616465722046756e6374696f6e73202d2d2d4c65646765722073657175656e6365206e756d62" - "65723a506172656e74206c65646765722074696d653a506172656e74206c656467657220686173683a535543434553" - "533a204c6564676572206865616465722066756e6374696f6e734552524f523a206765745f706172656e745f6c6564" - "6765725f686173682077726f6e67206c656e6774683a4552524f523a206765745f706172656e745f6c65646765725f" - "74696d65206661696c65643a4552524f523a206765745f6c65646765725f73716e206661696c65643a2d2d2d204361" - "7465676f727920323a205472616e73616374696f6e20446174612046756e6374696f6e73202d2d2d5472616e736163" - "74696f6e204163636f756e743a5472616e73616374696f6e20466565206c656e6774683a5472616e73616374696f6e" - "20466565202873657269616c697a65642058525020616d6f756e74293a5472616e73616374696f6e2053657175656e" - "63653a4e6573746564206669656c64206c656e6774683a4e6573746564206669656c643a494e464f3a206765745f74" - "785f6e65737465645f6669656c64206e6f74206170706c696361626c653a5369676e657273206172726179206c656e" - "6774683a4d656d6f73206172726179206c656e6774683a4e6573746564206172726179206c656e6774683a494e464f" - "3a206765745f74785f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a535543434553" - "533a205472616e73616374696f6e20646174612066756e6374696f6e734552524f523a206765745f74785f6669656c" - "642853657175656e6365292077726f6e67206c656e6774683a4552524f523a206765745f74785f6669656c64284665" - "65292077726f6e67206c656e67746820286578706563746564203820627974657320666f7220585250293a4552524f" - "523a206765745f74785f6669656c64284163636f756e74292077726f6e67206c656e6774683a2d2d2d204361746567" - "6f727920333a2043757272656e74204c6564676572204f626a6563742046756e6374696f6e73202d2d2d4375727265" - "6e74206f626a6563742062616c616e6365206c656e677468202858525020616d6f756e74293a43757272656e74206f" - "626a6563742062616c616e6365202873657269616c697a65642058525020616d6f756e74293a43757272656e74206f" - "626a6563742062616c616e6365206c656e67746820286e6f6e2d58525020616d6f756e74293a43757272656e74206f" - "626a6563742062616c616e63653a494e464f3a206765745f63757272656e745f6c65646765725f6f626a5f6669656c" - "642842616c616e636529206661696c656420286d6179206265206578706563746564293a43757272656e74206c6564" - "676572206f626a656374206163636f756e743a494e464f3a206765745f63757272656e745f6c65646765725f6f626a" - "5f6669656c64284163636f756e7429206661696c65643a43757272656e74206e6573746564206669656c64206c656e" - "6774683a43757272656e74206e6573746564206669656c643a494e464f3a206765745f63757272656e745f6c656467" - "65725f6f626a5f6e65737465645f6669656c64206e6f74206170706c696361626c653a43757272656e74206f626a65" - "6374205369676e657273206172726179206c656e6774683a43757272656e74206e6573746564206172726179206c65" - "6e6774683a494e464f3a206765745f63757272656e745f6c65646765725f6f626a5f6e65737465645f61727261795f" - "6c656e206e6f74206170706c696361626c653a535543434553533a2043757272656e74206c6564676572206f626a65" - "63742066756e6374696f6e732d2d2d2043617465676f727920343a20416e79204c6564676572204f626a6563742046" - "756e6374696f6e73202d2d2d5375636365737366756c6c7920636163686564206f626a65637420696e20736c6f743a" - "436163686564206f626a6563742062616c616e6365206c656e677468202858525020616d6f756e74293a4361636865" - "64206f626a6563742062616c616e6365202873657269616c697a65642058525020616d6f756e74293a436163686564" - "206f626a6563742062616c616e6365206c656e67746820286e6f6e2d58525020616d6f756e74293a43616368656420" - "6f626a6563742062616c616e63653a494e464f3a206765745f6c65646765725f6f626a5f6669656c642842616c616e" - "636529206661696c65643a436163686564206e6573746564206669656c64206c656e6774683a436163686564206e65" - "73746564206669656c643a494e464f3a206765745f6c65646765725f6f626a5f6e65737465645f6669656c64206e6f" - "74206170706c696361626c653a436163686564206f626a656374205369676e657273206172726179206c656e677468" - "3a436163686564206e6573746564206172726179206c656e6774683a494e464f3a206765745f6c65646765725f6f62" - "6a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a535543434553533a20416e7920" - "6c6564676572206f626a6563742066756e6374696f6e73494e464f3a2063616368655f6c65646765725f6f626a2066" - "61696c65642028657870656374656420776974682074657374206669787475726573293a494e464f3a206765745f6c" - "65646765725f6f626a5f6669656c64206661696c656420617320657870656374656420286e6f20636163686564206f" - "626a656374293a494e464f3a206765745f6c65646765725f6f626a5f6e65737465645f6669656c64206661696c6564" - "2061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a5f61727261795f6c656e20666169" - "6c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a5f6e65737465645f617272" - "61795f6c656e206661696c65642061732065787065637465643a535543434553533a20416e79206c6564676572206f" - "626a6563742066756e6374696f6e732028696e7465726661636520746573746564294552524f523a206163636f756e" - "74726f6f745f6964206661696c656420666f722063616368696e6720746573743a2d2d2d2043617465676f72792035" - "3a204b65796c65742047656e65726174696f6e2046756e6374696f6e73202d2d2d4163636f756e74206b65796c6574" - "3a546573745479706543726564656e7469616c206b65796c65743a494e464f3a2063726564656e7469616c5f6b6579" - "6c6574206661696c656420286578706563746564202d20696e74657266616365206973737565293a457363726f7720" - "6b65796c65743a4f7261636c65206b65796c65743a535543434553533a204b65796c65742067656e65726174696f6e" - "2066756e6374696f6e734552524f523a206f7261636c655f6b65796c6574206661696c65643a4552524f523a206573" - "63726f775f6b65796c6574206661696c65643a4552524f523a206163636f756e74726f6f745f6964206661696c6564" - "3a2d2d2d2043617465676f727920363a205574696c6974792046756e6374696f6e73202d2d2d48656c6c6f2c205852" - "504c205741534d20776f726c6421496e70757420646174613a5348413531322068616c6620686173683a4e46542064" - "617461206c656e6774683a4e465420646174613a494e464f3a206765745f6e6674206661696c656420286578706563" - "746564202d206e6f2073756368204e4654293a54657374207472616365206d6573736167657061796c6f6164547261" - "63652066756e6374696f6e206279746573207772697474656e3a54657374206e756d62657220747261636554726163" - "655f6e756d2066756e6374696f6e20737563636565646564535543434553533a205574696c6974792066756e637469" - "6f6e734552524f523a2074726163655f6e756d2829206661696c65643a4552524f523a207472616365282920666169" - "6c65643a4552524f523a20636f6d707574655f7368613531325f68616c66206661696c65643a2d2d2d204361746567" - "6f727920373a2044617461205570646174652046756e6374696f6e73202d2d2d55706461746564206c656467657220" - "656e74727920646174612066726f6d205741534d20746573745375636365737366756c6c792075706461746564206c" - "656467657220656e74727920776974683a535543434553533a2044617461207570646174652066756e6374696f6e73" - "4552524f523a207570646174655f64617461206661696c65643a004d0970726f64756365727302086c616e67756167" - "65010452757374000c70726f6365737365642d6279010572757374631d312e38372e30202831373036376539616320" - "323032352d30352d303929002c0f7461726765745f6665617475726573022b0f6d757461626c652d676c6f62616c73" - "2b087369676e2d657874"; + "0061736d0100000001550c60027f7f017f60037f7f7f017f60047f7f7f7f017f60017f017f60067f7f7f7f7f7f017f" + "60037f7f7f0060057f7f7f7f7f0060087f7f7f7f7f7f7f7f017f60057f7f7f7f7f017f60017f0060027f7f00600001" + "7f02b1041808686f73745f6c69620874785f6669656c64000108686f73745f6c6962057472616365000608686f7374" + "5f6c69620a6c6467725f696e646578000008686f73745f6c696210706172656e745f6c6467725f74696d6500000868" + "6f73745f6c696210706172656e745f6c6467725f68617368000008686f73745f6c69620874785f696e6e6572000208" + "686f73745f6c69620a74785f6172725f6c656e000308686f73745f6c69621074785f696e6e65725f6172725f6c656e" + "000008686f73745f6c69620d686f6d655f6c655f6669656c64000108686f73745f6c69620d686f6d655f6c655f696e" + "6e6572000208686f73745f6c69620f686f6d655f6c655f6172725f6c656e000308686f73745f6c696215686f6d655f" + "6c655f696e6e65725f6172725f6c656e000008686f73745f6c69620863616368655f6c65000108686f73745f6c6962" + "0d63726564656e7469616c5f6964000708686f73745f6c696209657363726f775f6964000408686f73745f6c696209" + "6f7261636c655f6964000408686f73745f6c69620b7368613531325f68616c66000208686f73745f6c6962076e6674" + "5f757269000408686f73745f6c6962087365745f64617461000008686f73745f6c69620a6c655f6172725f6c656e00" + "0008686f73745f6c69620e6163636f756e74726f6f745f6964000208686f73745f6c6962106c655f696e6e65725f61" + "72725f6c656e000108686f73745f6c6962086c655f6669656c64000208686f73745f6c6962086c655f696e6e657200" + "08030b0a090a05050b000101030005030100110619037f01418080c0000b7f0041c698c0000b7f0041d098c0000b07" + "3504066d656d6f727902000d657363726f775f66696e697368001c0a5f5f646174615f656e6403010b5f5f68656170" + "5f6261736503020acf210a9d0101027f230041206b2201240020014200370310200142003703082001410036021820" + "00027f024041818020200141086a41141000220241004e0440200241144b0d01200241144704402000418180808078" + "36020441010c030b20002001280218360011200020012903103700092000200129030837000141000c020b20002002" + "36020441010c010b2000417336020441010b3a0000200141206a24000b5a01017f230041106b2202240020012d0000" + "41014604402002200134020437030841df97c000410b4101200241086a41081001000b200020012800113600102000" + "200129000937000820002001290001370000200241106a24000b1900200241214f0440000b20002002360204200020" + "013602000b1900200241094f0440000b20002002360204200020013602000bdd1e01077f230041c0036b2200240041" + "ea97c000411b4107410141001001418598c0004119410741014100100141b583c000412b4107410141001001200041" + "003602500240024002400240024002400240200041d0006a41041002220141004a044020002000280250220141ff81" + "fc0771410878200141187841ff81fc077172ad3703c00141e083c00041174101200041c0016a220241081001200041" + "003602800120004180016a41041003220141004c0d012000200028028001220141ff81fc0771410878200141187841" + "ff81fc077172ad3703c00141f783c00041134101200241081001200042003703d801200042003703d0012000420037" + "03c801200042003703c0012002412010042201412047044020002001ac3703a00141bd84c000412b4101200041a001" + "6a4108100141997f21030c080b418a84c00041134106200041c0016a220441201001419d84c0004120410741014100" + "100141aa85c000412e4107410141001001200041003602b001200042003703a801200042003703a001418180202000" + "41a0016a22024114100022014114470d0241d885c00041144104200241141001200042003703384188801820004138" + "6a22024108100022014108470d03200042083703c00141ec85c00041174101200441081001418386c0004128410620" + "02410810012000410036027841848008200041f8006a22024104100022014104470d0441ab86c00041154106200241" + "04100120004100360051200041013a005020004100360054200042003703d801200042003703d001200042003703c8" + "01200042003703c0010240200041d0006a4108200441201005220141004e044020002001ad3703800141c086c00041" + "14410120004180016a41081001200041306a20042001101a41d486c000410d41062000280230200028023410010c01" + "0b20002001ac3703800141e186c0004129410120004180016a410810010b20004183803c1006ac37038001418a87c0" + "004115410120004180016a22024108100120004189803c1006ac37038001419f87c000411341012002410810010240" + "200041d0006a41081007220141004e044020002001ad3703800141b287c000411441012002410810010c010b200020" + "01ac3703800141c687c000412d410120004180016a410810010b41f387c0004123410741014100100141e792c00041" + "3341074101410010012000420037033841828018200041386a220141081008220241004c0d05200241084604402000" + "42083703c001419a93c000412b4101200041c0016a4108100141c593c000412f41062001410810010c070b20002002" + "ad3703c00141f493c000412f4101200041c0016a41081001200041286a200041386a2002101b41a394c00041174106" + "2000280228200028022c10010c060b20002001ac3703c001418d85c000411d4101200041c0016a41081001419b7f21" + "030c060b20002001ac3703c00141e884c00041254101200041c0016a41081001419a7f21030c050b20002001ac3703" + "c001418289c000412a4101200041c0016a4108100141b77e21030c040b20002001ac3703c00141c188c00041c10041" + "01200041c0016a4108100141b67e21030c030b20002001ac3703c001419688c000412b4101200041c0016a41081001" + "41b57e21030c020b20002002ac3703c00141ba94c00041c5004101200041c0016a410810010b200041003602b00120" + "0042003703a801200042003703a001024041818020200041a0016a220241141008220141004a044041ff94c000411e" + "41042002411410010c010b20002001ac3703c001419d95c00041334101200041c0016a410810010b20004100360051" + "200041013a005020004100360054200042003703d801200042003703d001200042003703c801200042003703c00102" + "40200041d0006a4108200041c0016a220141201009220241004e044020002002ad3703800141d095c000411c410120" + "004180016a41081001200041206a20012002101a41ec95c000411541062000280220200028022410010c010b200020" + "02ac37038001418196c0004139410120004180016a410810010b20004183803c100aac3703800141ba96c000412441" + "0120004180016a2201410810010240200041d0006a4108100b220241004e044020002002ad3703800141de96c00041" + "1c41012001410810010c010b20002002ac3703800141fa96c000413d410120004180016a410810010b41b797c00041" + "28410741014100100141ac89c000412f4107410141001001200041c0016a2204101820004180016a22012004101920" + "0042003703b801200042003703b001200042003703a801200042003703a001024002400240024002402001200041a0" + "016a2202101d22014120460440200241204100100c220541004a044020002005ad3703c00141db89c0004123410120" + "0441081001200042003703782005200041f8006a22014108101e220241004c0d0220024108460440200042083703c0" + "0141fe89c000412a410120044108100141a88ac000412e41062001410810010c060b20002002ad3703c00141d68ac0" + "00412e4101200041c0016a41081001200041186a200041f8006a2002101b41848bc000411641062000280218200028" + "021c10010c050b20002005ac3703c00141bc8dc000413c4101200041c0016a220141081001200042003703d8012000" + "42003703d001200042003703c801200042003703c001410120014120101e22014100480d020c030b20002001ac3703" + "c001419090c000412e4101200041c0016a4108100141ef7c21030c050b20002002ac3703c001419a8bc000412b4101" + "200041c0016a410810010c020b20002001ac37035041f88dc00041c1004101200041d0006a410810010b2000410036" + "0039200041013a00382000410036003c4101200041386a200041c0016a101f2201410048044020002001ac37035041" + "b98ec00041354101200041d0006a410810010b410110202201410048044020002001ac37035041ee8ec00041324101" + "200041d0006a410810010b4101200041386a10212201410048044020002001ac37035041a08fc00041394101200041" + "d0006a410810010b41d98fc000413741074101410010010c010b20004100360039200041013a00382000410036003c" + "200042003703d801200042003703d001200042003703c801200042003703c00102402005200041386a200041c0016a" + "2201101f220241004e044020002002ad37035041c58bc000411b4101200041d0006a41081001200041106a20012002" + "101a41e08bc000411441062000280210200028021410010c010b20002002ac37035041f48bc00041314101200041d0" + "006a410810010b200020051020ac37035041a58cc00041234101200041d0006a22014108100102402005200041386a" + "1021220241004e044020002002ad37035041c88cc000411b41012001410810010c010b20002002ac37035041e38cc0" + "0041354101200041d0006a410810010b41988dc000412441074101410010010b41be90c000412f4107410141001001" + "200041c0016a22011018200041386a2204200110192000420037036820004200370360200042003703582000420037" + "0350024002402004200041d0006a2202101d2201412046044041ed90c000410f410620024120100120004200370398" + "012000420037039001200042003703880120004200370380010240200441142004411441fc90c00041092000418001" + "6a22014120100d220241004a0440200041086a20012002101a418491c000411241062000280208200028020c10010c" + "010b20002002ac3703c001419691c000413c4101200041c0016a410810010b200042003703b801200042003703b001" + "200042003703a801200042003703a00120004180808cc07e360270200041386a22044114200041f0006a4104200041" + "a0016a22024120100e22014120470d0141d291c000410e4106200241201001200042003703d801200042003703d001" + "200042003703c801200042003703c001200041808080d00236027420044114200041f4006a4104200041c0016a4120" + "100f2201412047044020002001ac370378419292c000411c4101200041f8006a4108100141887c21030c040b41e091" + "c000410e4106200041c0016a22044120100141ee91c00041244107410141001001418080c000412541074101410010" + "0120004200370398012000420037039001200042003703880120004200370380010240024041a580c0004117200041" + "80016a2202412010102201412046044041bc80c000410b410641a580c0004117100141c780c0004111410620024120" + "100120041018200041d0006a220620041019200042003703b801200042003703b001200042003703a8012000420037" + "03a00102404100200422036b410371220220036a220520034d0d0020020440200221010340200341003a0000200341" + "016a2103200141016b22010d000b0b200241016b4107490d000340200341003a0000200341076a41003a0000200341" + "066a41003a0000200341056a41003a0000200341046a41003a0000200341036a41003a0000200341026a41003a0000" + "200341016a41003a0000200341086a22032005470d000b0b200541800220026b2201417c716a220320054b04400340" + "20054100360200200541046a22052003490d000b0b024020032001410371220120036a22024f0d0020012205044003" + "40200341003a0000200341016a2103200541016b22050d000b0b200141016b4107490d000340200341003a00002003" + "41076a41003a0000200341066a41003a0000200341056a41003a0000200341046a41003a0000200341036a41003a00" + "00200341026a41003a0000200341016a41003a0000200341086a22032002470d000b0b20064114200041a0016a4120" + "20044180021011220141004c0d0120002001ad37033841d880c00041104101200041386a4108100120014181024f0d" + "0541e880c000410941062004200110010c020b20002001ac3703c00141e381c00041224101200041c0016a41081001" + "41a77b21030c050b20002001ac37033841f180c000412e4101200041386a410810010b419f81c0004112410641b181" + "c000410710012000422a3703384101210341b881c00041114101200041386a4108100141c981c000411a4107410141" + "001001418582c0004129410741014100100141ae82c000412810122201412847044020002001ac3703c001419b83c0" + "00411a4101200041c0016a4108100141c37a21030c040b41d682c0004127410641ae82c0004128100141fd82c00041" + "1e4107410141001001419e98c000412841074101410010010c030b20002001ac3703c00141ca92c000411d41012000" + "41c0016a41081001418b7c21030c020b20002001ac3703c00141ae92c000411c4101200041c0016a4108100141897c" + "21030c010b000b200041c0036a240020030b0c00200041142001412010140b0e002000418280182001200210160b0e" + "002000200141082002412010170b0a0020004183803c10130b0a0020002001410810150b0bd0180100418080c0000b" + "c6182d2d2d2043617465676f727920363a205574696c6974792046756e6374696f6e73202d2d2d48656c6c6f2c2058" + "52504c205741534d20776f726c6421496e70757420646174613a5348413531322068616c6620686173683a4e465420" + "64617461206c656e6774683a4e465420646174613a494e464f3a206765745f6e6674206661696c6564202865787065" + "63746564202d206e6f2073756368204e4654293a54657374207472616365206d6573736167657061796c6f61645465" + "7374206e756d626572207472616365535543434553533a205574696c6974792066756e6374696f6e734552524f523a" + "20636f6d707574655f7368613531325f68616c66206661696c65643a2d2d2d2043617465676f727920373a20446174" + "61205570646174652046756e6374696f6e73202d2d2d55706461746564206c656467657220656e7472792064617461" + "2066726f6d205741534d20746573745375636365737366756c6c792075706461746564206c656467657220656e7472" + "7920776974683a535543434553533a2044617461207570646174652066756e6374696f6e734552524f523a20757064" + "6174655f64617461206661696c65643a2d2d2d2043617465676f727920313a204c6564676572204865616465722046" + "756e6374696f6e73202d2d2d4c65646765722073657175656e6365206e756d6265723a506172656e74206c65646765" + "722074696d653a506172656e74206c656467657220686173683a535543434553533a204c6564676572206865616465" + "722066756e6374696f6e734552524f523a206765745f706172656e745f6c65646765725f686173682077726f6e6720" + "6c656e6774683a4552524f523a206765745f706172656e745f6c65646765725f74696d65206661696c65643a455252" + "4f523a206765745f6c65646765725f73716e206661696c65643a2d2d2d2043617465676f727920323a205472616e73" + "616374696f6e20446174612046756e6374696f6e73202d2d2d5472616e73616374696f6e204163636f756e743a5472" + "616e73616374696f6e20466565206c656e6774683a5472616e73616374696f6e20466565202873657269616c697a65" + "642058525020616d6f756e74293a5472616e73616374696f6e2053657175656e63653a4e6573746564206669656c64" + "206c656e6774683a4e6573746564206669656c643a494e464f3a206765745f74785f6e65737465645f6669656c6420" + "6e6f74206170706c696361626c653a5369676e657273206172726179206c656e6774683a4d656d6f73206172726179" + "206c656e6774683a4e6573746564206172726179206c656e6774683a494e464f3a206765745f74785f6e6573746564" + "5f61727261795f6c656e206e6f74206170706c696361626c653a535543434553533a205472616e73616374696f6e20" + "646174612066756e6374696f6e734552524f523a206765745f74785f6669656c642853657175656e6365292077726f" + "6e67206c656e6774683a4552524f523a206765745f74785f6669656c6428466565292077726f6e67206c656e677468" + "20286578706563746564203820627974657320666f7220585250293a4552524f523a206765745f74785f6669656c64" + "284163636f756e74292077726f6e67206c656e6774683a2d2d2d2043617465676f727920343a20416e79204c656467" + "6572204f626a6563742046756e6374696f6e73202d2d2d5375636365737366756c6c7920636163686564206f626a65" + "637420696e20736c6f743a436163686564206f626a6563742062616c616e6365206c656e677468202858525020616d" + "6f756e74293a436163686564206f626a6563742062616c616e6365202873657269616c697a65642058525020616d6f" + "756e74293a436163686564206f626a6563742062616c616e6365206c656e67746820286e6f6e2d58525020616d6f75" + "6e74293a436163686564206f626a6563742062616c616e63653a494e464f3a206765745f6c65646765725f6f626a5f" + "6669656c642842616c616e636529206661696c65643a436163686564206e6573746564206669656c64206c656e6774" + "683a436163686564206e6573746564206669656c643a494e464f3a206765745f6c65646765725f6f626a5f6e657374" + "65645f6669656c64206e6f74206170706c696361626c653a436163686564206f626a656374205369676e6572732061" + "72726179206c656e6774683a436163686564206e6573746564206172726179206c656e6774683a494e464f3a206765" + "745f6c65646765725f6f626a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a5355" + "43434553533a20416e79206c6564676572206f626a6563742066756e6374696f6e73494e464f3a2063616368655f6c" + "65646765725f6f626a206661696c65642028657870656374656420776974682074657374206669787475726573293a" + "494e464f3a206765745f6c65646765725f6f626a5f6669656c64206661696c65642061732065787065637465642028" + "6e6f20636163686564206f626a656374293a494e464f3a206765745f6c65646765725f6f626a5f6e65737465645f66" + "69656c64206661696c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a5f6172" + "7261795f6c656e206661696c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a" + "5f6e65737465645f61727261795f6c656e206661696c65642061732065787065637465643a535543434553533a2041" + "6e79206c6564676572206f626a6563742066756e6374696f6e732028696e7465726661636520746573746564294552" + "524f523a206163636f756e74726f6f745f6964206661696c656420666f722063616368696e6720746573743a2d2d2d" + "2043617465676f727920353a204b65796c65742047656e65726174696f6e2046756e6374696f6e73202d2d2d416363" + "6f756e74206b65796c65743a546573745479706543726564656e7469616c206b65796c65743a494e464f3a20637265" + "64656e7469616c5f6b65796c6574206661696c656420286578706563746564202d20696e7465726661636520697373" + "7565293a457363726f77206b65796c65743a4f7261636c65206b65796c65743a535543434553533a204b65796c6574" + "2067656e65726174696f6e2066756e6374696f6e734552524f523a206f7261636c655f6b65796c6574206661696c65" + "643a4552524f523a20657363726f775f6b65796c6574206661696c65643a4552524f523a206163636f756e74726f6f" + "745f6964206661696c65643a2d2d2d2043617465676f727920333a2043757272656e74204c6564676572204f626a65" + "63742046756e6374696f6e73202d2d2d43757272656e74206f626a6563742062616c616e6365206c656e6774682028" + "58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e6365202873657269616c697a656420" + "58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e6365206c656e67746820286e6f6e2d" + "58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e63653a494e464f3a206765745f6375" + "7272656e745f6c65646765725f6f626a5f6669656c642842616c616e636529206661696c656420286d617920626520" + "6578706563746564293a43757272656e74206c6564676572206f626a656374206163636f756e743a494e464f3a2067" + "65745f63757272656e745f6c65646765725f6f626a5f6669656c64284163636f756e7429206661696c65643a437572" + "72656e74206e6573746564206669656c64206c656e6774683a43757272656e74206e6573746564206669656c643a49" + "4e464f3a206765745f63757272656e745f6c65646765725f6f626a5f6e65737465645f6669656c64206e6f74206170" + "706c696361626c653a43757272656e74206f626a656374205369676e657273206172726179206c656e6774683a4375" + "7272656e74206e6573746564206172726179206c656e6774683a494e464f3a206765745f63757272656e745f6c6564" + "6765725f6f626a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a53554343455353" + "3a2043757272656e74206c6564676572206f626a6563742066756e6374696f6e736572726f725f636f64653d3d3d3d" + "20484f53542046554e4354494f4e532054455354203d3d3d54657374696e6720323620686f73742066756e6374696f" + "6e73535543434553533a20416c6c20686f73742066756e6374696f6e2074657374732070617373656421004d097072" + "6f64756365727302086c616e6775616765010452757374000c70726f6365737365642d6279010572757374631d312e" + "39352e30202835393830373631366520323032362d30342d313429002c0f7461726765745f6665617475726573022b" + "0f6d757461626c652d676c6f62616c732b087369676e2d657874"; extern std::string const kAllKeyletsWasmHex = "0061736d0100000001500a60067f7f7f7f7f7f017f60047f7f7f7f017f60087f7f7f7f7f7f7f7f017f60047f7f7f7f" @@ -395,245 +401,233 @@ extern std::string const kAllKeyletsWasmHex = "5f6665617475726573022b0f6d757461626c652d676c6f62616c732b087369676e2d657874"; extern std::string const kCodecovTestsWasmHex = - "0061736d0100000001570b60067f7f7f7f7f7f017f60047f7f7f7f017f60027f7f017f60037f7f7f017f60077f7f7f" - "7f7f7f7f017f60057f7f7f7f7f017f60087f7f7f7f7f7f7f7f017f60017f017f60037f7f7e017f60047f7f7f7f0060" - "00017f02c60a3b08686f73745f6c6962057472616365000508686f73745f6c69620974726163655f6e756d00080868" - "6f73745f6c69620a6c6467725f696e646578000208686f73745f6c696210706172656e745f6c6467725f74696d6500" - "0208686f73745f6c696210706172656e745f6c6467725f68617368000208686f73745f6c696208626173655f666565" - "000208686f73745f6c696211616d656e646d656e745f656e61626c6564000208686f73745f6c69620874785f666965" - "6c64000308686f73745f6c69620e6163636f756e74726f6f745f6964000108686f73745f6c69620863616368655f6c" - "65000308686f73745f6c69620d686f6d655f6c655f6669656c64000308686f73745f6c6962086c655f6669656c6400" - "0108686f73745f6c69620874785f696e6e6572000108686f73745f6c69620d686f6d655f6c655f696e6e6572000108" - "686f73745f6c6962086c655f696e6e6572000508686f73745f6c69620a74785f6172725f6c656e000708686f73745f" - "6c69620f686f6d655f6c655f6172725f6c656e000708686f73745f6c69620a6c655f6172725f6c656e000208686f73" - "745f6c69621074785f696e6e65725f6172725f6c656e000208686f73745f6c696215686f6d655f6c655f696e6e6572" - "5f6172725f6c656e000208686f73745f6c6962106c655f696e6e65725f6172725f6c656e000308686f73745f6c6962" - "087365745f64617461000208686f73745f6c69620b7368613531325f68616c66000108686f73745f6c696209636865" - "636b5f736967000008686f73745f6c6962076e66745f757269000008686f73745f6c69620a6e66745f697373756572" - "000108686f73745f6c6962096e66745f7461786f6e000108686f73745f6c6962096e66745f666c616773000208686f" - "73745f6c69620c6e66745f786665725f666565000208686f73745f6c69620a6e66745f73657269616c000108686f73" - "745f6c69620a74726163655f61636374000108686f73745f6c69620974726163655f616d74000108686f73745f6c69" - "6208636865636b5f6964000008686f73745f6c69620f666c6f61745f66726f6d5f75696e74000508686f73745f6c69" - "620c74727573746c696e655f6964000608686f73745f6c696206616d6d5f6964000008686f73745f6c69620d637265" - "64656e7469616c5f6964000608686f73745f6c69620a6d70746f6b656e5f6964000008686f73745f6c69620c747261" - "63655f78666c6f6174000108686f73745f6c696209666c6f61745f636d70000108686f73745f6c696209666c6f6174" - "5f616464000408686f73745f6c696209666c6f61745f737562000408686f73745f6c69620a666c6f61745f6d756c74" - "000408686f73745f6c696209666c6f61745f646976000408686f73745f6c69620a666c6f61745f726f6f7400000868" - "6f73745f6c696209666c6f61745f706f77000008686f73745f6c696209657363726f775f6964000008686f73745f6c" - "69620f6d70745f69737375616e63655f6964000008686f73745f6c69620c6e66745f6f666665725f6964000008686f" - "73745f6c6962086f666665725f6964000008686f73745f6c6962096f7261636c655f6964000008686f73745f6c6962" - "0a7061796368616e5f6964000608686f73745f6c6962167065726d697373696f6e65645f646f6d61696e5f69640000" - "08686f73745f6c6962097469636b65745f6964000008686f73745f6c6962087661756c745f6964000008686f73745f" - "6c69620b64656c65676174655f6964000008686f73745f6c6962126465706f7369745f707265617574685f69640000" - "08686f73745f6c6962066469645f6964000108686f73745f6c69620a7369676e6572735f69640001030302090a0503" - "0100110619037f01418080c0000b7f0041cf9bc0000b7f0041d09bc0000b073504066d656d6f727902000d65736372" - "6f775f66696e697368003c0a5f5f646174615f656e6403010b5f5f686561705f6261736503020a8c2f024600024020" - "0020014704402002200341014100410010001a20004100480d01418b80c000410b2000ad1001000b200220032000ac" - "10011a0f0b418b80c000410b2000ac1001000bc22e020b7f017e23004190026b22002400419680c000412341014100" - "410010001a20004100360260200041e0006a220241041002410441888ac000410a103b200041003602602002410410" - "03410441928ac0004110103b200041f8006a22054200370300200041f0006a22014200370300200041e8006a220642" - "0037030020004200370360200241201004412041a28ac0004110103b20004100360260200241041005410441b28ac0" - "004108103b200041106a2208428182848890a0c08001370300200041186a2209428182848890a0c080013703002000" - "41206a220a428182848890a0c080013703002000428182848890a0c0800137030841b980c000410e1006410141c780" - "c0004111103b200041086a41201006410141c780c0004111103b418180202002411410072203411446044002402000" - "412e6a200041e2006a2d00003a0000200020002900673703e8012000200041ec006a2900003700ed01200020002f00" - "603b012c200020002903e8013703a801200020002900ed013700ad012000200028006336002f200041386a20002900" - "ad01370000200020002903a80137003320054200370300200142003703002006420037030020004200370360200041" - "2c6a2205411420024120100822034120470d00200041c2006a20002d00623a0000200041f0016a2207200041ef006a" - "290000220b370300200041cf006a200b370000200041d7006a200041f7006a290000370000200041df006a200041ff" - "006a2d00003a0000200020002f01603b01402000200028006336004320002000290067370047200041406b41204100" - "1009410141d880c0004108103b2001410036020020064200370300200042003703604181802020024114100a411441" - "ba8ac000410d103b20014100360200200642003703002000420037036041014181802020024114100b411441c78ac0" - "004108103b02404100200041e4006a22046b410371220320046a220120044d0d002003044020032106034020044100" - "3a0000200441016a2104200641016b22060d000b0b200341016b4107490d000340200441003a0000200441076a4100" - "3a0000200441066a41003a0000200441056a41003a0000200441046a41003a0000200441036a41003a000020044102" - "6a41003a0000200441016a41003a0000200441086a22042001470d000b0b2001413c20036b2203417c716a22042001" - "4b0440034020014100360200200141046a22012004490d000b0b024020042003410371220320046a22064f0d002003" - "220104400340200441003a0000200441016a2104200141016b22010d000b0b200341016b4107490d00034020044100" - "3a0000200441076a41003a0000200441066a41003a0000200441056a41003a0000200441046a41003a000020044103" - "6a41003a0000200441026a41003a0000200441016a41003a0000200441086a22042006470d000b0b200041043602a0" - "01200041818020360260200041f8016a2203410036020020074200370300200042003703e80120024104200041e801" - "6a22014114100c411441cf8ac0004108103b2003410036020020074200370300200042003703e801200220002802a0" - "0120014114100d411441d78ac000410d103b2003410036020020074200370300200042003703e80141012002200028" - "02a00120014114100e411441e48ac0004108103b4189803c100f412041e080c000410a103b4189803c1010412041ea" - "80c000410f103b41014189803c1011412041f980c000410a103b200220002802a00110124120418381c0004110103b" - "200220002802a00110134120419381c0004115103b4101200220002802a0011014412041a881c0004110103b200541" - "141015411441b881c0004108103b20004180026a220642003703002003420037030020074200370300200042003703" - "e801200220002802a001200141201016412041ec8ac000410b103b41c081c000410c41cc81c000410b41d781c00041" - "0e1017410141e581c0004109103b200041c0016a200a290300370300200041b8016a2009290300370300200041b001" - "6a2008290300370300200020002903083703a801200341003b010020074200370300200042003703e8012005411420" - "0041a8016a22044120200141121018411241f78ac0004107103b2003410036020020074200370300200042003703e8" - "0120044120200141141019411441fe8ac000410a103b200041003602e8012004412020014104101a410441888bc000" - "4109103b20044120101b410841ee81c0004109103b20044120101c410a41f781c000410c103b200041003602e80120" - "04412020014104101d410441918bc000410a103b418382c000410d20054114101e4100419082c000410a103b418382" - "c000410d419a82c0004108101f410041a282c0004109103b418382c000410d41ab82c0004108101f410041b382c000" - "410e103b417f41041004417141c182c0004118103b200041003602e8012001417f10044171419b8bc0004118103b20" - "0041ea016a41003a0000200041003b01e801200141031004417d41b38bc000411e103b200041003602e80120014180" - "94ebdc031004417341d18bc000411d103b4102100f416f41d982c0004119103b417f20002802a0011012417141f282" - "c0004118103b2002417f10124171418a83c0004118103b20024181081012417441a283c0004119103b200041e094eb" - "dc036a220820002802a0011012417341bb83c0004118103b2006420037030020034200370300200742003703002000" - "42003703e8012005411420084108200141201020417341ee8bc0004114103b20064200370300200342003703002007" - "4200370300200042003703e8012005411420054114200141201020417141828cc0004116103b200642003703002003" - "420037030020074200370300200042003703e801200841082001412041001021417341988cc0004117103b20064200" - "3703002003420037030020074200370300200042003703e801200220002802a0012001412041001021417141af8cc0" - "004120103b200820002802a00141011009417341d383c0004110103b200220002802a00141011009417141e383c000" - "4112103b200642003703002003420037030020074200370300200042003703e801200820002802a001200141201008" - "417341cf8cc0004116103b200642003703002003420037030020074200370300200042003703e801200220002802a0" - "01200141201008417141e58cc0004118103b200642003703002003420037030020074200370300200042003703e801" - "2005411420054114200820002802a001200141201022417341fd8cc000411d103b2006420037030020034200370300" - "20074200370300200042003703e8012005411420054114200220002802a0012001412010224171419a8dc000411f10" - "3b200642003703002003420037030020074200370300200042003703e80141bb9bc0004114200820002802a0012001" - "41201023417341b98dc0004115103b200642003703002003420037030020074200370300200042003703e80141bb9b" - "c0004114200220002802a001200141201023417141ce8dc000411b103b200642003703002003420037030020074200" - "370300200042003703e80141bb9bc000411441f583c0004114200141201023417141e98dc0004125103b2006420037" - "03002003420037030020074200370300200042003703e801418984c000412841bb9bc0004114200141201023417141" - "8e8ec0004121103b200041dc016a2000413c6a280100360200200041d4016a200041346a2901003702002000200029" - "012c3702cc01200041808080083602c801200041003b01e801200041c8016a2209411841bb9bc00041142001410210" - "23417141af8ec000410a103b200820002802a001422a1001417341b184c0004111103b200041003b01e80141022001" - "41021007416f41b98ec0004117103b200041003b01e801410220014102100a416f41d08ec000411c103b200041003b" - "01e8014101410220014102100b416f41ec8ec0004117103b4102100f416f41d982c0004119103b41021010416f41c2" - "84c000411e103b410141021011416f41e084c0004119103b41b980c0004181081006417441f984c000411f103b41b9" - "80c00041c10010064174419885c000411a103b200041003b01e801200241810820014102100c417441838fc0004116" - "103b200041003b01e801200241810820014102100d417441998fc000411b103b200041003b01e80141012002418108" - "20014102100e417441b48fc0004116103b20024181081012417441b285c000411e103b20024181081013417441d085" - "c0004123103b410120024181081014417441f385c000411e103b200241812010154174419186c0004116103b418382" - "c00041810841cc81c000410b41d781c000410e1017417441e581c0004109103b418382c000410d41cc81c000418108" - "41d781c000410e1017417441e581c0004109103b418382c000410d41cc81c000410b41d781c0004181081017417441" - "e581c0004109103b200041003b01e8012002418108200141021016417441ca8fc0004119103b200041003b01e80141" - "bb9bc00041810841bb9bc0004114200141021023417441e38fc0004114103b200041003b01e8012005411420054114" - "2002418108200141021024417441f78fc000411b103b200041003b01e8012009418108200541142001410210254174" - "419290c000411e103b418382c000410d200820002802a00141001000417341a786c000410f103b200042d487b6f4c7" - "d4b1c0003700e001418382c000410d200041e095ebdc036a220441081026417341b686c0004116103b418382c00041" - "0d200820002802a001101f417341cc86c0004113103b20044108200041e0016a220841081027417341df86c0004114" - "103b20084108200441081027417341f386c0004114103b200041003b01e80120044108200841082001410241001028" - "417341b090c0004114103b200041003b01e80120084108200441082001410241001028417341c490c0004114103b20" - "0041003b01e80120044108200841082001410241001029417341d890c0004114103b200041003b01e8012008410820" - "0441082001410241001029417341ec90c0004114103b200041003b01e8012004410820084108200141024100102a41" - "73418091c0004115103b200041003b01e8012008410820044108200141024100102a4173419591c0004115103b2000" - "41003b01e8012004410820084108200141024100102b417341aa91c0004114103b200041003b01e801200841082004" - "4108200141024100102b417341be91c0004114103b200041003b01e801200441084103200141024100102c417341d2" - "91c0004114103b200041003b01e801200441084103200141024100102d417341e691c0004113103b20064200370300" - "2003420037030020074200370300200042003703e801200541142005411420014120102e417141f991c000411b103b" - "200642003703002003420037030020074200370300200042003703e801200541142005411420014120102f41714194" - "92c0004121103b200642003703002003420037030020074200370300200042003703e8012005411420054114200141" - "201030417141b592c000411e103b200642003703002003420037030020074200370300200042003703e80120054114" - "20054114200141201031417141d392c000411a103b2006420037030020034200370300200742003703002000420037" - "03e8012005411420054114200141201032417141ed92c000411b103b20064200370300200342003703002007420037" - "0300200042003703e8012005411420054114200541142001412010334171418893c000411c103b2006420037030020" - "03420037030020074200370300200042003703e8012005411420054114200141201034417141a493c0004128103b20" - "0642003703002003420037030020074200370300200042003703e8012005411420054114200141201035417141cc93" - "c000411b103b200642003703002003420037030020074200370300200042003703e801200541142005411420014120" - "1036417141e793c000411a103b200220002802a001410010094171418787c000411b103b200041003b01e801200541" - "14200220002802a0012001410210184171418194c000411a103b200041003b01e801200220002802a0012001410210" - "194171419b94c000411d103b200041003b01e801200220002802a00120014102101a417141b894c000411c103b2002" - "20002802a001101b417141a287c000411c103b200220002802a001101c417141be87c000411f103b200041003602e8" - "01200220002802a00120014104101d417141d494c000411d103b200041003b01e801200220002802a0012001410210" - "08417141f194c0004124103b200041808080083602e801200041003b018e02200220002802a001200141042000418e" - "026a2203410210204171419595c000411e103b200041003b018e02200220002802a001220620054114200220062003" - "41021024417141b395c0004124103b200041003b018e0220054114200220002802a001220620022006200341021024" - "417141d795c0004124103b200041003b018e02200220002802a00120054114200341021037417141fb95c000412210" - "3b200041003b018e0220054114200220002802a0012003410210374171419d96c0004122103b200041003b018e0220" - "0220002802a00120054114200341021038417141bf96c0004129103b200041003b018e0220054114200220002802a0" - "01200341021038417141e896c0004129103b200041003b018e02200220002802a0012003410210394171419197c000" - "411c103b200041003b018e02200220002802a0012001410420034102102e417141ad97c000411f103b200041003b01" - "8e02200220002802a0012005411441f583c0004114200341021022417141cc97c0004123103b200041003b018e0220" - "054114200220002802a00141f583c0004114200341021022417141ef97c0004123103b200041003b018e0220022000" - "2802a0012001410420034102102f4171419298c0004125103b200041003b018e0220094118200220002802a0012003" - "41021025417141b798c0004120103b200041003b018e02200220002802a00120014104200341021030417141d798c0" - "004122103b200041003b018e02200220002802a00120014104200341021031417141f998c000411e103b200041003b" - "018e02200220002802a001200141042003410210324171419799c000411f103b200041003b018e02200220002802a0" - "012005411420014104200341021033417141b699c0004121103b200041003b018e0220054114200220002802a00120" - "014104200341021033417141d799c0004121103b200041003b018e02200220002802a0012001410420034102103441" - "7141f899c000412c103b200041003b018e02200220002802a00120034102103a417141a49ac0004120103b20004100" - "3b018e02200220002802a00120014104200341021035417141c49ac000411f103b200041003b018e02200220002802" - "a00120014104200341021036417141e39ac000411e103b200041003b018e02200220002802a00141dd87c000412020" - "0341021018417141819bc000411d103b418382c000410d200220002802a001101e417141fd87c0004120103b418396" - "abdd03410d41dd87c0004120410010004173419d88c0004110103b418396abdd03410d200841081026417341ad88c0" - "004117103b418396abdd03410d20054114101e417341c488c0004115103b418396abdd03410d41ab82c0004108101f" - "417341d988c0004114103b200220002802a001200241810841001000417441ed88c000410e103b2002418108420110" - "01417441fb88c0004112103b418382c0004181082008410810264174418d89c0004115103b418382c0004181082005" - "4114101e417441a289c0004113103b418382c00041810841ab82c0004108101f417441b589c0004112103b418382c0" - "00410d200220002802a001101f417141c789c0004116103b200041003b018e02200220002802a00120054114200341" - "0210254171419e9bc000411d103b418382c000410d200220002802a00141021000417141dd89c0004114103b410141" - "0020054114101e410041f189c0004117103b20004190026a240041010f0b0b418080c000410b417f20032003417f4e" - "1bac1001000b0ba61b0200418080c0000b89046572726f725f636f64653d54455354204641494c4544242424242420" - "5354415254494e47205741534d20455845435554494f4e202424242424746573745f616d656e646d656e74616d656e" - "646d656e745f656e61626c656463616368655f6c6574785f6172725f6c656e686f6d655f6c655f6172725f6c656e6c" - "655f6172725f6c656e74785f696e6e65725f6172725f6c656e686f6d655f6c655f696e6e65725f6172725f6c656e6c" - "655f696e6e65725f6172725f6c656e7365745f6461746174657374206d65737361676574657374207075626b657974" - "657374207369676e6174757265636865636b5f7369676e66745f666c6167736e66745f786665725f66656574657374" - "696e6720747261636574726163655f61636374400000000000005f74726163655f616d744000000000000000747261" - "63655f616d745f7a65726f706172656e745f6c6467725f686173685f6e65675f70747274785f6172725f6c656e5f69" - "6e76616c69645f736669656c6474785f696e6e65725f6172725f6c656e5f6e65675f70747274785f696e6e65725f61" - "72725f6c656e5f6e65675f6c656e74785f696e6e65725f6172725f6c656e5f746f6f5f6c6f6e6774785f696e6e6572" - "5f6172725f6c656e5f7074725f6f6f6263616368655f6c655f7074725f6f6f6263616368655f6c655f77726f6e675f" - "6c656e55534430303030303030303030303030303030300041b184c0000b8a1774726163655f6e756d5f6f6f625f73" - "7472686f6d655f6c655f6172725f6c656e5f696e76616c69645f736669656c646c655f6172725f6c656e5f696e7661" - "6c69645f736669656c64616d656e646d656e745f656e61626c65645f746f6f5f6269675f736c696365616d656e646d" - "656e745f656e61626c65645f746f6f5f6c6f6e6774785f696e6e65725f6172725f6c656e5f746f6f5f6269675f736c" - "696365686f6d655f6c655f696e6e65725f6172725f6c656e5f746f6f5f6269675f736c6963656c655f696e6e65725f" - "6172725f6c656e5f746f6f5f6269675f736c6963657365745f646174615f746f6f5f6269675f736c69636574726163" - "655f6f6f625f736c69636574726163655f78666c6f61745f6f6f625f736c69636574726163655f616d745f6f6f625f" - "736c696365666c6f61745f636d705f6f6f625f736c69636531666c6f61745f636d705f6f6f625f736c696365326361" - "6368655f6c655f77726f6e675f73697a655f75696e743235366e66745f666c6167735f77726f6e675f73697a655f75" - "696e743235366e66745f786665725f6665655f77726f6e675f73697a655f75696e7432353630303030303030303030" - "3030303030303030303030303030303030303030303174726163655f616363745f77726f6e675f73697a655f616363" - "6f756e745f696474726163655f6f6f625f737472696e6774726163655f78666c6f61745f6f6f625f737472696e6774" - "726163655f616363745f6f6f625f737472696e6774726163655f616d745f6f6f625f737472696e6774726163655f74" - "6f6f5f6c6f6e6774726163655f6e756d5f746f6f5f6c6f6e6774726163655f78666c6f61745f746f6f5f6c6f6e6774" - "726163655f616363745f746f6f5f6c6f6e6774726163655f616d745f746f6f5f6c6f6e6774726163655f616d745f77" - "726f6e675f6c656e67746874726163655f696e76616c69645f61735f68657874726163655f616363745f636865636b" - "5f646573796e636c6467725f696e646578706172656e745f6c6467725f74696d65706172656e745f6c6467725f6861" - "7368626173655f666565686f6d655f6c655f6669656c646c655f6669656c6474785f696e6e6572686f6d655f6c655f" - "696e6e65726c655f696e6e65727368613531325f68616c666e66745f7572696e66745f6973737565726e66745f7461" - "786f6e6e66745f73657269616c706172656e745f6c6467725f686173685f6e65675f6c656e706172656e745f6c6467" - "725f686173685f6275665f746f6f5f736d616c6c706172656e745f6c6467725f686173685f6c656e5f746f6f5f6c6f" - "6e67636865636b5f69645f6f6f625f6c656e5f753332636865636b5f69645f77726f6e675f6c656e5f753332666c6f" - "61745f66726f6d5f75696e745f6c656e5f6f6f62666c6f61745f66726f6d5f75696e745f77726f6e675f6c656e5f75" - "696e7436346163636f756e74726f6f745f69645f6c656e5f6f6f626163636f756e74726f6f745f69645f77726f6e67" - "5f6c656e74727573746c696e655f69645f6c656e5f6f6f625f63757272656e637974727573746c696e655f69645f77" - "726f6e675f6c656e5f63757272656e6379616d6d5f69645f6c656e5f6f6f625f617373657432616d6d5f69645f6c65" - "6e5f77726f6e675f6c656e5f617373657432616d6d5f69645f6c656e5f77726f6e675f6e6f6e5f7872705f63757272" - "656e63795f6c656e616d6d5f69645f6c656e5f77726f6e675f7872705f63757272656e63795f6c656e616d6d5f6964" - "5f6d707474785f6669656c645f696e76616c69645f736669656c64686f6d655f6c655f6669656c645f696e76616c69" - "645f736669656c646c655f6669656c645f696e76616c69645f736669656c6474785f696e6e65725f746f6f5f626967" - "5f736c696365686f6d655f6c655f696e6e65725f746f6f5f6269675f736c6963656c655f696e6e65725f746f6f5f62" - "69675f736c6963657368613531325f68616c665f746f6f5f6269675f736c696365616d6d5f69645f746f6f5f626967" - "5f736c69636563726564656e7469616c5f69645f746f6f5f6269675f736c6963656d70746f6b656e5f69645f746f6f" - "5f6269675f736c6963655f6d70746964666c6f61745f6164645f6f6f625f736c69636531666c6f61745f6164645f6f" - "6f625f736c69636532666c6f61745f7375625f6f6f625f736c69636531666c6f61745f7375625f6f6f625f736c6963" - "6532666c6f61745f6d756c745f6f6f625f736c69636531666c6f61745f6d756c745f6f6f625f736c69636532666c6f" - "61745f6469765f6f6f625f736c69636531666c6f61745f6469765f6f6f625f736c69636532666c6f61745f726f6f74" - "5f6f6f625f736c696365666c6f61745f706f775f6f6f625f736c696365657363726f775f69645f77726f6e675f7369" - "7a655f75696e7433326d70745f69737375616e63655f69645f77726f6e675f73697a655f75696e7433326e66745f6f" - "666665725f69645f77726f6e675f73697a655f75696e7433326f666665725f69645f77726f6e675f73697a655f7569" - "6e7433326f7261636c655f69645f77726f6e675f73697a655f75696e7433327061796368616e5f69645f77726f6e67" - "5f73697a655f75696e7433327065726d697373696f6e65645f646f6d61696e5f69645f77726f6e675f73697a655f75" - "696e7433327469636b65745f69645f77726f6e675f73697a655f75696e7433327661756c745f69645f77726f6e675f" - "73697a655f75696e7433326e66745f7572695f77726f6e675f73697a655f75696e743235366e66745f697373756572" - "5f77726f6e675f73697a655f75696e743235366e66745f7461786f6e5f77726f6e675f73697a655f75696e74323536" - "6e66745f73657269616c5f77726f6e675f73697a655f75696e743235366163636f756e74726f6f745f69645f77726f" - "6e675f73697a655f6163636f756e745f6964636865636b5f69645f77726f6e675f73697a655f6163636f756e745f69" - "6463726564656e7469616c5f69645f77726f6e675f73697a655f6163636f756e745f69643163726564656e7469616c" - "5f69645f77726f6e675f73697a655f6163636f756e745f69643264656c65676174655f69645f77726f6e675f73697a" - "655f6163636f756e745f69643164656c65676174655f69645f77726f6e675f73697a655f6163636f756e745f696432" - "6465706f7369745f707265617574685f69645f77726f6e675f73697a655f6163636f756e745f6964316465706f7369" - "745f707265617574685f69645f77726f6e675f73697a655f6163636f756e745f6964326469645f69645f77726f6e67" - "5f73697a655f6163636f756e745f6964657363726f775f69645f77726f6e675f73697a655f6163636f756e745f6964" - "74727573746c696e655f69645f77726f6e675f73697a655f6163636f756e745f69643174727573746c696e655f6964" - "5f77726f6e675f73697a655f6163636f756e745f6964326d70745f69737375616e63655f69645f77726f6e675f7369" - "7a655f6163636f756e745f69646d70746f6b656e5f69645f77726f6e675f73697a655f6163636f756e745f69646e66" - "745f6f666665725f69645f77726f6e675f73697a655f6163636f756e745f69646f666665725f69645f77726f6e675f" - "73697a655f6163636f756e745f69646f7261636c655f69645f77726f6e675f73697a655f6163636f756e745f696470" - "61796368616e5f69645f77726f6e675f73697a655f6163636f756e745f6964317061796368616e5f69645f77726f6e" - "675f73697a655f6163636f756e745f6964327065726d697373696f6e65645f646f6d61696e5f69645f77726f6e675f" - "73697a655f6163636f756e745f69647369676e6572735f69645f77726f6e675f73697a655f6163636f756e745f6964" - "7469636b65745f69645f77726f6e675f73697a655f6163636f756e745f69647661756c745f69645f77726f6e675f73" - "697a655f6163636f756e745f69646e66745f7572695f77726f6e675f73697a655f6163636f756e745f69646d70746f" - "6b656e5f69645f6d707469645f77726f6e675f6c656e677468004d0970726f64756365727302086c616e6775616765" - "010452757374000c70726f6365737365642d6279010572757374631d312e38372e3020283137303637653961632032" - "3032352d30352d303929002c0f7461726765745f6665617475726573022b0f6d757461626c652d676c6f62616c732b" - "087369676e2d657874"; + "0061736d01000000015c0c60067f7f7f7f7f7f017f60027f7f017f60047f7f7f7f017f60037f7f7f017f60077f7f7f" + "7f7f7f7f017f60087f7f7f7f7f7f7f7f017f60057f7f7f7f7f017f60017f017f60057f7f7f7f7f0060047f7f7f7f00" + "60017f006000017f02ee093708686f73745f6c6962057472616365000808686f73745f6c69620a6c6467725f696e64" + "6578000108686f73745f6c696210706172656e745f6c6467725f74696d65000108686f73745f6c696210706172656e" + "745f6c6467725f68617368000108686f73745f6c696208626173655f666565000108686f73745f6c696211616d656e" + "646d656e745f656e61626c6564000108686f73745f6c69620874785f6669656c64000308686f73745f6c69620e6163" + "636f756e74726f6f745f6964000208686f73745f6c69620863616368655f6c65000308686f73745f6c69620d686f6d" + "655f6c655f6669656c64000308686f73745f6c6962086c655f6669656c64000208686f73745f6c69620874785f696e" + "6e6572000208686f73745f6c69620d686f6d655f6c655f696e6e6572000208686f73745f6c6962086c655f696e6e65" + "72000608686f73745f6c69620a74785f6172725f6c656e000708686f73745f6c69620f686f6d655f6c655f6172725f" + "6c656e000708686f73745f6c69620a6c655f6172725f6c656e000108686f73745f6c69621074785f696e6e65725f61" + "72725f6c656e000108686f73745f6c696215686f6d655f6c655f696e6e65725f6172725f6c656e000108686f73745f" + "6c6962106c655f696e6e65725f6172725f6c656e000308686f73745f6c6962087365745f64617461000108686f7374" + "5f6c69620b7368613531325f68616c66000208686f73745f6c696209636865636b5f736967000008686f73745f6c69" + "62076e66745f757269000008686f73745f6c69620a6e66745f697373756572000208686f73745f6c6962096e66745f" + "7461786f6e000208686f73745f6c6962096e66745f666c616773000108686f73745f6c69620c6e66745f786665725f" + "666565000108686f73745f6c69620a6e66745f73657269616c000208686f73745f6c696208636865636b5f69640000" + "08686f73745f6c69620f666c6f61745f66726f6d5f75696e74000608686f73745f6c69620c74727573746c696e655f" + "6964000508686f73745f6c696206616d6d5f6964000008686f73745f6c69620d63726564656e7469616c5f69640005" + "08686f73745f6c69620a6d70746f6b656e5f6964000008686f73745f6c696209666c6f61745f636d70000208686f73" + "745f6c696209666c6f61745f616464000408686f73745f6c696209666c6f61745f737562000408686f73745f6c6962" + "0a666c6f61745f6d756c74000408686f73745f6c696209666c6f61745f646976000408686f73745f6c69620a666c6f" + "61745f726f6f74000008686f73745f6c696209666c6f61745f706f77000008686f73745f6c696209657363726f775f" + "6964000008686f73745f6c69620f6d70745f69737375616e63655f6964000008686f73745f6c69620c6e66745f6f66" + "6665725f6964000008686f73745f6c6962086f666665725f6964000008686f73745f6c6962096f7261636c655f6964" + "000008686f73745f6c69620a7061796368616e5f6964000508686f73745f6c6962167065726d697373696f6e65645f" + "646f6d61696e5f6964000008686f73745f6c6962097469636b65745f6964000008686f73745f6c6962087661756c74" + "5f6964000008686f73745f6c69620b64656c65676174655f6964000008686f73745f6c6962126465706f7369745f70" + "7265617574685f6964000008686f73745f6c6962066469645f6964000208686f73745f6c69620a7369676e6572735f" + "69640002030403090a0b05030100110619037f01418080c0000b7f0041da98c0000b7f0041e098c0000b073504066d" + "656d6f727902000d657363726f775f66696e69736800390a5f5f646174615f656e6403010b5f5f686561705f626173" + "6503020ac32e037201017f230041106b22042400024002402000200147044020022003410741014100100020004100" + "480d0120042000ad3703080c020b20042000ac370308200220034101200441086a41081000200441106a24000f0b20" + "042000ac3703080b418080c000410b4101200441086a41081000000b2801017f230041106b2201240020012000ac37" + "030841be91c000410b4101200141086a41081000000ba42d02087f017e230041a0026b2200240041c991c000412341" + "0741014100100020004100360260200041e0006a220141041001410441a090c000410a103720004100360260200141" + "041002410441908bc00041101037200042003703782000420037037020004200370368200042003703602001412010" + "03412041f180c0004110103720004100360260200141041004410441ff83c000410810372000428182848890a0c080" + "013703202000428182848890a0c080013703182000428182848890a0c080013703102000428182848890a0c0800137" + "030841ec91c000410e1005410141fa91c00041111037200041086a41201005410141fa91c000411110372000410036" + "02702000420037036820004200370360024002404181802020014114100622014100480d00200141144b0440417321" + "010c010b20014114460d0141818080807821010b20011038000b2000200029006c3700fd01200020002900673703f8" + "01200020002d00623a002e200020002f01603b012c2000200028006336002f200020002903f8013700332000200029" + "00fd013700382000420037037820004200370370200042003703682000420037036002402000412c6a4114200041e0" + "006a4120100722014120470440200141004e0d0120011038000b200020002d00623a0042200020002f01603b014020" + "00200029006f22083703800220002000280063360043200020002900673700472000200837004f2000200029007737" + "0057200020002d007f3a005f200041406b4120410010084101418b92c0004108103720004100360270200042003703" + "682000420037036041818020200041e0006a220241141009411441d38dc000410d1037200041003602702000420037" + "03682000420037036041014181802020024114100a4114418784c0004108103702404100200041e4006a22046b4103" + "71220320046a220120044d0d0020030440200321050340200441003a0000200441016a2104200541016b22050d000b" + "0b200341016b4107490d000340200441003a0000200441076a41003a0000200441066a41003a0000200441056a4100" + "3a0000200441046a41003a0000200441036a41003a0000200441026a41003a0000200441016a41003a000020044108" + "6a22042001470d000b0b2001413c20036b2203417c716a220420014b0440034020014100360200200141046a220120" + "04490d000b0b024020042003410371220320046a22054f0d002003220104400340200441003a0000200441016a2104" + "200141016b22010d000b0b200341016b4107490d000340200441003a0000200441076a41003a0000200441066a4100" + "3a0000200441056a41003a0000200441046a41003a0000200441036a41003a0000200441026a41003a000020044101" + "6a41003a0000200441086a22042005470d000b0b200041043602a00120004181802036026020004100360288022000" + "420037038002200042003703f80120024104200041f8016a22014114100b411441a280c00041081037200041003602" + "88022000420037038002200042003703f801200220002802a00120014114100c411441d084c000410d103720004100" + "360288022000420037038002200042003703f8014101200220002802a00120014114100d411441b08dc00041081037" + "4189803c100e4120419392c000410a10374189803c100f4120419d92c000410f103741014189803c1010412041ac92" + "c000410a1037200220002802a0011011412041b692c00041101037200220002802a0011012412041c692c000411510" + "374101200220002802a0011013412041db92c000411010372000412c6a220341141014411441eb92c0004108103720" + "0042003703900220004200370388022000420037038002200042003703f801200220002802a0012001412010154120" + "418f84c000410b103741f392c000410c41ff92c000410b418a93c000410e10164101419893c0004109103720002000" + "2903203703c001200020002903183703b801200020002903103703b001200020002903083703a801200041003b0188" + "022000420037038002200042003703f80120034114200041a8016a22054120200141121017411241d18fc000410710" + "3720004100360288022000420037038002200042003703f80120054120200141141018411441ac8fc000410a103720" + "0041003602f801200541202001410410194104419790c0004109103720054120101a410841a193c000410910372005" + "4120101b410a41aa93c000410c1037200041003602f8012005412020014104101c410441ba83c000410a103741b693" + "c000410d410420034114100041b693c000410d410541c393c0004108100041b693c000410d410541cb93c000410810" + "00417f41041003417141d393c00041181037200041003602f8012001417f1003417141a888c0004118103720004100" + "3a00fa01200041003b01f801200141031003417d41e790c000411e1037200041003602f8012001418094ebdc031003" + "417341bd8ec000411d10374102100e416f41eb93c00041191037417f20002802a00110114171418494c00041181037" + "2002417f10114171419c94c0004118103720024181081011417441b494c00041191037200041e094ebdc036a220420" + "002802a0011011417341cd94c000411810372000420037039002200042003703880220004200370380022000420037" + "03f801200341142004410820014120101d417341cc8cc0004114103720004200370390022000420037038802200042" + "0037038002200042003703f801200341142003411420014120101d417141918ec00041161037200042003703900220" + "004200370388022000420037038002200042003703f80120044108200141204100101e4173418b80c0004117103720" + "0042003703900220004200370388022000420037038002200042003703f801200220002802a001200141204100101e" + "417141a485c00041201037200420002802a00141011008417341e594c00041101037200220002802a0014101100841" + "7141f594c00041121037200042003703900220004200370388022000420037038002200042003703f8012004200028" + "02a001200141201007417341a78ec00041161037200042003703900220004200370388022000420037038002200042" + "003703f801200220002802a0012001412010074171418e83c000411810372000420037039002200042003703880220" + "00420037038002200042003703f8012003411420034114200420002802a00120014120101f4173418591c000411d10" + "37200042003703900220004200370388022000420037038002200042003703f8012003411420034114200220002802" + "a00120014120101f4171419581c000411f103720004200370390022000420037038802200042003703800220004200" + "3703f80141c698c0004114200420002802a001200141201020417341dc8ac000411510372000420037039002200042" + "00370388022000420037038002200042003703f80141c698c0004114200220002802a0012001412010204171418e89" + "c000411b1037200042003703900220004200370388022000420037038002200042003703f80141c698c00041144187" + "95c0004114200141201020417141a38ac0004125103720004200370390022000420037038802200042003703800220" + "0042003703f801419b95c000412841c698c00041142001412010204171418887c000412110372000200028013c3602" + "dc01200020002901343702d4012000200029012c3702cc01200041808080083602c801200041003b01f801200041c8" + "016a2207411841c698c0004114200141021020417141be80c000410a10372000422a3703e001200420002802a00141" + "01200041e0016a41081000200041003b01f8014102200141021006416f41b481c00041171037200041003b01f80141" + "02200141021009416f41f68ec000411c1037200041003b01f8014101410220014102100a416f41b586c00041171037" + "4102100e416f41eb93c000411910374102100f416f41c395c000411e1037410141021010416f41e195c00041191037" + "41ec91c0004181081005417441fa95c000411f103741ec91c00041c10010054174419996c000411a1037200041003b" + "01f801200241810820014102100b417441a987c00041161037200041003b01f801200241810820014102100c417441" + "aa90c000411b1037200041003b01f8014101200241810820014102100d417441db88c0004116103720024181081011" + "417441b396c000411e103720024181081012417441d196c00041231037410120024181081013417441f496c000411e" + "1037200241810810144174419297c0004116103741b693c00041810841ff92c000410b418a93c000410e1016417441" + "9893c0004109103741b693c000410d41ff92c000418108418a93c000410e10164174419893c0004109103741b693c0" + "00410d41ff92c000410b418a93c00041810810164174419893c00041091037200041003b01f8012002418108200141" + "021015417441c483c00041191037200041003b01f80141c698c00041810841c698c0004114200141021020417441dd" + "82c00041141037200041003b01f80120034114200341142002418108200141021021417441cc86c000411b10372000" + "41003b01f801200741810820034114200141021022417441c389c000411e103741b693c000410d4107200420002802" + "a0011000200042d487b6f4c7d4b1c0003700ec0141b693c000410d4103200041ec95ebdc036a22054108100041b693" + "c000410d4105200420002802a001100020054108200041ec016a220441081023417341a897c0004114103720044108" + "200541081023417341bc97c00041141037200041003b01f80120054108200441082001410241001024417341e08dc0" + "0041141037200041003b01f801200441082005410820014102410010244173418181c00041141037200041003b01f8" + "0120054108200441082001410241001025417341aa80c00041141037200041003b01f8012004410820054108200141" + "0241001025417341e08cc00041141037200041003b01f80120054108200441082001410241001026417341bb84c000" + "41151037200041003b01f80120044108200541082001410241001026417341c98bc00041151037200041003b01f801" + "20054108200441082001410241001027417341a683c00041141037200041003b01f801200441082005410820014102" + "41001027417341c88ac00041141037200041003b01f80120054108410320014102410010284173419488c000411410" + "37200041003b01f8012005410841032001410241001029417341ff85c0004113103720004200370390022000420037" + "0388022000420037038002200042003703f801200341142003411420014120102a417141c088c000411b1037200042" + "003703900220004200370388022000420037038002200042003703f801200341142003411420014120102b417141bc" + "82c00041211037200042003703900220004200370388022000420037038002200042003703f8012003411420034114" + "20014120102c417141928dc000411e1037200042003703900220004200370388022000420037038002200042003703" + "f801200341142003411420014120102d417141928fc000411a10372000420037039002200042003703880220004200" + "37038002200042003703f801200341142003411420014120102e417141b88dc000411b103720004200370390022000" + "4200370388022000420037038002200042003703f80120034114200341142003411420014120102f417141a291c000" + "411c1037200042003703900220004200370388022000420037038002200042003703f8012003411420034114200141" + "201030417141ef81c00041281037200042003703900220004200370388022000420037038002200042003703f80120" + "03411420034114200141201031417141b68fc000411b10372000420037039002200042003703880220004200370380" + "02200042003703f8012003411420034114200141201032417141a989c000411a1037200220002802a0014100100841" + "7141d097c000411b1037200041003b01f80120034114200220002802a001200141021017417141de87c000411a1037" + "200041003b01f801200220002802a001200141021018417141e285c000411d1037200041003b01f801200220002802" + "a001200141021019417141da8ec000411c1037200220002802a001101a417141eb97c000411c1037200220002802a0" + "01101b4171418798c000411f1037200041003602f801200220002802a00120014104101c417141f48dc000411d1037" + "200041003b01f801200220002802a001200141021007417141ff89c00041241037200041808080083602f401200041" + "003b01f801200220002802a001200041f4016a2205410420014102101d417141f48cc000411e1037200041003b01f8" + "01200220002802a00122062003411420022006200141021021417141dd84c00041241037200041003b01f801200341" + "14200220002802a001220620022006200141021021417141cb81c00041241037200041003b01f801200220002802a0" + "0120034114200141021033417141dd83c00041221037200041003b01f80120034114200220002802a0012001410210" + "33417141de8bc00041221037200041003b01f801200220002802a00120034114200141021034417141c880c0004129" + "1037200041003b01f80120034114200220002802a001200141021034417141a08bc00041291037200041003b01f801" + "200220002802a001200141021035417141f887c000411c1037200041003b01f801200220002802a001200541042001" + "4102102a417141f18ac000411f1037200041003b01f801200220002802a00120034114418795c00041142001410210" + "1f4171419286c00041231037200041003b01f80120034114200220002802a001418795c000411420014102101f4171" + "418185c00041231037200041003b01f801200220002802a0012005410420014102102b4171419782c0004125103720" + "0041003b01f80120074118200220002802a001200141021022417141ac8cc00041201037200041003b01f801200220" + "002802a0012005410420014102102c417141c590c00041221037200041003b01f801200220002802a0012005410420" + "014102102d417141e189c000411e1037200041003b01f801200220002802a0012005410420014102102e417141bf87" + "c000411f1037200041003b01f801200220002802a001200341142005410420014102102f417141e786c00041211037" + "200041003b01f80120034114200220002802a0012005410420014102102f4171419a84c00041211037200041003b01" + "f801200220002802a00120054104200141021030417141808cc000412c1037200041003b01f801200220002802a001" + "200141021036417141f78fc00041201037200041003b01f801200220002802a00120054104200141021031417141d8" + "8fc000411f1037200041003b01f801200220002802a00120054104200141021032417141c485c000411e1037200041" + "003b01f801200220002802a00141a698c0004120200141021017417141f182c000411d103741b693c000410d410420" + "0220002802a001100041b6a7abdd03410d410741a698c0004120100041b6a7abdd03410d410320044108100041b6a7" + "abdd03410d410420034114100041b6a7abdd03410d410541cb93c00041081000200220002802a00141072002418108" + "1000200042013703f8012002418108410120014108100041b693c000418108410320044108100041b693c000418108" + "410420034114100041b693c000418108410541cb93c0004108100041b693c000410d4105200220002802a001100020" + "0041003b019e02200220002802a001200341142000419e026a41021022417141f188c000411d103741b693c000410d" + "41e300200220002802a0011000410141004104200341141000200041a0026a240041010f0b000b0bb1180200418080" + "c0000b9b1554455354204641494c4544666c6f61745f66726f6d5f75696e745f6c656e5f6f6f6274785f696e6e6572" + "666c6f61745f7375625f6f6f625f736c69636531616d6d5f69645f6d70746465706f7369745f707265617574685f69" + "645f77726f6e675f73697a655f6163636f756e745f696431706172656e745f6c6467725f68617368666c6f61745f61" + "64645f6f6f625f736c6963653274727573746c696e655f69645f77726f6e675f6c656e5f63757272656e637974785f" + "6669656c645f696e76616c69645f736669656c6463726564656e7469616c5f69645f77726f6e675f73697a655f6163" + "636f756e745f6964327065726d697373696f6e65645f646f6d61696e5f69645f77726f6e675f73697a655f75696e74" + "33326d70745f69737375616e63655f69645f77726f6e675f73697a655f6163636f756e745f69646d70745f69737375" + "616e63655f69645f77726f6e675f73697a655f75696e743332616d6d5f69645f746f6f5f6269675f736c6963656e66" + "745f7572695f77726f6e675f73697a655f6163636f756e745f69646163636f756e74726f6f745f69645f77726f6e67" + "5f6c656e666c6f61745f6469765f6f6f625f736c696365316e66745f73657269616c7368613531325f68616c665f74" + "6f6f5f6269675f736c69636564656c65676174655f69645f77726f6e675f73697a655f6163636f756e745f69643162" + "6173655f6665656c655f6669656c647368613531325f68616c667061796368616e5f69645f77726f6e675f73697a65" + "5f6163636f756e745f696432666c6f61745f6d756c745f6f6f625f736c69636531686f6d655f6c655f696e6e657263" + "726564656e7469616c5f69645f77726f6e675f73697a655f6163636f756e745f69643174727573746c696e655f6964" + "5f77726f6e675f73697a655f6163636f756e745f696432666c6f61745f66726f6d5f75696e745f77726f6e675f6c65" + "6e5f75696e7436347661756c745f69645f77726f6e675f73697a655f6163636f756e745f69646e66745f6973737565" + "725f77726f6e675f73697a655f75696e74323536666c6f61745f706f775f6f6f625f736c69636574727573746c696e" + "655f69645f77726f6e675f73697a655f6163636f756e745f6964316c655f6669656c645f696e76616c69645f736669" + "656c6463726564656e7469616c5f69645f746f6f5f6269675f736c6963657061796368616e5f69645f77726f6e675f" + "73697a655f6163636f756e745f696431616d6d5f69645f6c656e5f77726f6e675f7872705f63757272656e63795f6c" + "656e74785f696e6e65725f746f6f5f6269675f736c6963656f7261636c655f69645f77726f6e675f73697a655f6163" + "636f756e745f69646e66745f7572695f77726f6e675f73697a655f75696e743235366469645f69645f77726f6e675f" + "73697a655f6163636f756e745f6964666c6f61745f726f6f745f6f6f625f736c696365706172656e745f6c6467725f" + "686173685f6e65675f6c656e657363726f775f69645f77726f6e675f73697a655f75696e7433326c655f696e6e6572" + "5f746f6f5f6269675f736c6963656d70746f6b656e5f69645f6d707469645f77726f6e675f6c656e677468616d6d5f" + "69645f6c656e5f77726f6e675f6c656e5f6173736574327661756c745f69645f77726f6e675f73697a655f75696e74" + "33326d70746f6b656e5f69645f746f6f5f6269675f736c6963655f6d707469646f666665725f69645f77726f6e675f" + "73697a655f6163636f756e745f69646163636f756e74726f6f745f69645f77726f6e675f73697a655f6163636f756e" + "745f6964616d6d5f69645f6c656e5f77726f6e675f6e6f6e5f7872705f63757272656e63795f6c656e666c6f61745f" + "6469765f6f6f625f736c69636532616d6d5f69645f6c656e5f6f6f625f617373657432657363726f775f69645f7772" + "6f6e675f73697a655f6163636f756e745f6964706172656e745f6c6467725f74696d656465706f7369745f70726561" + "7574685f69645f77726f6e675f73697a655f6163636f756e745f696432666c6f61745f6d756c745f6f6f625f736c69" + "63653264656c65676174655f69645f77726f6e675f73697a655f6163636f756e745f6964327065726d697373696f6e" + "65645f646f6d61696e5f69645f77726f6e675f73697a655f6163636f756e745f69646d70746f6b656e5f69645f7772" + "6f6e675f73697a655f6163636f756e745f6964636865636b5f69645f6f6f625f6c656e5f753332666c6f61745f7375" + "625f6f6f625f736c69636532636865636b5f69645f77726f6e675f73697a655f6163636f756e745f69646e66745f6f" + "666665725f69645f77726f6e675f73697a655f75696e7433326c655f696e6e65726f7261636c655f69645f77726f6e" + "675f73697a655f75696e743332686f6d655f6c655f6669656c64666c6f61745f6164645f6f6f625f736c696365316e" + "66745f73657269616c5f77726f6e675f73697a655f75696e74323536636865636b5f69645f77726f6e675f6c656e5f" + "7533326163636f756e74726f6f745f69645f6c656e5f6f6f62706172656e745f6c6467725f686173685f6c656e5f74" + "6f6f5f6c6f6e676e66745f7461786f6e5f77726f6e675f73697a655f75696e74323536686f6d655f6c655f6669656c" + "645f696e76616c69645f736669656c646f666665725f69645f77726f6e675f73697a655f75696e7433326e66745f69" + "73737565727469636b65745f69645f77726f6e675f73697a655f75696e7433326e66745f7572697469636b65745f69" + "645f77726f6e675f73697a655f6163636f756e745f69647369676e6572735f69645f77726f6e675f73697a655f6163" + "636f756e745f69646e66745f7461786f6e6c6467725f696e646578686f6d655f6c655f696e6e65725f746f6f5f6269" + "675f736c6963656e66745f6f666665725f69645f77726f6e675f73697a655f6163636f756e745f6964706172656e74" + "5f6c6467725f686173685f6275665f746f6f5f736d616c6c74727573746c696e655f69645f6c656e5f6f6f625f6375" + "7272656e63797061796368616e5f69645f77726f6e675f73697a655f75696e7433326572726f725f636f64653d2424" + "242424205354415254494e47205741534d20455845435554494f4e202424242424746573745f616d656e646d656e74" + "616d656e646d656e745f656e61626c656463616368655f6c6574785f6172725f6c656e686f6d655f6c655f6172725f" + "6c656e6c655f6172725f6c656e74785f696e6e65725f6172725f6c656e686f6d655f6c655f696e6e65725f6172725f" + "6c656e6c655f696e6e65725f6172725f6c656e7365745f6461746174657374206d6573736167657465737420707562" + "6b657974657374207369676e6174757265636865636b5f7369676e66745f666c6167736e66745f786665725f666565" + "74657374696e67207472616365400000000000005f4000000000000000706172656e745f6c6467725f686173685f6e" + "65675f70747274785f6172725f6c656e5f696e76616c69645f736669656c6474785f696e6e65725f6172725f6c656e" + "5f6e65675f70747274785f696e6e65725f6172725f6c656e5f6e65675f6c656e74785f696e6e65725f6172725f6c65" + "6e5f746f6f5f6c6f6e6774785f696e6e65725f6172725f6c656e5f7074725f6f6f6263616368655f6c655f7074725f" + "6f6f6263616368655f6c655f77726f6e675f6c656e55534430303030303030303030303030303030300041c395c000" + "0b8303686f6d655f6c655f6172725f6c656e5f696e76616c69645f736669656c646c655f6172725f6c656e5f696e76" + "616c69645f736669656c64616d656e646d656e745f656e61626c65645f746f6f5f6269675f736c696365616d656e64" + "6d656e745f656e61626c65645f746f6f5f6c6f6e6774785f696e6e65725f6172725f6c656e5f746f6f5f6269675f73" + "6c696365686f6d655f6c655f696e6e65725f6172725f6c656e5f746f6f5f6269675f736c6963656c655f696e6e6572" + "5f6172725f6c656e5f746f6f5f6269675f736c6963657365745f646174615f746f6f5f6269675f736c696365666c6f" + "61745f636d705f6f6f625f736c69636531666c6f61745f636d705f6f6f625f736c6963653263616368655f6c655f77" + "726f6e675f73697a655f75696e743235366e66745f666c6167735f77726f6e675f73697a655f75696e743235366e66" + "745f786665725f6665655f77726f6e675f73697a655f75696e74323536303030303030303030303030303030303030" + "3030303030303030303030303031004d0970726f64756365727302086c616e6775616765010452757374000c70726f" + "6365737365642d6279010572757374631d312e39352e30202835393830373631366520323032362d30342d31342900" + "2c0f7461726765745f6665617475726573022b0f6d757461626c652d676c6f62616c732b087369676e2d657874"; extern std::string const kBadAlignWasmHex = "0061736d01000000011b046000017f60057f7f7f7f7f017f60067f7f7f7f7f7f017f60000002260203656e760f666c" From abfa572370599dceff908ad55c62c154d155ba03 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Sun, 9 Aug 2026 15:49:11 -0400 Subject: [PATCH 86/86] fix: Adapt wasm keylet calls to SeqProxy API (#7890) develop changed every sequence-based `keylet::` factory to take `SeqProxy const&` instead of `std::uint32_t`, and removed the two-argument `mptokenIssuance(seq, issuer)` overload. The wasm host functions and their tests still passed raw sequences, so the branch merged cleanly but did not compile. Wrap the raw sequences at the call sites, matching the idiom develop adopted in its own tests: - `SeqProxy::rawSequence` for check, escrow, nftokenOffer, offer, payChannel, permissionedDomain and vault - `SeqProxy::rawTicket` for ticket - `keylet::mptokenIssuance(makeMptID(seq, issuer))` for the removed overload No computed keylet changes: the factories only read `seq.value()`, and the removed overload was itself defined as `mptokenIssuance(makeMptID( seq, issuer))`. --- src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp | 19 ++- src/test/app/HostFuncImpl_test.cpp | 186 ++++++++++++++------- src/test/app/TestHostFunctions.h | 5 +- 3 files changed, 134 insertions(+), 76 deletions(-) diff --git a/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp b/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp index a16fc071a1..3ffb9ef364 100644 --- a/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp +++ b/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -41,7 +42,7 @@ WasmHostFunctionsImpl::checkKeylet(AccountID const& account, std::uint32_t seq) { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::check(account, seq); + auto const keylet = keylet::check(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -99,7 +100,7 @@ WasmHostFunctionsImpl::escrowKeylet(AccountID const& account, std::uint32_t seq) { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::escrow(account, seq); + auto const keylet = keylet::escrow(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -126,7 +127,7 @@ WasmHostFunctionsImpl::mptokenIssuanceKeylet(AccountID const& issuer, std::uint3 if (!issuer) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::mptokenIssuance(seq, issuer); + auto const keylet = keylet::mptokenIssuance(makeMptID(seq, issuer)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -147,7 +148,7 @@ WasmHostFunctionsImpl::nftokenOfferKeylet(AccountID const& account, std::uint32_ { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::nftokenOffer(account, seq); + auto const keylet = keylet::nftokenOffer(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -156,7 +157,7 @@ WasmHostFunctionsImpl::offerKeylet(AccountID const& account, std::uint32_t seq) { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::offer(account, seq); + auto const keylet = keylet::offer(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -179,7 +180,7 @@ WasmHostFunctionsImpl::paychannelKeylet( return std::unexpected(HostFunctionError::InvalidAccount); if (account == destination) return std::unexpected(HostFunctionError::InvalidParams); - auto const keylet = keylet::payChannel(account, destination, seq); + auto const keylet = keylet::payChannel(account, destination, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -188,7 +189,7 @@ WasmHostFunctionsImpl::permissionedDomainKeylet(AccountID const& account, std::u { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::permissionedDomain(account, seq); + auto const keylet = keylet::permissionedDomain(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -206,7 +207,7 @@ WasmHostFunctionsImpl::ticketKeylet(AccountID const& account, std::uint32_t seq) { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::ticket(account, seq); + auto const keylet = keylet::ticket(account, SeqProxy::rawTicket(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -215,7 +216,7 @@ WasmHostFunctionsImpl::vaultKeylet(AccountID const& account, std::uint32_t seq) { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::vault(account, seq); + auto const keylet = keylet::vault(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp index 8c0bf1ab60..58d4eb4419 100644 --- a/src/test/app/HostFuncImpl_test.cpp +++ b/src/test/app/HostFuncImpl_test.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -382,7 +383,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -409,7 +411,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -439,7 +442,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -470,7 +474,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -497,7 +502,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -561,7 +567,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, 2); + auto const dummyEscrow = keylet::escrow(env.master, SeqProxy::rawSequence(2)); auto const accountKeylet = keylet::account(env.master); { VirtualRuntime vrt; @@ -701,7 +707,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite obj.setFieldV256(sfCredentialIDs, credIds); }); ApplyContext ac = createApplyContext(env, ov, stx); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); { VirtualRuntime vrt; @@ -957,7 +964,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite ApplyContext ac = createApplyContext(env, ov); // Find the escrow ledger object - auto const escrowKeylet = keylet::escrow(env.master, env.seq(env.master) - 1); + auto const escrowKeylet = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) - 1)); BEAST_EXPECT(env.le(escrowKeylet)); VirtualRuntime vrt; @@ -1022,7 +1030,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master) + 5); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5)); VirtualRuntime vrt2; WasmHostFunctionsImpl hfs2(ac, dummyEscrow); @@ -1059,7 +1068,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite ApplyContext ac = createApplyContext(env, ov); auto const accountKeylet = keylet::account(env.master.id()); - auto const escrowKeylet = keylet::escrow(env.master.id(), env.seq(env.master) - 1); + auto const escrowKeylet = + keylet::escrow(env.master.id(), SeqProxy::rawSequence(env.seq(env.master) - 1)); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, escrowKeylet); @@ -1174,7 +1184,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite }); ApplyContext ac = createApplyContext(env, ov, stx); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -1537,7 +1548,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite // hfs.getCurrentLedgerObjNestedField(locator); { - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master) + 5); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5)); VirtualRuntime vrt2; WasmHostFunctionsImpl dummyHfs(ac, dummyEscrow); @@ -1580,7 +1592,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -1824,7 +1837,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite }); ApplyContext ac = createApplyContext(env, ov, stx); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -1930,7 +1944,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master) + 5); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5)); VirtualRuntime vrt2; WasmHostFunctionsImpl dummyHfs(ac, dummyEscrow); @@ -1963,7 +1978,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -2051,7 +2067,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite }); ApplyContext ac = createApplyContext(env, ov, stx); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -2165,7 +2182,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite expectError({sfSigners.getCode()}, HostFunctionError::FieldNotFound); { - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master) + 5); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5)); VirtualRuntime vrt2; WasmHostFunctionsImpl dummyHfs(ac, dummyEscrow); @@ -2205,7 +2223,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -2309,7 +2328,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const escrowKeylet = keylet::escrow(env.master, env.seq(env.master) - 1); + auto const escrowKeylet = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) - 1)); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, escrowKeylet); @@ -2353,7 +2373,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -2509,7 +2530,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -2544,7 +2566,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -2599,7 +2622,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::check(masterID, 1u); + auto const expected = keylet::check(masterID, SeqProxy::rawSequence(1u)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("check_id"), params, result, masterID, toBytes(1u), 1024, 32); if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32)) @@ -2749,7 +2772,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::escrow(masterID, 1u); + auto const expected = keylet::escrow(masterID, SeqProxy::rawSequence(1u)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("escrow_id"), params, result, masterID, toBytes(1u), 1024, 32); if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32)) @@ -2810,7 +2833,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::mptokenIssuance(1u, masterID); + auto const expected = keylet::mptokenIssuance(makeMptID(1u, masterID)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("mpt_issuance_id"), params, result, masterID, toBytes(1u), 1024, 32); @@ -2850,7 +2873,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::nftokenOffer(masterID, 1u); + auto const expected = keylet::nftokenOffer(masterID, SeqProxy::rawSequence(1u)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("nft_offer_id"), params, result, masterID, toBytes(1u), 1024, 32); @@ -2868,7 +2891,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::offer(masterID, 1u); + auto const expected = keylet::offer(masterID, SeqProxy::rawSequence(1u)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("offer_id"), params, result, masterID, toBytes(1u), 1024, 32); if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32)) @@ -2902,7 +2925,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::payChannel(masterID, alice.id(), 1u); + auto const expected = + keylet::payChannel(masterID, alice.id(), SeqProxy::rawSequence(1u)); WasmValVec params(8), result(1); auto* trap = ww( &imp.at("paychan_id"), params, result, masterID, alice.id(), toBytes(1u), 1024, 32); @@ -2946,7 +2970,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::permissionedDomain(masterID, 1u); + auto const expected = keylet::permissionedDomain(masterID, SeqProxy::rawSequence(1u)); WasmValVec params(6), result(1); auto* trap = ww( &imp.at("permissioned_domain_id"), params, result, masterID, toBytes(1u), 1024, 32); @@ -2986,7 +3010,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::ticket(masterID, 1u); + auto const expected = keylet::ticket(masterID, SeqProxy::rawTicket(1u)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("ticket_id"), params, result, masterID, toBytes(1u), 1024, 32); if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32)) @@ -3003,7 +3027,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite } { - auto const expected = keylet::vault(masterID, 1u); + auto const expected = keylet::vault(masterID, SeqProxy::rawSequence(1u)); WasmValVec params(6), result(1); auto* trap = ww(&imp.at("vault_id"), params, result, masterID, toBytes(1u), 1024, 32); if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32)) @@ -3043,7 +3067,7 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(alice, env.seq(alice)); + auto const dummyEscrow = keylet::escrow(alice, SeqProxy::rawSequence(env.seq(alice))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -3179,7 +3203,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -3244,7 +3269,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -3280,7 +3306,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -3326,7 +3353,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -3373,7 +3401,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -3436,7 +3465,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3540,7 +3570,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3583,7 +3614,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3649,7 +3681,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3691,7 +3724,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3730,7 +3764,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3772,7 +3807,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3859,7 +3895,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -3985,7 +4022,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -4037,7 +4075,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite beast::Journal const jlog{sink}; ApplyContext ac = createApplyContext(env, ov, jlog); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl hfs(ac, dummyEscrow); VirtualRuntime vrt; @@ -4074,7 +4113,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4146,7 +4186,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4216,7 +4257,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4421,7 +4463,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4511,7 +4554,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4644,7 +4688,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4775,7 +4820,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -4929,7 +4975,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -5083,7 +5130,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -5266,7 +5314,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -5442,7 +5491,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); WasmHostFunctionsImpl const hfs(ac, dummyEscrow); testcase("float non-canonical"); @@ -5464,7 +5514,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -5658,7 +5709,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -5765,7 +5817,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -5975,7 +6028,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -6178,7 +6232,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); @@ -6228,7 +6283,8 @@ struct HostFuncImpl_test : public beast::unit_test::Suite Env env{*this}; OpenView ov{*env.current()}; ApplyContext ac = createApplyContext(env, ov); - auto const dummyEscrow = keylet::escrow(env.master, env.seq(env.master)); + auto const dummyEscrow = + keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master))); VirtualRuntime vrt; WasmHostFunctionsImpl hfs(ac, dummyEscrow); diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h index 5feeb25783..46e4ef0c7b 100644 --- a/src/test/app/TestHostFunctions.h +++ b/src/test/app/TestHostFunctions.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -288,7 +289,7 @@ public: { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::check(account, seq); + auto const keylet = keylet::check(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; } @@ -308,7 +309,7 @@ public: { if (!account) return std::unexpected(HostFunctionError::InvalidAccount); - auto const keylet = keylet::escrow(account, seq); + auto const keylet = keylet::escrow(account, SeqProxy::rawSequence(seq)); return Bytes{keylet.key.begin(), keylet.key.end()}; }