From 813cec4af3028f731ce5d6a358b1cc82b42da8fd Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <1830604455@qq.com> Date: Mon, 15 Jun 2026 14:49:40 -0400 Subject: [PATCH 1/6] fix account_objects sponsor field wrong check --- src/test/rpc/AccountObjects_test.cpp | 119 ++++++++++++++++-- .../rpc/handlers/account/AccountObjects.cpp | 22 +++- 2 files changed, 127 insertions(+), 14 deletions(-) diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 83a7122b545..2615349a766 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -1398,17 +1398,17 @@ class AccountObjects_test : public beast::unit_test::Suite 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,7 +1421,7 @@ class AccountObjects_test : public beast::unit_test::Suite // 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); } @@ -1443,7 +1443,7 @@ class AccountObjects_test : public beast::unit_test::Suite // 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); @@ -1462,7 +1462,7 @@ class AccountObjects_test : public beast::unit_test::Suite // 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 +1476,107 @@ class AccountObjects_test : public beast::unit_test::Suite BEAST_EXPECT(!foundSponsoredTrustLine); } + // Only the queried side of a shared trust line should determine + // sponsorship classification. + { + Env env2(*this, testableAmendments()); + Account const issuer("issuer"); + Account const user("user"); + Account const sponsor2("sponsor2"); + auto const usd2 = issuer["USD"]; + + env2.fund(XRP(10000), issuer, user, sponsor2); + env2.close(); + + env2(trust(issuer, user["USD"](100))); + env2.close(); + + env2(trust(user, usd2(100))); + env2.close(); + + auto const trustId = keylet::line(user, issuer, usd2.currency); + BEAST_EXPECT(env2.le(trustId)); + + env2( + sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env2.close(); + + auto const line = env2.le(trustId); + BEAST_EXPECT(line); + + 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(env2, user.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 1); + } + { + auto const resp = acctObjsSponsored(env2, user.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env2, issuer.id(), true, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env2, issuer.id(), false, jss::state); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 1); + } + } + + // A Sponsorship object is visible to both sides, but its reserve side + // belongs only to sfOwner. + { + Env env2(*this, testableAmendments()); + Account const owner("owner"); + Account const sponsee("sponsee"); + Account const sponsor2("sponsor2"); + + env2.fund(XRP(10000), owner, sponsee, sponsor2); + env2.close(); + + env2(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(owner)); + env2.close(); + + env2( + sponsor::set(owner, 0, 100, XRP(100)), + sponsor::SponseeAcc(sponsee), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env2.close(); + + auto const sponsorship = env2.le(keylet::sponsor(owner, sponsee)); + BEAST_EXPECT(sponsorship); + BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor)); + + { + auto const resp = acctObjsSponsored(env2, owner.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 1); + } + { + auto const resp = acctObjsSponsored(env2, sponsee.id(), true, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 0); + } + { + auto const resp = acctObjsSponsored(env2, sponsee.id(), false, jss::sponsorship); + auto const& objs = resp[jss::result][jss::account_objects]; + BEAST_EXPECT(objs.size() == 1); + } + } + // NFT page sponsored filter { // Mint an NFT for bob (creates NFT page) @@ -1498,7 +1599,7 @@ class AccountObjects_test : public beast::unit_test::Suite // 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/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index a51737a3928..2ea06862ce8 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -204,16 +204,28 @@ 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(); From 1f45af3d62072e3dc31a68ddd630624875beba49 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <1830604455@qq.com> Date: Mon, 15 Jun 2026 15:07:27 -0400 Subject: [PATCH 2/6] fix Simulate underprices sponsor-multisigned transactions issue --- src/test/rpc/Simulate_test.cpp | 51 +++++++++++++++++++ .../rpc/handlers/transaction/Simulate.cpp | 34 ++++++------- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index ed090245a39..c74f1ab0fb3 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -817,6 +817,56 @@ class Simulate_test : public beast::unit_test::Suite } } + void + testSuccessfulSponsoredTransactionMultisigned() + { + testcase("Successful sponsored multi-signed transaction"); + + using namespace jtx; + Env env(*this); + static auto const kNewDomain = "123ABC"; + 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]; + 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] = kNewDomain; + 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); + + testTx(env, tx, validateOutput, false); + } + void testTransactionSigningFailure() { @@ -1249,6 +1299,7 @@ class Simulate_test : public beast::unit_test::Suite testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); + testSuccessfulSponsoredTransactionMultisigned(); testTransactionSigningFailure(); testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index e0bb8d8a9fc..e4ab4245bbc 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -131,23 +131,6 @@ 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; @@ -172,6 +155,23 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) txJson[jss::NetworkID] = to_string(networkId); } + 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; + } + return std::nullopt; } From cb22ad330b31937772430144bc475e9cc799da42 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <1830604455@qq.com> Date: Tue, 16 Jun 2026 14:28:25 -0400 Subject: [PATCH 3/6] fix ripple_path_find rejects valid sponsored account-creation routes --- src/test/app/Path_test.cpp | 83 +++++++++++++++++++++++- src/test/jtx/impl/paths.cpp | 1 + src/xrpld/rpc/detail/PathRequest.cpp | 45 ++++++++++++- src/xrpld/rpc/detail/PathRequest.h | 1 + src/xrpld/rpc/detail/Pathfinder.cpp | 10 ++- src/xrpld/rpc/detail/Pathfinder.h | 2 + src/xrpld/rpc/detail/PathfinderUtils.h | 43 ++++++++++++ src/xrpld/rpc/detail/TransactionSign.cpp | 5 ++ 8 files changed, 186 insertions(+), 4 deletions(-) diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 4cfe938798d..23ef50c4a14 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -146,7 +146,9 @@ class Path_test : public beast::unit_test::Suite STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, std::optional const& saSrcCurrency = std::nullopt, - std::optional const& domain = std::nullopt) + std::optional const& domain = std::nullopt, + std::optional const& txFlags = std::nullopt, + bool allowError = false) { using namespace jtx; @@ -184,6 +186,8 @@ class Path_test : public beast::unit_test::Suite } if (domain) params[jss::domain] = to_string(*domain); + if (txFlags) + params[jss::Flags] = *txFlags; json::Value result; Gate g; @@ -196,7 +200,8 @@ class Path_test : public beast::unit_test::Suite using namespace std::chrono_literals; BEAST_EXPECT(g.waitFor(5s)); - BEAST_EXPECT(!result.isMember(jss::error)); + if (!allowError) + BEAST_EXPECT(!result.isMember(jss::error)); return result; } @@ -419,6 +424,79 @@ class Path_test : public beast::unit_test::Suite BEAST_EXPECT(std::get<0>(result).empty()); } + void + sponsoredCreateAccountPathFind() + { + testcase("sponsored create account path find"); + using namespace jtx; + + Env env = pathTestEnv(); + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const charlie = Account("charlie"); + auto const dan = Account("dan"); + auto const gw = Account("gw"); + auto const usd = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(usd(100), alice, bob); + env.close(); + env(pay(gw, alice, usd(50))); + env.close(); + + env(offer(bob, usd(50), XRP(5))); + env.close(); + + auto hasAlternatives = [](json::Value const& result) { + return result.isMember(jss::alternatives) && result[jss::alternatives].isArray() && + result[jss::alternatives].size() > 0; + }; + + auto const fundedResult = + findPathsRequest(env, alice, bob, drops(1), std::nullopt, usd.currency); + BEAST_EXPECTS(hasAlternatives(fundedResult), fundedResult.toStyledString()); + + env(pay(alice, charlie, drops(1)), + Path(~XRP), + Sendmax(usd(50)), + Txflags(tfSponsorCreatedAccount)); + env.close(); + + auto const charlieSle = env.le(keylet::account(charlie)); + BEAST_EXPECT(charlieSle); + if (charlieSle) + { + BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsor)); + BEAST_EXPECT(charlieSle->getAccountID(sfSponsor) == alice.id()); + } + + auto const withoutFlag = findPathsRequest( + env, + alice, + dan, + drops(1), + std::nullopt, + usd.currency, + std::nullopt, + std::nullopt, + true); + BEAST_EXPECT(withoutFlag.isMember(jss::error)); + BEAST_EXPECT(withoutFlag[jss::error].asString() == "dstAmtMalformed"); + + auto const withFlag = findPathsRequest( + env, + alice, + dan, + drops(1), + std::nullopt, + usd.currency, + std::nullopt, + tfSponsorCreatedAccount); + BEAST_EXPECTS(hasAlternatives(withFlag), withFlag.toStyledString()); + } + void pathFindConsumeAll(bool const domainEnabled) { @@ -1879,6 +1957,7 @@ class Path_test : public beast::unit_test::Suite trustAutoClearTrustNormalClear(); trustAutoClearTrustAutoClear(); norippleCombinations(); + sponsoredCreateAccountPathFind(); for (bool const domainEnabled : {false, true}) { diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index eb4b36ae4ff..e12278cce33 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -50,6 +50,7 @@ Paths::operator()(Env& env, JTx& jt) const amount, std::nullopt, domain, + false, env.app()); if (!pf.findPaths(depth_)) return; diff --git a/src/xrpld/rpc/detail/PathRequest.cpp b/src/xrpld/rpc/detail/PathRequest.cpp index 3c09917dad0..1f88d96ca0d 100644 --- a/src/xrpld/rpc/detail/PathRequest.cpp +++ b/src/xrpld/rpc/detail/PathRequest.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -196,6 +198,18 @@ PathRequest::isValid(std::shared_ptr const& crCache) return false; } + if (sponsorCreatedAccount_ && !lrLedger->rules().enabled(featureSponsor)) + { + jvStatus_ = rpcError(RpcNotEnabled); + return false; + } + + if (sponsorCreatedAccount_ && !saDstAmount_.native()) + { + jvStatus_ = rpcError(RpcDstAmtMalformed); + return false; + } + auto const sleDest = lrLedger->read(keylet::account(*raDstAccount_)); json::Value& jvDestCur = (jvStatus_[jss::destination_currencies] = json::ValueType::Array); @@ -210,7 +224,8 @@ PathRequest::isValid(std::shared_ptr const& crCache) return false; } - if (!convertAll_ && saDstAmount_ < STAmount(lrLedger->fees().reserve)) + if (!sponsorCreatedAccount_ && !convertAll_ && + saDstAmount_ < STAmount(lrLedger->fees().reserve)) { // Payment must meet reserve. jvStatus_ = rpcError(RpcDstAmtMalformed); @@ -219,6 +234,12 @@ PathRequest::isValid(std::shared_ptr const& crCache) } else { + if (sponsorCreatedAccount_) + { + jvStatus_ = rpcError(RpcInvalidParams); + return false; + } + bool const disallowXRP(sleDest->isFlag(lsfDisallowXRP)); auto const destAssets = accountDestAssets(*raDstAccount_, crCache, !disallowXRP); @@ -320,6 +341,19 @@ PathRequest::parseJson(json::Value const& jvParams) return PFR_PJ_INVALID; } + sponsorCreatedAccount_ = false; + if (jvParams.isMember(jss::Flags)) + { + auto const& flags = jvParams[jss::Flags]; + if (!flags.isUInt() || (flags.asUInt() & ~tfSponsorCreatedAccount) != 0u) + { + jvStatus_ = rpcError(RpcInvalidParams); + return PFR_PJ_INVALID; + } + + sponsorCreatedAccount_ = (flags.asUInt() & tfSponsorCreatedAccount) != 0u; + } + if (jvParams.isMember(jss::send_max)) { // Send_max requires destination amount to be -1. @@ -519,6 +553,7 @@ PathRequest::getPathFinder( dstAmount, saSendMax_, domain_, + sponsorCreatedAccount_, app_); // NOLINTEND(bugprone-unchecked-optional-access) if (pathfinder->findPaths(level, continueCallback)) @@ -625,6 +660,10 @@ PathRequest::findPaths( if (convertAll_) rcInput.partialPaymentAllowed = true; auto sandbox = std::make_unique(&*cache->getLedger(), TapNone); + // NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set + preparePathfindingSandboxForSponsoredDestination( + *sandbox, *raSrcAccount_, *raDstAccount_, sponsorCreatedAccount_); + // NOLINTEND(bugprone-unchecked-optional-access) auto rc = path::RippleCalc::rippleCalculate( *sandbox, saMaxAmount, // --> Amount to send is unlimited @@ -646,6 +685,10 @@ PathRequest::findPaths( ps.pushBack(fullLiquidityPath); sandbox = std::make_unique(&*cache->getLedger(), TapNone); + // NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set + preparePathfindingSandboxForSponsoredDestination( + *sandbox, *raSrcAccount_, *raDstAccount_, sponsorCreatedAccount_); + // NOLINTEND(bugprone-unchecked-optional-access) rc = path::RippleCalc::rippleCalculate( *sandbox, saMaxAmount, // --> Amount to send is unlimited diff --git a/src/xrpld/rpc/detail/PathRequest.h b/src/xrpld/rpc/detail/PathRequest.h index 372223e99f2..9c54fc96f9c 100644 --- a/src/xrpld/rpc/detail/PathRequest.h +++ b/src/xrpld/rpc/detail/PathRequest.h @@ -134,6 +134,7 @@ class PathRequest final : public InfoSubRequest, std::optional raDstAccount_; STAmount saDstAmount_; std::optional saSendMax_; + bool sponsorCreatedAccount_{false}; std::set sciSourceAssets_; std::map context_; diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index 25da86ef8f8..f2e7f7f22ef 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -218,6 +218,7 @@ Pathfinder::Pathfinder( STAmount const& saDstAmount, std::optional const& srcAmount, std::optional const& domain, + bool sponsorCreatedAccount, Application& app) : srcAccount_(uSrcAccount) , dstAccount_(uDstAccount) @@ -228,6 +229,7 @@ Pathfinder::Pathfinder( , srcAmount_(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)) , convertAll_(convertAllCheck(dstAmount_)) , domain_(domain) + , sponsorCreatedAccount_(sponsorCreatedAccount) , ledger_(cache->getLedger()) , rLCache_(cache) , app_(app) @@ -314,7 +316,7 @@ Pathfinder::findPaths(int searchLevel, std::function const& continue } auto const reserve = STAmount(ledger_->fees().reserve); - if (dstAmount_ < reserve) + if (!sponsorCreatedAccount_ && dstAmount_ < reserve) { JLOG(j_.debug()) << "New account not getting enough funding: " << dstAmount_ << " < " << reserve; @@ -401,6 +403,10 @@ Pathfinder::getPathLiquidity( if (convertAll_) rcInput.partialPaymentAllowed = true; + if (!preparePathfindingSandboxForSponsoredDestination( + sandbox, srcAccount_, dstAccount_, sponsorCreatedAccount_)) + return tecPATH_DRY; + auto rc = path::RippleCalc::rippleCalculate( sandbox, srcAmount_, @@ -457,6 +463,8 @@ Pathfinder::computePathRanks(int maxPaths, std::function const& cont try { PaymentSandbox sandbox(&*ledger_, TapNone); + preparePathfindingSandboxForSponsoredDestination( + sandbox, srcAccount_, dstAccount_, sponsorCreatedAccount_); path::RippleCalc::Input rcInput; rcInput.partialPaymentAllowed = true; diff --git a/src/xrpld/rpc/detail/Pathfinder.h b/src/xrpld/rpc/detail/Pathfinder.h index 36caab308e8..d40c4e8e4a0 100644 --- a/src/xrpld/rpc/detail/Pathfinder.h +++ b/src/xrpld/rpc/detail/Pathfinder.h @@ -31,6 +31,7 @@ class Pathfinder : public CountedObject STAmount const& dstAmount, std::optional const& srcAmount, std::optional const& domain, + bool sponsorCreatedAccount, Application& app); Pathfinder(Pathfinder const&) = delete; Pathfinder& @@ -180,6 +181,7 @@ class Pathfinder : public CountedObject STAmount remainingAmount_; bool convertAll_; std::optional domain_; + bool sponsorCreatedAccount_; std::shared_ptr ledger_; std::unique_ptr loadEvent_; diff --git a/src/xrpld/rpc/detail/PathfinderUtils.h b/src/xrpld/rpc/detail/PathfinderUtils.h index a703127148c..0332a6a4244 100644 --- a/src/xrpld/rpc/detail/PathfinderUtils.h +++ b/src/xrpld/rpc/detail/PathfinderUtils.h @@ -1,9 +1,52 @@ #pragma once +#include +#include +#include +#include +#include #include +#include +#include + +#include +#include +#include namespace xrpl { +inline bool +preparePathfindingSandboxForSponsoredDestination( + PaymentSandbox& sandbox, + AccountID const& srcAccount, + AccountID const& dstAccount, + bool sponsorCreatedAccount) +{ + if (!sponsorCreatedAccount || sandbox.exists(keylet::account(dstAccount))) + return true; + + auto const sponsor = sandbox.peek(keylet::account(srcAccount)); + if (!sponsor) + return false; + + auto const currentSponsoringAccountCount = sponsor->getFieldU32(sfSponsoringAccountCount); + if (currentSponsoringAccountCount == std::numeric_limits::max()) + return false; + + sponsor->setFieldU32(sfSponsoringAccountCount, currentSponsoringAccountCount + 1); + sandbox.update(sponsor); + + auto const k = keylet::account(dstAccount); + auto sleDst = std::make_shared(k); + sleDst->setAccountID(sfAccount, dstAccount); + sleDst->setFieldU32(sfSequence, sandbox.seq()); + sleDst->setFieldAmount(sfBalance, XRPAmount(beast::kZero)); + sleDst->setAccountID(sfSponsor, srcAccount); + sandbox.insert(sleDst); + + return true; +} + inline STAmount largestAmount(STAmount const& amt) { diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 8c3a5ea245b..05709966bb3 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -299,6 +300,9 @@ checkPayment( return rpcError(RpcTooBusy); STPathSet result; + bool const sponsorCreatedAccount = txJson.isMember(jss::Flags) && + txJson[jss::Flags].isUInt() && + ((txJson[jss::Flags].asUInt() & tfSponsorCreatedAccount) != 0u); if (auto ledger = app.getOpenLedger().current()) { @@ -311,6 +315,7 @@ checkPayment( amount, std::nullopt, domain, + sponsorCreatedAccount, app); if (pf.findPaths(app.config().pathSearchOld)) { From 268ac8a575ddf48449aff3db0f062633d6e48db6 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <1830604455@qq.com> Date: Tue, 16 Jun 2026 14:46:28 -0400 Subject: [PATCH 4/6] fix simulate RPC Node Crash --- src/test/rpc/Simulate_test.cpp | 14 ++++++++++++++ src/xrpld/rpc/handlers/transaction/Simulate.cpp | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index c74f1ab0fb3..e6f4d5cf3fc 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 'tx.SponsorSignature', not object."); + } { // Invalid transaction json::Value params; diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index e4ab4245bbc..78c778006e3 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -136,7 +136,11 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) if (txJson.isMember(sfSponsorSignature.jsonName)) { - if (auto error = autofillSignature(txJson[sfSponsorSignature.jsonName])) + auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; + if (!sponsorSignature.isObject()) + return RPC::objectFieldError("tx.SponsorSignature"); + + if (auto error = autofillSignature(sponsorSignature)) return error; } From 9468b7ed2d78037880b0231399c91bbb9122fba4 Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <1830604455@qq.com> Date: Thu, 18 Jun 2026 12:13:30 -0400 Subject: [PATCH 5/6] Revert "fix ripple_path_find rejects valid sponsored account-creation routes" This reverts commit cb22ad330b31937772430144bc475e9cc799da42. --- src/test/app/Path_test.cpp | 83 +----------------------- src/test/jtx/impl/paths.cpp | 1 - src/xrpld/rpc/detail/PathRequest.cpp | 45 +------------ src/xrpld/rpc/detail/PathRequest.h | 1 - src/xrpld/rpc/detail/Pathfinder.cpp | 10 +-- src/xrpld/rpc/detail/Pathfinder.h | 2 - src/xrpld/rpc/detail/PathfinderUtils.h | 43 ------------ src/xrpld/rpc/detail/TransactionSign.cpp | 5 -- 8 files changed, 4 insertions(+), 186 deletions(-) diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index 23ef50c4a14..4cfe938798d 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -146,9 +146,7 @@ class Path_test : public beast::unit_test::Suite STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, std::optional const& saSrcCurrency = std::nullopt, - std::optional const& domain = std::nullopt, - std::optional const& txFlags = std::nullopt, - bool allowError = false) + std::optional const& domain = std::nullopt) { using namespace jtx; @@ -186,8 +184,6 @@ class Path_test : public beast::unit_test::Suite } if (domain) params[jss::domain] = to_string(*domain); - if (txFlags) - params[jss::Flags] = *txFlags; json::Value result; Gate g; @@ -200,8 +196,7 @@ class Path_test : public beast::unit_test::Suite using namespace std::chrono_literals; BEAST_EXPECT(g.waitFor(5s)); - if (!allowError) - BEAST_EXPECT(!result.isMember(jss::error)); + BEAST_EXPECT(!result.isMember(jss::error)); return result; } @@ -424,79 +419,6 @@ class Path_test : public beast::unit_test::Suite BEAST_EXPECT(std::get<0>(result).empty()); } - void - sponsoredCreateAccountPathFind() - { - testcase("sponsored create account path find"); - using namespace jtx; - - Env env = pathTestEnv(); - - auto const alice = Account("alice"); - auto const bob = Account("bob"); - auto const charlie = Account("charlie"); - auto const dan = Account("dan"); - auto const gw = Account("gw"); - auto const usd = gw["USD"]; - - env.fund(XRP(10000), alice, bob, gw); - env.close(); - env.trust(usd(100), alice, bob); - env.close(); - env(pay(gw, alice, usd(50))); - env.close(); - - env(offer(bob, usd(50), XRP(5))); - env.close(); - - auto hasAlternatives = [](json::Value const& result) { - return result.isMember(jss::alternatives) && result[jss::alternatives].isArray() && - result[jss::alternatives].size() > 0; - }; - - auto const fundedResult = - findPathsRequest(env, alice, bob, drops(1), std::nullopt, usd.currency); - BEAST_EXPECTS(hasAlternatives(fundedResult), fundedResult.toStyledString()); - - env(pay(alice, charlie, drops(1)), - Path(~XRP), - Sendmax(usd(50)), - Txflags(tfSponsorCreatedAccount)); - env.close(); - - auto const charlieSle = env.le(keylet::account(charlie)); - BEAST_EXPECT(charlieSle); - if (charlieSle) - { - BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsor)); - BEAST_EXPECT(charlieSle->getAccountID(sfSponsor) == alice.id()); - } - - auto const withoutFlag = findPathsRequest( - env, - alice, - dan, - drops(1), - std::nullopt, - usd.currency, - std::nullopt, - std::nullopt, - true); - BEAST_EXPECT(withoutFlag.isMember(jss::error)); - BEAST_EXPECT(withoutFlag[jss::error].asString() == "dstAmtMalformed"); - - auto const withFlag = findPathsRequest( - env, - alice, - dan, - drops(1), - std::nullopt, - usd.currency, - std::nullopt, - tfSponsorCreatedAccount); - BEAST_EXPECTS(hasAlternatives(withFlag), withFlag.toStyledString()); - } - void pathFindConsumeAll(bool const domainEnabled) { @@ -1957,7 +1879,6 @@ class Path_test : public beast::unit_test::Suite trustAutoClearTrustNormalClear(); trustAutoClearTrustAutoClear(); norippleCombinations(); - sponsoredCreateAccountPathFind(); for (bool const domainEnabled : {false, true}) { diff --git a/src/test/jtx/impl/paths.cpp b/src/test/jtx/impl/paths.cpp index e12278cce33..eb4b36ae4ff 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -50,7 +50,6 @@ Paths::operator()(Env& env, JTx& jt) const amount, std::nullopt, domain, - false, env.app()); if (!pf.findPaths(depth_)) return; diff --git a/src/xrpld/rpc/detail/PathRequest.cpp b/src/xrpld/rpc/detail/PathRequest.cpp index 1f88d96ca0d..3c09917dad0 100644 --- a/src/xrpld/rpc/detail/PathRequest.cpp +++ b/src/xrpld/rpc/detail/PathRequest.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -30,7 +29,6 @@ #include #include #include -#include #include #include #include @@ -198,18 +196,6 @@ PathRequest::isValid(std::shared_ptr const& crCache) return false; } - if (sponsorCreatedAccount_ && !lrLedger->rules().enabled(featureSponsor)) - { - jvStatus_ = rpcError(RpcNotEnabled); - return false; - } - - if (sponsorCreatedAccount_ && !saDstAmount_.native()) - { - jvStatus_ = rpcError(RpcDstAmtMalformed); - return false; - } - auto const sleDest = lrLedger->read(keylet::account(*raDstAccount_)); json::Value& jvDestCur = (jvStatus_[jss::destination_currencies] = json::ValueType::Array); @@ -224,8 +210,7 @@ PathRequest::isValid(std::shared_ptr const& crCache) return false; } - if (!sponsorCreatedAccount_ && !convertAll_ && - saDstAmount_ < STAmount(lrLedger->fees().reserve)) + if (!convertAll_ && saDstAmount_ < STAmount(lrLedger->fees().reserve)) { // Payment must meet reserve. jvStatus_ = rpcError(RpcDstAmtMalformed); @@ -234,12 +219,6 @@ PathRequest::isValid(std::shared_ptr const& crCache) } else { - if (sponsorCreatedAccount_) - { - jvStatus_ = rpcError(RpcInvalidParams); - return false; - } - bool const disallowXRP(sleDest->isFlag(lsfDisallowXRP)); auto const destAssets = accountDestAssets(*raDstAccount_, crCache, !disallowXRP); @@ -341,19 +320,6 @@ PathRequest::parseJson(json::Value const& jvParams) return PFR_PJ_INVALID; } - sponsorCreatedAccount_ = false; - if (jvParams.isMember(jss::Flags)) - { - auto const& flags = jvParams[jss::Flags]; - if (!flags.isUInt() || (flags.asUInt() & ~tfSponsorCreatedAccount) != 0u) - { - jvStatus_ = rpcError(RpcInvalidParams); - return PFR_PJ_INVALID; - } - - sponsorCreatedAccount_ = (flags.asUInt() & tfSponsorCreatedAccount) != 0u; - } - if (jvParams.isMember(jss::send_max)) { // Send_max requires destination amount to be -1. @@ -553,7 +519,6 @@ PathRequest::getPathFinder( dstAmount, saSendMax_, domain_, - sponsorCreatedAccount_, app_); // NOLINTEND(bugprone-unchecked-optional-access) if (pathfinder->findPaths(level, continueCallback)) @@ -660,10 +625,6 @@ PathRequest::findPaths( if (convertAll_) rcInput.partialPaymentAllowed = true; auto sandbox = std::make_unique(&*cache->getLedger(), TapNone); - // NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set - preparePathfindingSandboxForSponsoredDestination( - *sandbox, *raSrcAccount_, *raDstAccount_, sponsorCreatedAccount_); - // NOLINTEND(bugprone-unchecked-optional-access) auto rc = path::RippleCalc::rippleCalculate( *sandbox, saMaxAmount, // --> Amount to send is unlimited @@ -685,10 +646,6 @@ PathRequest::findPaths( ps.pushBack(fullLiquidityPath); sandbox = std::make_unique(&*cache->getLedger(), TapNone); - // NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set - preparePathfindingSandboxForSponsoredDestination( - *sandbox, *raSrcAccount_, *raDstAccount_, sponsorCreatedAccount_); - // NOLINTEND(bugprone-unchecked-optional-access) rc = path::RippleCalc::rippleCalculate( *sandbox, saMaxAmount, // --> Amount to send is unlimited diff --git a/src/xrpld/rpc/detail/PathRequest.h b/src/xrpld/rpc/detail/PathRequest.h index 9c54fc96f9c..372223e99f2 100644 --- a/src/xrpld/rpc/detail/PathRequest.h +++ b/src/xrpld/rpc/detail/PathRequest.h @@ -134,7 +134,6 @@ class PathRequest final : public InfoSubRequest, std::optional raDstAccount_; STAmount saDstAmount_; std::optional saSendMax_; - bool sponsorCreatedAccount_{false}; std::set sciSourceAssets_; std::map context_; diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp index f2e7f7f22ef..25da86ef8f8 100644 --- a/src/xrpld/rpc/detail/Pathfinder.cpp +++ b/src/xrpld/rpc/detail/Pathfinder.cpp @@ -218,7 +218,6 @@ Pathfinder::Pathfinder( STAmount const& saDstAmount, std::optional const& srcAmount, std::optional const& domain, - bool sponsorCreatedAccount, Application& app) : srcAccount_(uSrcAccount) , dstAccount_(uDstAccount) @@ -229,7 +228,6 @@ Pathfinder::Pathfinder( , srcAmount_(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)) , convertAll_(convertAllCheck(dstAmount_)) , domain_(domain) - , sponsorCreatedAccount_(sponsorCreatedAccount) , ledger_(cache->getLedger()) , rLCache_(cache) , app_(app) @@ -316,7 +314,7 @@ Pathfinder::findPaths(int searchLevel, std::function const& continue } auto const reserve = STAmount(ledger_->fees().reserve); - if (!sponsorCreatedAccount_ && dstAmount_ < reserve) + if (dstAmount_ < reserve) { JLOG(j_.debug()) << "New account not getting enough funding: " << dstAmount_ << " < " << reserve; @@ -403,10 +401,6 @@ Pathfinder::getPathLiquidity( if (convertAll_) rcInput.partialPaymentAllowed = true; - if (!preparePathfindingSandboxForSponsoredDestination( - sandbox, srcAccount_, dstAccount_, sponsorCreatedAccount_)) - return tecPATH_DRY; - auto rc = path::RippleCalc::rippleCalculate( sandbox, srcAmount_, @@ -463,8 +457,6 @@ Pathfinder::computePathRanks(int maxPaths, std::function const& cont try { PaymentSandbox sandbox(&*ledger_, TapNone); - preparePathfindingSandboxForSponsoredDestination( - sandbox, srcAccount_, dstAccount_, sponsorCreatedAccount_); path::RippleCalc::Input rcInput; rcInput.partialPaymentAllowed = true; diff --git a/src/xrpld/rpc/detail/Pathfinder.h b/src/xrpld/rpc/detail/Pathfinder.h index d40c4e8e4a0..36caab308e8 100644 --- a/src/xrpld/rpc/detail/Pathfinder.h +++ b/src/xrpld/rpc/detail/Pathfinder.h @@ -31,7 +31,6 @@ class Pathfinder : public CountedObject STAmount const& dstAmount, std::optional const& srcAmount, std::optional const& domain, - bool sponsorCreatedAccount, Application& app); Pathfinder(Pathfinder const&) = delete; Pathfinder& @@ -181,7 +180,6 @@ class Pathfinder : public CountedObject STAmount remainingAmount_; bool convertAll_; std::optional domain_; - bool sponsorCreatedAccount_; std::shared_ptr ledger_; std::unique_ptr loadEvent_; diff --git a/src/xrpld/rpc/detail/PathfinderUtils.h b/src/xrpld/rpc/detail/PathfinderUtils.h index 0332a6a4244..a703127148c 100644 --- a/src/xrpld/rpc/detail/PathfinderUtils.h +++ b/src/xrpld/rpc/detail/PathfinderUtils.h @@ -1,52 +1,9 @@ #pragma once -#include -#include -#include -#include -#include #include -#include -#include - -#include -#include -#include namespace xrpl { -inline bool -preparePathfindingSandboxForSponsoredDestination( - PaymentSandbox& sandbox, - AccountID const& srcAccount, - AccountID const& dstAccount, - bool sponsorCreatedAccount) -{ - if (!sponsorCreatedAccount || sandbox.exists(keylet::account(dstAccount))) - return true; - - auto const sponsor = sandbox.peek(keylet::account(srcAccount)); - if (!sponsor) - return false; - - auto const currentSponsoringAccountCount = sponsor->getFieldU32(sfSponsoringAccountCount); - if (currentSponsoringAccountCount == std::numeric_limits::max()) - return false; - - sponsor->setFieldU32(sfSponsoringAccountCount, currentSponsoringAccountCount + 1); - sandbox.update(sponsor); - - auto const k = keylet::account(dstAccount); - auto sleDst = std::make_shared(k); - sleDst->setAccountID(sfAccount, dstAccount); - sleDst->setFieldU32(sfSequence, sandbox.seq()); - sleDst->setFieldAmount(sfBalance, XRPAmount(beast::kZero)); - sleDst->setAccountID(sfSponsor, srcAccount); - sandbox.insert(sleDst); - - return true; -} - inline STAmount largestAmount(STAmount const& amt) { diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 05709966bb3..8c3a5ea245b 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -300,9 +299,6 @@ checkPayment( return rpcError(RpcTooBusy); STPathSet result; - bool const sponsorCreatedAccount = txJson.isMember(jss::Flags) && - txJson[jss::Flags].isUInt() && - ((txJson[jss::Flags].asUInt() & tfSponsorCreatedAccount) != 0u); if (auto ledger = app.getOpenLedger().current()) { @@ -315,7 +311,6 @@ checkPayment( amount, std::nullopt, domain, - sponsorCreatedAccount, app); if (pf.findPaths(app.config().pathSearchOld)) { From a457c4b2bd32cc662b5ac573cf286000b7dbdffb Mon Sep 17 00:00:00 2001 From: Zhiyuan Wang <1830604455@qq.com> Date: Thu, 18 Jun 2026 13:11:21 -0400 Subject: [PATCH 6/6] comment fix --- src/test/rpc/AccountObjects_test.cpp | 107 ++++++++++-------- src/test/rpc/Simulate_test.cpp | 8 +- .../rpc/handlers/account/AccountObjects.cpp | 27 +++-- .../rpc/handlers/transaction/Simulate.cpp | 9 +- 4 files changed, 88 insertions(+), 63 deletions(-) diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 2615349a766..28f3cc7b2bf 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -1428,7 +1428,8 @@ class AccountObjects_test : public beast::unit_test::Suite // 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,6 +1439,8 @@ class AccountObjects_test : public beast::unit_test::Suite // 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)); } @@ -1446,9 +1449,10 @@ class AccountObjects_test : public beast::unit_test::Suite 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 (BEAST_EXPECT(objs.size() == 1)) { + auto const& obj = objs[0u]; + BEAST_EXPECT(obj[sfLedgerEntryType.jsonName] == jss::RippleState); if (obj[sfLedgerEntryType.jsonName] == jss::RippleState) { BEAST_EXPECT( @@ -1479,32 +1483,33 @@ class AccountObjects_test : public beast::unit_test::Suite // Only the queried side of a shared trust line should determine // sponsorship classification. { - Env env2(*this, testableAmendments()); + Env env(*this, testableAmendments()); Account const issuer("issuer"); Account const user("user"); - Account const sponsor2("sponsor2"); - auto const usd2 = issuer["USD"]; + Account const sponsor("sponsor"); + auto const usd = issuer["USD"]; - env2.fund(XRP(10000), issuer, user, sponsor2); - env2.close(); + env.fund(XRP(10000), issuer, user, sponsor); + env.close(); - env2(trust(issuer, user["USD"](100))); - env2.close(); + env(trust(issuer, user["USD"](100))); + env.close(); - env2(trust(user, usd2(100))); - env2.close(); + env(trust(user, usd(100))); + env.close(); - auto const trustId = keylet::line(user, issuer, usd2.currency); - BEAST_EXPECT(env2.le(trustId)); + auto const trustId = keylet::line(user, issuer, usd.currency); + if (!BEAST_EXPECT(env.le(trustId))) + return; - env2( - sponsor::transfer(user, tfSponsorshipCreate, trustId.key), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env2.close(); + env(sponsor::transfer(user, tfSponsorshipCreate, trustId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); - auto const line = env2.le(trustId); - BEAST_EXPECT(line); + 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; @@ -1514,66 +1519,70 @@ class AccountObjects_test : public beast::unit_test::Suite BEAST_EXPECT(!line->isFieldPresent(issuerSponsorField)); { - auto const resp = acctObjsSponsored(env2, user.id(), true, jss::state); + auto const resp = acctObjsSponsored(env, user.id(), true, jss::state); auto const& objs = resp[jss::result][jss::account_objects]; - BEAST_EXPECT(objs.size() == 1); + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); } { - auto const resp = acctObjsSponsored(env2, user.id(), false, jss::state); + 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(env2, issuer.id(), true, jss::state); + 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(env2, issuer.id(), false, jss::state); + auto const resp = acctObjsSponsored(env, issuer.id(), false, jss::state); auto const& objs = resp[jss::result][jss::account_objects]; - BEAST_EXPECT(objs.size() == 1); + 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 env2(*this, testableAmendments()); + Env env(*this, testableAmendments()); Account const owner("owner"); Account const sponsee("sponsee"); - Account const sponsor2("sponsor2"); + Account const sponsor("sponsor"); - env2.fund(XRP(10000), owner, sponsee, sponsor2); - env2.close(); + env.fund(XRP(10000), owner, sponsee, sponsor); + env.close(); - env2(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(owner)); - env2.close(); + env(sponsor::set_reserve(sponsor, 0, 100), sponsor::SponseeAcc(owner)); + env.close(); - env2( - sponsor::set(owner, 0, 100, XRP(100)), + env(sponsor::set(owner, 0, 100, XRP(100)), sponsor::SponseeAcc(sponsee), - sponsor::As(sponsor2, spfSponsorReserve), - Sig(sfSponsorSignature, sponsor2)); - env2.close(); + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); - auto const sponsorship = env2.le(keylet::sponsor(owner, sponsee)); - BEAST_EXPECT(sponsorship); + auto const sponsorship = env.le(keylet::sponsor(owner, sponsee)); + if (!BEAST_EXPECT(sponsorship)) + return; BEAST_EXPECT(sponsorship->isFieldPresent(sfSponsor)); { - auto const resp = acctObjsSponsored(env2, owner.id(), true, jss::sponsorship); + auto const resp = acctObjsSponsored(env, owner.id(), true, jss::sponsorship); auto const& objs = resp[jss::result][jss::account_objects]; - BEAST_EXPECT(objs.size() == 1); + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); } { - auto const resp = acctObjsSponsored(env2, sponsee.id(), true, 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(env2, sponsee.id(), false, jss::sponsorship); + auto const resp = acctObjsSponsored(env, sponsee.id(), false, jss::sponsorship); auto const& objs = resp[jss::result][jss::account_objects]; - BEAST_EXPECT(objs.size() == 1); + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Sponsorship); } } @@ -1584,7 +1593,8 @@ class AccountObjects_test : public beast::unit_test::Suite 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), @@ -1593,7 +1603,10 @@ class AccountObjects_test : public beast::unit_test::Suite 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 diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index e6f4d5cf3fc..0da51872df7 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -376,7 +376,7 @@ 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.SponsorSignature', not object."); + "Invalid field 'SponsorSignature', not object."); } { // Invalid transaction @@ -838,7 +838,6 @@ class Simulate_test : public beast::unit_test::Suite using namespace jtx; Env env(*this); - static auto const kNewDomain = "123ABC"; Account const sponsor("sponsor"); Account const signer("signer"); env.fund(XRP(10000), sponsor, signer); @@ -849,6 +848,7 @@ class Simulate_test : public beast::unit_test::Suite 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); @@ -868,7 +868,7 @@ class Simulate_test : public beast::unit_test::Suite json::Value tx; tx[jss::Account] = env.master.human(); tx[jss::TransactionType] = jss::AccountSet; - tx[sfDomain] = kNewDomain; + tx[sfDomain] = "123ABC"; tx[sfSponsor.jsonName] = sponsor.human(); tx[sfSponsorFlags.jsonName] = spfSponsorFee; tx[sfSponsorSignature.jsonName] = json::ValueType::Object; @@ -878,6 +878,8 @@ class Simulate_test : public beast::unit_test::Suite 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); } diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index 2ea06862ce8..3e1a29efa20 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) @@ -230,7 +232,7 @@ getAccountObjects( }; std::optional const sponsor = getSponsor(); - if (sponsored.has_value() && !sponsoredMatchesFilter(sponsored.value(), sponsor)) + if (hasSponsoredFilter && !sponsoredMatchesFilter(sponsored, sponsor)) canAppend = false; if (canAppend) @@ -381,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()) @@ -392,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 78c778006e3..82c77adb3b2 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -138,9 +138,9 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) { auto& sponsorSignature = txJson[sfSponsorSignature.jsonName]; if (!sponsorSignature.isObject()) - return RPC::objectFieldError("tx.SponsorSignature"); + return RPC::objectFieldError(sfSponsorSignature.jsonName); - if (auto error = autofillSignature(sponsorSignature)) + if (auto const error = autofillSignature(sponsorSignature)) return error; } @@ -161,9 +161,8 @@ 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 + // 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(),