diff --git a/include/xrpl/ledger/ApplyView.h b/include/xrpl/ledger/ApplyView.h index b7abd149bd..afe6b291b2 100644 --- a/include/xrpl/ledger/ApplyView.h +++ b/include/xrpl/ledger/ApplyView.h @@ -413,6 +413,17 @@ public: emptyDirDelete(Keylet const& directory); }; +/** Bundles the mutable ledger view and the transaction being applied. + + Passed together to avoid threading two separate parameters through every + helper that needs both the view (for state reads/writes) and the + transaction (for field inspection and metadata). + + Both members are non-owning references; the caller is responsible for + ensuring that the referenced objects outlive the ApplyViewContext. + + TODO: replace with ApplyContext after it's untangled with xrpl/tx +*/ struct ApplyViewContext { ApplyView& view; diff --git a/include/xrpl/ledger/OwnerCounts.h b/include/xrpl/ledger/OwnerCounts.h index ea0f4008cc..3c399c8c0a 100644 --- a/include/xrpl/ledger/OwnerCounts.h +++ b/include/xrpl/ledger/OwnerCounts.h @@ -43,7 +43,7 @@ struct OwnerCounts } if (x > std::numeric_limits::max()) - return std::numeric_limits::max(); + return std::numeric_limits::max(); // LCOV_EXCL_LINE return static_cast(x); } @@ -65,15 +65,6 @@ struct OwnerCounts return this == &o || (owner == o.owner && sponsored == o.sponsored && sponsoring == o.sponsoring); } - - [[nodiscard]] bool - valid() const - { - int64_t const x = static_cast(owner) - sponsored + sponsoring; - if (x < 0 || x > std::numeric_limits::max()) - return false; - return owner >= sponsored; - } }; } // namespace xrpl diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h index a640a0ca3a..110ebc1d7e 100644 --- a/include/xrpl/ledger/helpers/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -68,7 +68,7 @@ escrowUnlockApplyHelper( if (!ctx.view.exists(trustLineKey) && createAsset) { // Can the account cover the trust line's reserve? - auto const sponsorSle = getTxReserveSponsor(ctx); + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); if (!sponsorSle) return sponsorSle.error(); // LCOV_EXCL_LINE @@ -197,7 +197,7 @@ escrowUnlockApplyHelper( auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver); if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer) { - auto const sponsorSle = getTxReserveSponsor(ctx); + auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest); if (!sponsorSle) return sponsorSle.error(); // LCOV_EXCL_LINE diff --git a/include/xrpl/ledger/helpers/SponsorHelpers.h b/include/xrpl/ledger/helpers/SponsorHelpers.h index 8ebeed3b6b..350f57805d 100644 --- a/include/xrpl/ledger/helpers/SponsorHelpers.h +++ b/include/xrpl/ledger/helpers/SponsorHelpers.h @@ -93,12 +93,6 @@ getLedgerEntryReserveSponsor( SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor); -SLE::const_pointer -getLedgerEntryReserveSponsor( - ReadView const& view, - SLE::const_ref sle, - SF_ACCOUNT const& field = sfSponsor); - void addSponsorToLedgerEntry( SLE::ref sle, diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 316fdbae4b..e805596c00 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1163,8 +1163,9 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, {sfHolder, SoeRequired}, {sfMPTAmount, SoeRequired}, {sfZKProof, SoeRequired}, -}))/** This transaction transfer sponsorship */ +})) +/** This transaction transfers sponsorship on an object/account. */ #if TRANSACTION_INCLUDE # include #endif @@ -1177,7 +1178,7 @@ TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, {sfSponsee, SoeOptional}, })) -/** This transaction create sponsorship object */ +/** This transaction creates a Sponsorship object. */ #if TRANSACTION_INCLUDE # include #endif diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index 577d7c15b8..ac14c3b66d 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -942,8 +942,7 @@ createMPToken( (*mptoken)[sfFlags] = flags; (*mptoken)[sfOwnerNode] = *ownerNode; - if (sponsorSle) - addSponsorToLedgerEntry(mptoken, sponsorSle); + addSponsorToLedgerEntry(mptoken, sponsorSle); view.insert(mptoken); diff --git a/src/libxrpl/ledger/helpers/SponsorHelpers.cpp b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp index c57b415f02..cda85bb609 100644 --- a/src/libxrpl/ledger/helpers/SponsorHelpers.cpp +++ b/src/libxrpl/ledger/helpers/SponsorHelpers.cpp @@ -40,7 +40,7 @@ getTxReserveSponsor(ApplyViewContext ctx) // already checked in Transactor::checkSponsor if (!sle) - return std::unexpected(tecINTERNAL); + return std::unexpected(tecINTERNAL); // LCOV_EXCL_LINE return sle; } return SLE::pointer(); @@ -89,15 +89,6 @@ getLedgerEntryReserveSponsor(ApplyView& view, SLE::const_ref sle, SF_ACCOUNT con return {}; } -SLE::const_pointer -getLedgerEntryReserveSponsor(ReadView const& view, SLE::const_ref sle, SF_ACCOUNT const& field) -{ - auto const sponsorID = getLedgerEntryReserveSponsorID(sle, field); - if (sponsorID) - return view.read(keylet::account(*sponsorID)); - return {}; -} - void addSponsorToLedgerEntry(SLE::ref sle, SLE::const_ref sponsorSle, SF_ACCOUNT const& field) { diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index bfe7019c5b..679e3373e7 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -409,7 +409,7 @@ Transactor::checkSponsor(ReadView const& view, STTx const& tx) // Reserve sponsorship with permissioned delegation is disallowed. if (tx.isFieldPresent(sfDelegate) && isReserveSponsored(tx)) - return terNO_PERMISSION; + return temINVALID; if (auto const sponsorSle = getTxReserveSponsor(view, tx); !sponsorSle) return terNO_ACCOUNT; @@ -641,8 +641,9 @@ Transactor::payFee() } // A co-signed sponsor pays the fee out of its own account balance, but must - // never be charged into its account reserve. Mirror the spendable amount - // computed in checkFee() so the reserve is protected on the apply path too. + // never be charged into its account reserve, and a pre-funded sponsorship's + // fee is capped by sfMaxFee. Mirror the spendable amount computed in + // checkFee() so both limits are enforced on the apply path too. XRPAmount spendable = balance; if (feePayer.type == FeePayerType::SponsorCoSigned) { @@ -650,6 +651,11 @@ Transactor::payFee() // max(balance - reserve, 0) with overflow handling spendable = balance > sponsorReserve ? balance - sponsorReserve : beast::kZero; } + else if (feePayer.type == FeePayerType::SponsorPreFunded && sle->isFieldPresent(sfMaxFee)) + { + auto const cap = sle->getFieldAmount(sfMaxFee).xrp(); + spendable = std::min(spendable, cap); + } // Only sponsor fee-payers reject here on insufficient funds. For an // ordinary account, the fee falls through and is capped by reset(), which diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 538b74515f..8bc98c7aa8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -222,7 +222,6 @@ EscrowFinish::preclaim(PreclaimContext const& ctx) return ret; } } - return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp index 37504fa0a9..fb2c1104ef 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp @@ -273,6 +273,12 @@ SponsorshipTransfer::preclaim(PreclaimContext const& ctx) // be sponsored if (!newSponsorSle || !isSponsored) return tecNO_PERMISSION; + + // Reassigning to the current sponsor would change no state, but would + // still draw down the sponsor's pre-funded reserve budget (and its + // reserve headroom would be double-counted in checkReserve). + if (targetSle->getAccountID(*sponsorField) == ctx.tx.getAccountID(sfSponsor)) + return tecNO_PERMISSION; } else if (ctx.tx.isFlag(tfSponsorshipEnd)) { diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index bfe1d690e3..de7174e5e7 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1029,6 +1029,15 @@ public: BEAST_EXPECT(sle2->isFieldPresent(sfSponsor)); BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id()); + // Reassign to the current sponsor is a no-op and is rejected + env(sponsor::transfer(alice, tfSponsorshipReassign), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2), + Ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(sponsoringAccountCount(env, sponsor2) == 1); + // sponsor 2 accounts adjustAccountXRPBalance(env, sponsor2, accountReserve(env, 3)); env(sponsor::transfer(bob, tfSponsorshipCreate), @@ -1178,6 +1187,15 @@ public: Ter(tecNO_PERMISSION)); env.close(); + // Reassign to the current sponsor is a no-op and is rejected + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor1), + Ter(tecNO_PERMISSION)); + env.close(); + + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + // transfer sponsor env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), sponsor::As(sponsor2, spfSponsorReserve), @@ -1322,6 +1340,17 @@ public: auto sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + // Reassign to the current sponsor is rejected and must not draw + // down the pre-funded reserve budget + env(sponsor::transfer(alice, tfSponsorshipReassign, checkId), + sponsor::As(sponsor1, spfSponsorReserve), + Ter(tecNO_PERMISSION)); + env.close(); + + sponsor1Sle = env.le(keylet::sponsorship(sponsor1, alice)); + BEAST_EXPECT(sponsor1Sle->getFieldU32(sfRemainingOwnerCount) == 99); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor1) == 1); + // transfer sponsor env(sponsor::set_reserve(sponsor2, 0, 100), sponsor::SponseeAcc(alice)); env.close(); @@ -4539,7 +4568,7 @@ public: delegate::As(bob), sponsor::As(sponsor, spfSponsorReserve), Sig(sfSponsorSignature, sponsor), - Ter(terNO_PERMISSION)); + Ter(temINVALID)); } // Pre-funded reserve sponsorship is blocked for delegated transactions. @@ -4555,7 +4584,7 @@ public: env(check::create(alice, carol, XRP(1)), delegate::As(bob), sponsor::As(sponsor, spfSponsorReserve), - Ter(terNO_PERMISSION)); + Ter(temINVALID)); } } diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 9d55b04ee8..6ba6e7de9f 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -1467,8 +1467,8 @@ public: BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0); } - // Only the queried side of a shared trust line should determine - // sponsorship classification. + // A trust line sponsored on either side is classified as sponsored + // for both parties. { Env env(*this, testableAmendments()); Account const issuer("issuer"); @@ -1519,13 +1519,59 @@ public: { auto const resp = acctObjsSponsored(env, issuer.id(), true, jss::state); auto const& objs = resp[jss::result][jss::account_objects]; - BEAST_EXPECT(objs.size() == 0); + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::RippleState); } { 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); + BEAST_EXPECT(objs.size() == 0); + } + } + + // A sponsored Check is classified as sponsored in both the writer's + // and the destination's results. + { + Env env(*this, testableAmendments()); + Account const owner("owner"); + Account const dest("dest"); + Account const sponsor("sponsor"); + + env.fund(XRP(10000), owner, dest, sponsor); + env.close(); + + auto const checkSeq = env.seq(owner); + env(check::create(owner, dest, XRP(1))); + env.close(); + + auto const checkId = keylet::check(owner, checkSeq); + if (!BEAST_EXPECT(env.le(checkId))) + return; + + env(sponsor::transfer(owner, tfSponsorshipCreate, checkId.key), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor)); + env.close(); + + { + auto const sle = env.le(checkId); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(sle->isFieldPresent(sfSponsor)); + } + + for (auto const& acct : {owner.id(), dest.id()}) + { + { + auto const resp = acctObjsSponsored(env, acct, true, jss::check); + auto const& objs = resp[jss::result][jss::account_objects]; + if (BEAST_EXPECT(objs.size() == 1)) + BEAST_EXPECT(objs[0u][sfLedgerEntryType.jsonName] == jss::Check); + } + { + auto const resp = acctObjsSponsored(env, acct, false, jss::check); + BEAST_EXPECT(resp[jss::result][jss::account_objects].size() == 0); + } } } diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index 6a0959e08b..16df733a66 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -378,6 +378,23 @@ class Simulate_test : public beast::unit_test::Suite resp[jss::result][jss::error_message] == "Invalid field 'SponsorSignature', not object."); } + { + // Invalid SponsorSignature.Signers field + json::Value params; + json::Value txJson = json::ValueType::Object; + txJson[jss::TransactionType] = jss::AccountSet; + txJson[jss::Account] = env.master.human(); + json::Value sponsorSignature = json::ValueType::Object; + sponsorSignature[sfSigners] = "1"; + txJson[sfSponsorSignature] = sponsorSignature; + params[jss::tx_json] = txJson; + + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECTS( + resp[jss::result][jss::error_message] == + "Invalid field 'tx.SponsorSignature.Signers'.", + resp.toStyledString()); + } { // Invalid transaction json::Value params; diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index 69681581f7..8967509262 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -197,20 +197,19 @@ getAccountObjects( !typeMatchesFilter(typeFilter.value(), sleNode->getType())) canAppend = false; + // An object counts as sponsored no matter which party's directory + // it was found through; the sponsorship need not belong to + // `account`'s side. std::optional sponsor; - if (sleNode->getType() == ltSPONSORSHIP) + if (sleNode->getType() == ltRIPPLE_STATE) { - // ltSPONSORSHIP is in both parties' directories; only the - // owner's side carries a sponsor. - if (sleNode->getAccountID(sfOwner) == account) - sponsor = getLedgerEntryReserveSponsorID(sleNode); + sponsor = getLedgerEntryReserveSponsorID(sleNode, sfHighSponsor); + if (!sponsor) + sponsor = getLedgerEntryReserveSponsorID(sleNode, sfLowSponsor); } - else if ( - isLedgerEntrySupportedBySponsorship(*sleNode) && - getLedgerEntryOwner(ledger, *sleNode, account).has_value()) + else if (isLedgerEntrySupportedBySponsorship(*sleNode)) { - sponsor = getLedgerEntryReserveSponsorID( - sleNode, getLedgerEntrySponsorField(*sleNode, account)); + sponsor = getLedgerEntryReserveSponsorID(sleNode); } if (!sponsoredMatchesFilter(sponsor)) diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp index 150bb50e6b..0f163c7356 100644 --- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp @@ -77,7 +77,7 @@ getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context) } static std::optional -autofillSignature(json::Value& sigObject) +autofillSignature(json::Value& sigObject, std::string const& fieldPrefix = "tx") { if (!sigObject.isMember(jss::SigningPubKey)) { @@ -88,14 +88,17 @@ autofillSignature(json::Value& sigObject) if (sigObject.isMember(jss::Signers)) { if (!sigObject[jss::Signers].isArray()) - return RPC::invalidFieldError("tx.Signers"); + return RPC::invalidFieldError(fieldPrefix + ".Signers"); // check multisigned signers for (unsigned index = 0; index < sigObject[jss::Signers].size(); index++) { auto& signer = sigObject[jss::Signers][index]; if (!signer.isObject() || !signer.isMember(jss::Signer) || !signer[jss::Signer].isObject()) - return RPC::invalidFieldError("tx.Signers[" + std::to_string(index) + "]"); + { + return RPC::invalidFieldError( + fieldPrefix + ".Signers[" + std::to_string(index) + "]"); + } if (!signer[jss::Signer].isMember(jss::SigningPubKey)) { @@ -141,7 +144,7 @@ autofillTx(json::Value& txJson, RPC::JsonContext& context) if (!sponsorSignature.isObject()) return RPC::objectFieldError(sfSponsorSignature.jsonName); - if (auto const error = autofillSignature(sponsorSignature)) + if (auto const error = autofillSignature(sponsorSignature, "tx.SponsorSignature")) return error; }