more fixes (part 5) (#7779)

This commit is contained in:
Mayukha Vadari
2026-07-09 18:02:15 -04:00
committed by GitHub
parent a065ab6c09
commit 9abeb52051
15 changed files with 149 additions and 57 deletions

View File

@@ -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;

View File

@@ -43,7 +43,7 @@ struct OwnerCounts
}
if (x > std::numeric_limits<std::uint32_t>::max())
return std::numeric_limits<std::uint32_t>::max();
return std::numeric_limits<std::uint32_t>::max(); // LCOV_EXCL_LINE
return static_cast<std::uint32_t>(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<int64_t>(owner) - sponsored + sponsoring;
if (x < 0 || x > std::numeric_limits<std::uint32_t>::max())
return false;
return owner >= sponsored;
}
};
} // namespace xrpl

View File

@@ -68,7 +68,7 @@ escrowUnlockApplyHelper<Issue>(
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<MPTIssue>(
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

View File

@@ -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,

View File

@@ -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 <xrpl/tx/transactors/sponsor/SponsorshipTransfer.h>
#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 <xrpl/tx/transactors/sponsor/SponsorshipSet.h>
#endif

View File

@@ -942,8 +942,7 @@ createMPToken(
(*mptoken)[sfFlags] = flags;
(*mptoken)[sfOwnerNode] = *ownerNode;
if (sponsorSle)
addSponsorToLedgerEntry(mptoken, sponsorSle);
addSponsorToLedgerEntry(mptoken, sponsorSle);
view.insert(mptoken);

View File

@@ -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)
{

View File

@@ -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

View File

@@ -222,7 +222,6 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
return ret;
}
}
return tesSUCCESS;
}

View File

@@ -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))
{

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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<AccountID> 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))

View File

@@ -77,7 +77,7 @@ getAutofillSequence(json::Value const& txJson, RPC::JsonContext& context)
}
static std::optional<json::Value>
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;
}