diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 6643ab2cb1..20de0634a9 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -201,7 +201,7 @@ canWithdraw(ReadView const& view, STTx const& tx); [[nodiscard]] TER doWithdraw( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index 533daa0cf2..a80a290b84 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -17,7 +17,7 @@ namespace xrpl { template TER escrowUnlockApplyHelper( - ApplyViewContext& ctx, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -31,7 +31,7 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyViewContext& ctx, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, @@ -168,7 +168,7 @@ escrowUnlockApplyHelper( template <> inline TER escrowUnlockApplyHelper( - ApplyViewContext& ctx, + ApplyViewContext ctx, Rate lockedRate, SLE::ref sleDest, STAmount const& xrpBalance, diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index 44d4ec437d..5ec51603d1 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -71,7 +71,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue); [[nodiscard]] TER authorizeMPToken( - ApplyViewContext& ctx, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -102,7 +102,7 @@ requireAuth( */ [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyViewContext& ctx, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, @@ -188,7 +188,7 @@ canMPTTradeAndTransfer( [[nodiscard]] TER addEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -196,7 +196,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal); diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index 3cd5b79eee..53c843ec1d 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -229,7 +229,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc /// canAddHolding() in preflight with the same View and Asset [[nodiscard]] TER addEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -237,7 +237,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal); diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index 596a5ae612..be73b3252c 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -24,14 +24,6 @@ isReserveSponsored(STTx const& tx) return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u; } -inline bool -isSponsorReserveCoSigning(STTx const& tx) -{ - if (!tx.isFieldPresent(sfSponsorSignature)) - return false; - return isReserveSponsored(tx); -} - inline std::optional getTxReserveSponsorAccountID(STTx const& tx) { @@ -43,7 +35,7 @@ getTxReserveSponsorAccountID(STTx const& tx) } inline std::expected -getTxReserveSponsor(ApplyViewContext& ctx) +getTxReserveSponsor(ApplyViewContext ctx) { auto const sponsorID = getTxReserveSponsorAccountID(ctx.tx); if (sponsorID) diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 70d4ecab58..c8c42a8c80 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -230,7 +230,7 @@ canAddHolding(ReadView const& view, Asset const& asset); [[nodiscard]] TER addEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -238,7 +238,7 @@ addEmptyHolding( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal); diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index ae4daa861c..0124096a62 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -112,11 +112,11 @@ public: TER checkInvariants(TER const result, XRPAmount const fee); - ApplyViewContext& + ApplyViewContext getApplyViewContext() { - XRPL_ASSERT(view_.has_value() && (&viewCtx_.view == &*view_), "Previous view discarded"); - return viewCtx_; + XRPL_ASSERT(view_.has_value(), "Previous view exists"); + return {.view = *view_, .tx = tx}; } private: @@ -133,11 +133,6 @@ private: // The ID of the batch transaction we are executing under, if seated. std::optional parentBatchId_; - - // A temporary helper object that passes around ApplyContext info - // Only necessary (for now) because the ApplyContext can't be passed into helpers due to - // levelization - ApplyViewContext viewCtx_; }; } // namespace xrpl diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h index 0b3acb5d79..8d4cea1542 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h @@ -63,7 +63,7 @@ public: beast::Journal const& j) override; static std::expected - create(ApplyViewContext& ctx, beast::Journal journal, MPTCreateArgs const& args); + create(ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args); }; } // namespace xrpl diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp index 51b0fcdb4c..de3994ec88 100644 --- a/src/libxrpl/ledger/View.cpp +++ b/src/libxrpl/ledger/View.cpp @@ -432,7 +432,7 @@ canWithdraw(ReadView const& view, STTx const& tx) TER doWithdraw( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& senderAcct, AccountID const& dstAcct, AccountID const& sourceAcct, diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp index 88582479df..620c9270f0 100644 --- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp @@ -369,14 +369,14 @@ checkInsufficientReserve( { if (sponsorSle) { - auto const isCoSigning = isSponsorReserveCoSigning(tx); - auto const sle = view.read( keylet::sponsorship( sponsorSle->getAccountID(sfAccount), accSle->getAccountID(sfAccount))); - // prefunded sponsor should have a sponsorship entry - if (!isCoSigning && !sle) + // A reserve-sponsored tx must carry a sponsor signature + // (cosigning path) and/or have a pre-existing sponsorship SLE + // (prefunded path). Absence of both is an internal invariant break. + if (isReserveSponsored(tx) && !sle && !tx.isFieldPresent(sfSponsorSignature)) return tecINTERNAL; // LCOV_EXCL_LINE if (sle) diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 53cbf74f35..336e670a9d 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -125,7 +125,7 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue) [[nodiscard]] TER addEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, MPTIssue const& mptIssue, @@ -148,7 +148,7 @@ addEmptyHolding( [[nodiscard]] TER authorizeMPToken( - ApplyViewContext& ctx, + ApplyViewContext ctx, XRPAmount const& priorBalance, MPTID const& mptIssuanceID, AccountID const& account, @@ -193,17 +193,16 @@ authorizeMPToken( // - create the MPToken object for the holder auto const isSponsored = ctx.reserveContext.isSponsored(); - auto const isSponsoredAndPreFunded = isSponsored && !isSponsorReserveCoSigning(ctx.tx); // The reserve that is required to create the MPToken. Note // that although the reserve increases with every item // an account owns, in the case of MPTokens we only // *enforce* a reserve if the user owns more than two // items. This is similar to the reserve requirements of trust lines. - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. - if (ownerCount(view, isSponsored ? ctx.reserveContext.sponsorSle : sleAcct, journal) >= 2 || - isSponsoredAndPreFunded) + // The "free-tier" shortcut (ownerCount < 2) does not apply once a sponsor is on + // the tx — the sponsor must always cover the reserve (via balance or prefunded + // budget), so this check always runs for sponsored transactions. + if (isSponsored || ownerCount(view, sleAcct, journal) >= 2) { if (auto const ret = checkInsufficientReserve( view, @@ -285,7 +284,7 @@ authorizeMPToken( [[nodiscard]] TER removeEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, MPTIssue const& mptIssue, beast::Journal journal) @@ -417,7 +416,7 @@ requireAuth( [[nodiscard]] TER enforceMPTokenAuthorization( - ApplyViewContext& ctx, + ApplyViewContext ctx, MPTID const& mptIssuanceID, AccountID const& account, XRPAmount const& priorBalance, // for MPToken authorization diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp index b69eee597e..9eeac028dc 100644 --- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp @@ -633,7 +633,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc TER addEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Issue const& issue, @@ -699,7 +699,7 @@ addEmptyHolding( TER removeEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, Issue const& issue, beast::Journal journal) diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 188d11b456..2a3aeba863 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -473,7 +473,7 @@ canAddHolding(ReadView const& view, Asset const& asset) TER addEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, XRPAmount priorBalance, Asset const& asset, @@ -488,7 +488,7 @@ addEmptyHolding( TER removeEmptyHolding( - ApplyViewContext& ctx, + ApplyViewContext ctx, AccountID const& accountID, Asset const& asset, beast::Journal journal) diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp index 224c934d05..93b0d101af 100644 --- a/src/libxrpl/tx/ApplyContext.cpp +++ b/src/libxrpl/tx/ApplyContext.cpp @@ -40,13 +40,12 @@ ApplyContext::ApplyContext( , journal(journal) , base_(base) , flags_(flags) - , view_{std::in_place, &base_, flags_} , parentBatchId_(parentBatchId) - , viewCtx_{.view = *view_, .tx = tx, .reserveContext = ReserveContext::makeFromTx(*view_, tx)} { XRPL_ASSERT( parentBatchId.has_value() == ((flags_ & TapBatch) == TapBatch), "Parent Batch ID should be set if batch apply flag is set"); + view_.emplace(&base_, flags_); } void diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp index ecc151deba..42cc435a9b 100644 --- a/src/libxrpl/tx/transactors/check/CheckCash.cpp +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -389,8 +388,7 @@ CheckCash::doApply() STAmount const flowDeliver{ optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)}; - ApplyViewContext ctx(psb, ctx_.tx); - auto const sponsorSle = getTxReserveSponsor(ctx); + auto const sponsorSle = getTxReserveSponsor({.view = psb, .tx = ctx_.tx}); if (!sponsorSle) return sponsorSle.error(); // LCOV_EXCL_LINE diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp index 3e5a903b05..f669b1af45 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp @@ -104,7 +104,7 @@ MPTokenIssuanceCreate::preflight(PreflightContext const& ctx) std::expected MPTokenIssuanceCreate::create( - ApplyViewContext& ctx, + ApplyViewContext ctx, beast::Journal journal, MPTCreateArgs const& args) { diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 1c0291e1f7..bce4beacc0 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -334,9 +334,9 @@ TrustSet::doApply() std::uint32_t const uOwnerCount = ownerCount(view(), *sponsorSle ? *sponsorSle : sle, j_); - bool const isSponsoredAndPreFunded = *sponsorSle && !isSponsorReserveCoSigning(ctx_.tx); - // If PreFunded Sponsor, it must be checked whether sufficient - // ReserveCount exists. + // The "free-tier" shortcut (ownerCount < 2) only applies when there is no sponsor. + // With any sponsor on the tx, the sponsor must cover the reserve (via balance or + // prefunded budget), so the reserve check always runs. bool const freeTrustLine = uOwnerCount < 2 && !*sponsorSle; std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0); @@ -540,7 +540,7 @@ TrustSet::doApply() // calling adjustOwnerCount(). if (auto const ret = checkInsufficientReserve( view(), ctx_.tx, sleLowAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + *sponsorSle && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for low account. @@ -569,7 +569,7 @@ TrustSet::doApply() // calling adjustOwnerCount(). if (auto const ret = checkInsufficientReserve( view(), ctx_.tx, sleHighAccount, preFeeBalance_, *sponsorSle, 1, 0, j_); - isSponsoredAndPreFunded && !isTesSuccess(ret)) + *sponsorSle && !isTesSuccess(ret)) return tecINSUF_RESERVE_LINE; // Set reserve for high account. diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index d8c6b46290..04d448e2bc 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -746,6 +746,47 @@ public: } } + void + testSponsoredFreeTierReserve() + { + testcase("Sponsored Free-Tier Reserve"); + using namespace test::jtx; + Account const alice("alice"); + Account const issuer("issuer"); + Account const sponsor("sponsor"); + + // Trust lines and MPTokens normally skip the reserve check when the + // holder's ownerCount < 2 (the "free-tier" / first-two-items shortcut). When the + // tx is sponsored, that shortcut must not apply — the sponsor must + // still cover the reserve. + Env env{*this, testableAmendments()}; + env.fund(XRP(10000), alice, issuer); + // Sponsor is funded just below the reserve required to cover a single + // sponsored item. + env.fund(reserve(env, 1) - drops(1), sponsor); + env.close(); + BEAST_EXPECT(ownerCount(env, alice) == 0); + + MPTTester mptt(env, issuer, {.fund = false}); + mptt.create(); + + // Free-tier trust line cosigned by an undercapitalized sponsor must + // fail — the holder's free-first-two-items shortcut does not let the + // sponsor skip the reserve check. + env(trust(alice, issuer["USD"](100)), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecNO_LINE_INSUF_RESERVE)); + env.close(); + + // Free-tier MPTokenAuthorize must also fail for the same reason. + env(MPTTester::authorizeJV({.account = alice, .id = mptt.issuanceID()}), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + void testTransferSponsor() { @@ -2821,10 +2862,11 @@ public: env(noop(sponsor), ticket::Use(ticketSeq)); env.close(); - // pass (free mptoken) + // pass (free-tier mptoken for the holder, but the sponsor is still + // charged a reserve increment regardless of the ownerCount < 2 shortcut). if (cosigning) { - adjustAccountXRPBalance(env, sponsor, reserve(env, 2) - drops(1)); + adjustAccountXRPBalance(env, sponsor, reserve(env, 2)); env(jv, sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), @@ -3723,6 +3765,7 @@ protected: testSimpleSponsorshipSet(); testPreFundAndCosign(); + testSponsoredFreeTierReserve(); testTransferSponsor(); testSponsorFee(); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index fc8305ac4e..d319b92950 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -1398,17 +1398,17 @@ public: env.close(); // Helper to call account_objects with sponsored filter - auto acctObjsSponsored = [&env]( - AccountID const& acct, - bool sponsored, - std::optional const& type = std::nullopt) { + auto acctObjsSponsored = [](Env& testEnv, + AccountID const& acct, + bool sponsored, + std::optional const& type = std::nullopt) { json::Value params; params[jss::account] = to_string(acct); params[jss::sponsored] = sponsored; if (type) params[jss::type] = *type; params[jss::ledger_index] = "validated"; - return env.rpc("json", "account_objects", to_string(params)); + return testEnv.rpc("json", "account_objects", to_string(params)); }; // Create a sponsorship (alice sponsors bob) @@ -1421,14 +1421,15 @@ public: // sponsored=true should not find any objects for bob (doesn't have any sponsored objects) { - auto const resp = acctObjsSponsored(bob.id(), true); + auto const resp = acctObjsSponsored(env, bob.id(), true); auto const& objs = resp[jss::result][jss::account_objects]; BEAST_EXPECT(objs.size() == 0); } // Now sponsor bob's trust line auto const trustId = keylet::line(bob, gw, usd.currency); - BEAST_EXPECT(env.le(trustId)); + if (!BEAST_EXPECT(env.le(trustId))) + return; env(sponsor::transfer(bob, tfSponsorshipCreate, trustId.key), sponsor::As(sponsor1, spfSponsorReserve), @@ -1438,31 +1439,27 @@ public: // Verify trust line has sponsor field { auto const sle = env.le(trustId); + if (!BEAST_EXPECT(sle)) + return; BEAST_EXPECT(sle->isFieldPresent(sfHighSponsor) || sle->isFieldPresent(sfLowSponsor)); } // sponsored=true on bob should include the sponsored trust line { - auto const resp = acctObjsSponsored(bob.id(), true); + auto const resp = acctObjsSponsored(env, bob.id(), true); auto const& objs = resp[jss::result][jss::account_objects]; - bool foundTrustLine = false; - BEAST_EXPECT(objs.size() == 1); - for (auto const& obj : objs) - { - if (obj[sfLedgerEntryType.jsonName] == jss::RippleState) - { - BEAST_EXPECT( - obj.isMember(sfHighSponsor.jsonName) || - obj.isMember(sfLowSponsor.jsonName)); - foundTrustLine = true; - } - } - BEAST_EXPECT(foundTrustLine); + if (!BEAST_EXPECT(objs.size() == 1)) + return; + + auto const& obj = objs[0u]; + BEAST_EXPECT(obj[sfLedgerEntryType.jsonName] == jss::RippleState); + BEAST_EXPECT( + obj.isMember(sfHighSponsor.jsonName) || obj.isMember(sfLowSponsor.jsonName)); } // sponsored=false on bob should NOT include the sponsored trust line { - auto const resp = acctObjsSponsored(bob.id(), false); + auto const resp = acctObjsSponsored(env, bob.id(), false); auto const& objs = resp[jss::result][jss::account_objects]; bool foundSponsoredTrustLine = false; for (auto const& obj : objs) @@ -1476,6 +1473,112 @@ public: BEAST_EXPECT(!foundSponsoredTrustLine); } + // Only the queried side of a shared trust line should determine + // sponsorship classification. + { + Env env(*this, testableAmendments()); + Account const issuer("issuer"); + Account const user("user"); + Account const sponsor("sponsor"); + auto const usd = issuer["USD"]; + + env.fund(XRP(10000), issuer, user, sponsor); + env.close(); + + env(trust(issuer, user["USD"](100))); + env.close(); + + env(trust(user, usd(100))); + env.close(); + + auto const trustId = keylet::line(user, issuer, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; + + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const line = env.le(trustId); + if (!BEAST_EXPECT(line)) + return; + + auto const userIsHigh = line->getFieldAmount(sfHighLimit).getIssuer() == user.id(); + auto const& userSponsorField = userIsHigh ? sfHighSponsor : sfLowSponsor; + auto const& issuerSponsorField = userIsHigh ? sfLowSponsor : sfHighSponsor; + + BEAST_EXPECT(line->isFieldPresent(userSponsorField)); + BEAST_EXPECT(!line->isFieldPresent(issuerSponsorField)); + + { + auto const resp = acctObjsSponsored(env, user.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + { + auto const resp = acctObjsSponsored(env, user.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, issuer.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); + } + } + + // A Sponsorship object is visible to both sides, but its reserve side + // belongs only to sfOwner. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const sponsee("sponsee"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), owner, sponsee, sponsor); + env.close(); + + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(owner)); + env.close(); + + env(sponsor::set(owner, 0, 100, XRP(100)), + sponsor::SponseeAcc(sponsee), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + auto const sponsorship = env.le(keylet::sponsorship(owner, sponsee)); + if (!BEAST_EXPECT(sponsorship)) + return; + BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor)); + + { + auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); + } + } + // NFT page sponsored filter { // Mint an NFT for bob (creates NFT page) @@ -1483,7 +1586,8 @@ public: env.close(); auto const nftPageKeylet = keylet::nftpageMax(bob); - BEAST_EXPECT(env.le(nftPageKeylet)); + if (!BEAST_EXPECT(env.le(nftPageKeylet))) + return; // Sponsor the NFT page env(sponsor::transfer(bob, tfSponsorshipCreate, nftPageKeylet.key), @@ -1492,13 +1596,16 @@ public: env.close(); // Verify NFT page has sponsor field - BEAST_EXPECT(env.le(nftPageKeylet)->isFieldPresent(sfSponsor)); + auto const nftPage = env.le(nftPageKeylet); + if (!BEAST_EXPECT(nftPage)) + return; + BEAST_EXPECT(nftPage->isFieldPresent(sfSponsor)); // sponsored=true should include the sponsored NFT page // sponsored=false should NOT include the sponsored NFT page for (auto const sponsored : {true, false}) { - auto const resp = acctObjsSponsored(bob.id(), sponsored); + auto const resp = acctObjsSponsored(env, bob.id(), sponsored); auto const& objs = resp[jss::result][jss::account_objects]; bool foundNFTPage = false; for (auto const& obj : objs) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index ed090245a3..0da51872df 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -364,6 +364,20 @@ class Simulate_test : public beast::unit_test::Suite auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT(resp[jss::result][jss::error_message] == "Invalid field 'tx.Signers[0]'."); } + { + // Non-object SponsorSignature field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + txJson[sfSponsorSignature] = ""; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == + "Invalid field 'SponsorSignature', not object."); + } { // Invalid transaction json::Value params; @@ -817,6 +831,58 @@ class Simulate_test : public beast::unit_test::Suite } } + void + testSuccessfulSponsoredTransactionMultisigned() + { + testcase("Successful sponsored multi-signed transaction"); + + using namespace jtx; + Env env(*this); + Account const sponsor("sponsor"); + Account const signer("signer"); + env.fund(XRP(10000), sponsor, signer); + env.close(); + + env(signers(sponsor, 1, {{signer, 1}})); + env.close(); + + auto validateOutput = [&](json::Value const& resp, json::Value const& tx) { + auto const result = resp[jss::result]; + // Verifies Fee autofill counts nested sponsor-signature signers. + auto const expectedFee = env.current()->fees().base * 2; + checkBasicReturnValidity(result, tx, env.seq(env.master), expectedFee); + + BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS"); + BEAST_EXPECT(result[jss::engine_result_code] == 0); + BEAST_EXPECT( + result[jss::engine_result_message] == + "The simulated transaction would have been applied."); + + if (BEAST_EXPECT(result.isMember(jss::meta) || result.isMember(jss::meta_blob))) + { + json::Value const metadata = getJsonMetadata(result); + BEAST_EXPECT(metadata[sfTransactionResult.jsonName] == "tesSUCCESS"); + } + }; + + json::Value tx; + tx[jss::Account] = env.master.human(); + tx[jss::TransactionType] = jss::AccountSet; + tx[sfDomain] = "123ABC"; + tx[sfSponsor.jsonName] = sponsor.human(); + tx[sfSponsorFlags.jsonName] = spfSponsorFee; + tx[sfSponsorSignature.jsonName] = json::ValueType::Object; + tx[sfSponsorSignature.jsonName][sfSigners.jsonName] = json::ValueType::Array; + + json::Value signerObj; + signerObj[sfSigner][jss::Account] = signer.human(); + tx[sfSponsorSignature.jsonName][sfSigners.jsonName].append(signerObj); + + // Leave Fee unset so simulate must autofill it after sponsor signer normalization. + BEAST_EXPECT(!tx.isMember(jss::Fee)); + testTx(env, tx, validateOutput, false); + } + void testTransactionSigningFailure() { @@ -1249,6 +1315,7 @@ public: testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); + testSuccessfulSponsoredTransactionMultisigned(); testTransactionSigningFailure(); testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index a51737a392..3e1a29efa2 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -33,7 +33,8 @@ namespace xrpl { @param dirIndex Begin gathering account objects from this directory. @param entryIndex Begin gathering objects from this directory node. @param limit Maximum number of objects to find. - @param sponsored Whether to filter by sponsored objects. + @param hasSponsoredFilter Whether to filter by sponsored objects. + @param sponsored Whether filtered objects should be sponsored. @param jvResult A JSON result that holds the request objects. */ bool @@ -44,7 +45,8 @@ getAccountObjects( uint256 dirIndex, uint256 entryIndex, std::uint32_t const limit, - std::optional const sponsored, + bool const hasSponsoredFilter, + bool const sponsored, json::Value& jvResult) { // check if dirIndex is valid @@ -103,12 +105,12 @@ getAccountObjects( while (currentPage) { bool canAppendNFT = true; - if (sponsored.has_value()) + if (hasSponsoredFilter) { std::optional const nftSponsor = currentPage->isFieldPresent(sfSponsor) ? currentPage->getAccountID(sfSponsor) : std::optional(std::nullopt); - if (!sponsoredMatchesFilter(sponsored.value(), nftSponsor)) + if (!sponsoredMatchesFilter(sponsored, nftSponsor)) canAppendNFT = false; } if (canAppendNFT) @@ -204,21 +206,33 @@ getAccountObjects( !typeMatchesFilter(typeFilter.value(), sleNode->getType())) canAppend = false; - auto const getSponsor = [&sleNode]() -> std::optional { - if (sleNode->isFieldPresent(sfSponsor)) - return sleNode->getAccountID(sfSponsor); + auto const getSponsor = [&account, &sleNode]() -> std::optional { if (sleNode->getType() == ltRIPPLE_STATE) { - if (sleNode->isFieldPresent(sfHighSponsor)) + if (sleNode->isFlag(lsfHighReserve) && + sleNode->getFieldAmount(sfHighLimit).getIssuer() == account && + sleNode->isFieldPresent(sfHighSponsor)) return sleNode->getAccountID(sfHighSponsor); - if (sleNode->isFieldPresent(sfLowSponsor)) + if (sleNode->isFlag(lsfLowReserve) && + sleNode->getFieldAmount(sfLowLimit).getIssuer() == account && + sleNode->isFieldPresent(sfLowSponsor)) return sleNode->getAccountID(sfLowSponsor); + + return std::nullopt; } + + if (sleNode->getType() == ltSPONSORSHIP && + sleNode->getAccountID(sfOwner) != account) + return std::nullopt; + + if (sleNode->isFieldPresent(sfSponsor)) + return sleNode->getAccountID(sfSponsor); + return std::nullopt; }; std::optional const sponsor = getSponsor(); - if (sponsored.has_value() && !sponsoredMatchesFilter(sponsored.value(), sponsor)) + if (hasSponsoredFilter && !sponsoredMatchesFilter(sponsored, sponsor)) canAppend = false; if (canAppend) @@ -369,8 +383,9 @@ doAccountObjects(RPC::JsonContext& context) return RPC::invalidFieldError(jss::marker); } - std::optional sponsored; - if (params.isMember(jss::sponsored)) + bool const hasSponsoredFilter = params.isMember(jss::sponsored); + bool sponsored = false; + if (hasSponsoredFilter) { auto const& sponsoredJv = params[jss::sponsored]; if (!sponsoredJv.isBool()) @@ -380,7 +395,15 @@ doAccountObjects(RPC::JsonContext& context) } if (!getAccountObjects( - *ledger, accountID, typeFilter, dirIndex, entryIndex, limit, sponsored, result)) + *ledger, + accountID, + typeFilter, + dirIndex, + entryIndex, + limit, + hasSponsoredFilter, + sponsored, + result)) return RPC::invalidFieldError(jss::marker); result[jss::account] = toBase58(accountID); diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index e0bb8d8a9f..82c77adb3b 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -131,29 +131,16 @@ autofillSignature(json::Value& sigObject) static std::optional autofillTx(json::Value& txJson, RPC::JsonContext& context) { - if (!txJson.isMember(jss::Fee)) - { - // autofill Fee - // Must happen after all the other autofills happen - // Error handling/messaging works better that way - auto feeOrError = RPC::getCurrentNetworkFee( - context.role, - context.app.config(), - context.app.getFeeTrack(), - context.app.getTxQ(), - context.app, - txJson); - if (feeOrError.isMember(jss::error)) - return feeOrError; - txJson[jss::Fee] = feeOrError; - } - if (auto error = autofillSignature(txJson)) return error; if (txJson.isMember(sfSponsorSignature.jsonName)) { - if (auto error = autofillSignature(txJson[sfSponsorSignature.jsonName])) + auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; + if (!sponsorSignature.isObject()) + return RPC::objectFieldError(sfSponsorSignature.jsonName); + + if (auto const error = autofillSignature(sponsorSignature)) return error; } @@ -172,6 +159,22 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) txJson[jss::NetworkID] = to_string(networkId); } + if (!txJson.isMember(jss::Fee)) + { + // Autofill Fee after normalizing nested signer fields so the fee + // estimator sees the full transaction shape. + auto feeOrError = RPC::getCurrentNetworkFee( + context.role, + context.app.config(), + context.app.getFeeTrack(), + context.app.getTxQ(), + context.app, + txJson); + if (feeOrError.isMember(jss::error)) + return feeOrError; + txJson[jss::Fee] = feeOrError; + } + return std::nullopt; }