From a43ae9a3ce7c6530fbaf8a8efb8df872c14b3051 Mon Sep 17 00:00:00 2001 From: tequ Date: Thu, 18 Sep 2025 18:49:53 +0900 Subject: [PATCH] add InvariantCheck for sponsor count --- src/test/app/Invariants_test.cpp | 69 ++++++++++++++++ src/xrpld/app/tx/detail/DeleteAccount.cpp | 2 + src/xrpld/app/tx/detail/InvariantCheck.cpp | 94 ++++++++++++++++++++++ src/xrpld/app/tx/detail/InvariantCheck.h | 59 ++++++++++++++ 4 files changed, 224 insertions(+) diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index ae2a1c45df..dc9c71373d 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -112,6 +112,11 @@ class Invariants_test : public beast::unit_test::suite { terActual = ac.checkInvariants(terActual, fee); BEAST_EXPECT(terExpect == terActual); + if (terExpect != terActual) + { + printf("terActual: %s\n", transHuman(terActual).c_str()); + printf("terExpect: %s\n", transHuman(terExpect).c_str()); + } BEAST_EXPECT( sink.messages().str().starts_with("Invariant failed:") || sink.messages().str().starts_with( @@ -1604,6 +1609,69 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } + void + testSponsorship() + { + using namespace test::jtx; + using namespace std::string_literals; + testcase << "Sponsorship"; + { + auto const expect_message = + "SponsoredOwnerCount does not equal " + "SponsoringOwnerCount delta."; + + doInvariantCheck( + {{expect_message}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoredOwnerCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expect_message}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringOwnerCount, 1); + ac.view().update(sle); + return true; + }); + } + + { + auto const expect_message = + "Invariant failed: Net delta of SponsoringAccountCount does " + "not match net delta of sfSponsorAccount presence."; + + doInvariantCheck( + {{expect_message}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + sle->setFieldU32(sfSponsoringAccountCount, 1); + ac.view().update(sle); + return true; + }); + + doInvariantCheck( + {{expect_message}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + auto const sle = ac.view().peek(keylet::account(A1.id())); + if (!sle) + return false; + sle->setAccountID(sfSponsorAccount, A2.id()); + ac.view().update(sle); + return true; + }); + } + } + public: void run() override @@ -1623,6 +1691,7 @@ public: testNFTokenPageInvariants(); testPermissionedDomainInvariants(); testPermissionedDEX(); + testSponsorship(); } }; diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 45c496730a..9de07e5a51 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -453,6 +453,8 @@ DeleteAccount::doApply() sponsorSle->setFieldU32( sfSponsoringAccountCount, sponsoringAccountCount - 1); view().update(sponsorSle); + + (*src).makeFieldAbsent(sfSponsorAccount); } XRPL_ASSERT( diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index fd1ab52f00..cafc1c92d0 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2037,4 +2037,98 @@ ValidAMM::finalize( return true; } +// Add new sponsorship-related invariants implementations + +void +SponsorshipOwnerCountsMatch::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto getSponsored = + [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoredOwnerCount); + return 0; + }; + auto getSponsoring = + [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringOwnerCount); + return 0; + }; + + std::int64_t const beforeSponsored = getSponsored(before); + std::int64_t const afterSponsored = getSponsored(after); + std::int64_t const beforeSponsoring = getSponsoring(before); + std::int64_t const afterSponsoring = getSponsoring(after); + + deltaSponsoredOwnerCount_ += (afterSponsored - beforeSponsored); + deltaSponsoringOwnerCount_ += (afterSponsoring - beforeSponsoring); +} + +bool +SponsorshipOwnerCountsMatch::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (deltaSponsoredOwnerCount_ != deltaSponsoringOwnerCount_) + { + JLOG(j.fatal()) << "Invariant failed: SponsoredOwnerCount does not " + "equal SponsoringOwnerCount delta."; + return false; + } + + return true; +} + +void +SponsorshipAccountCountMatchesField::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + auto getSponsoringAccountCount = + [](std::shared_ptr const& sle) -> std::uint32_t { + if (sle && sle->getType() == ltACCOUNT_ROOT) + return sle->getFieldU32(sfSponsoringAccountCount); + return 0; + }; + + auto hasSponsorField = [](std::shared_ptr const& sle) -> bool { + return sle && sle->getType() == ltACCOUNT_ROOT && + sle->isFieldPresent(sfSponsorAccount); + }; + + std::int64_t const beforeCount = getSponsoringAccountCount(before); + std::int64_t const afterCount = getSponsoringAccountCount(after); + deltaSponsoringAccountCount_ += (afterCount - beforeCount); + + int const beforePresent = hasSponsorField(before) ? 1 : 0; + int const afterPresent = hasSponsorField(after) ? 1 : 0; + deltaSponsorFieldPresence_ += (afterPresent - beforePresent); +} + +bool +SponsorshipAccountCountMatchesField::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + if (deltaSponsoringAccountCount_ != deltaSponsorFieldPresence_) + { + JLOG(j.fatal()) + << "Invariant failed: Net delta of SponsoringAccountCount does not " + "match net delta of sfSponsorAccount presence."; + return false; + } + + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 529c05ce0e..745be3b365 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -531,6 +531,63 @@ public: beast::Journal const&); }; +/** + * @brief Invariant: Sponsored owner counts are balanced. + * + * The following check is made for every transaction: + * - The sum of all per-account deltas of `sfSponsoredOwnerCount` equals + * the sum of all per-account deltas of `sfSponsoringOwnerCount`. + */ +class SponsorshipOwnerCountsMatch +{ + std::int64_t deltaSponsoredOwnerCount_ = 0; + std::int64_t deltaSponsoringOwnerCount_ = 0; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + +/** + * @brief Invariant: Sponsoring account relationships tracked consistently. + * + * The following check is made for every transaction: + * - The net delta of `sfSponsoringAccountCount` across all accounts equals + * the net delta of the count of ltACCOUNT_ROOT entries having + * `sfSponsorAccount` present (presence transitions only: add/remove). + */ +class SponsorshipAccountCountMatchesField +{ + std::int64_t deltaSponsoringAccountCount_ = 0; + std::int64_t deltaSponsorFieldPresence_ = 0; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + /** * @brief Invariant: Token holder's trustline balance cannot be negative after * Clawback. @@ -716,6 +773,8 @@ using InvariantChecks = std::tuple< NoXRPTrustLines, NoDeepFreezeTrustLinesWithoutFreeze, TransfersNotFrozen, + SponsorshipOwnerCountsMatch, + SponsorshipAccountCountMatchesField, NoBadOffers, NoZeroEscrow, ValidNewAccountRoot,