Merge branch 'xrplf/sponsor' of https://github.com/XRPLF/rippled into mvadari/sponsor/reserve-context

This commit is contained in:
Mayukha Vadari
2026-07-06 13:22:00 -04:00
25 changed files with 438 additions and 215 deletions

View File

@@ -83,7 +83,7 @@ accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adju
return accountReserve(view, view.read(keylet::account(id)), j, adj);
}
/** Check if an account has insufficient reserve.
/** Check if an account has sufficient reserve.
*
* The transaction-level reserve sponsor (if any) is taken from
* `ctx.txReserveContext` and, per XLS-68, is only applied when `accSle` is the
@@ -98,7 +98,7 @@ accountReserve(ReadView const& view, AccountID const& id, beast::Journal j, Adju
* @return Transaction result code
*/
[[nodiscard]] TER
checkInsufficientReserve(
checkReserve(
ApplyViewContext ctx,
SLE::const_ref accSle,
STAmount const& accBalance,

View File

@@ -70,7 +70,7 @@ escrowUnlockApplyHelper<Issue>(
{
// Can the account cover the trust line's reserve?
if (auto const ret =
checkInsufficientReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
checkReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
{
JLOG(journal.trace()) << "Trust line does not exist. "
@@ -194,7 +194,7 @@ escrowUnlockApplyHelper<MPTIssue>(
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
{
if (auto const ret =
checkInsufficientReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
checkReserve(ctx, sleDest, xrpBalance, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return ret;

View File

@@ -31,88 +31,35 @@ isReserveSponsored(STTx const& tx)
return (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
}
inline std::optional<AccountID>
getTxReserveSponsorAccountID(STTx const& tx)
{
if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx))
{
return tx.getAccountID(sfSponsor);
}
return {};
}
std::optional<AccountID>
getTxReserveSponsorAccountID(STTx const& tx);
inline std::expected<SLE::const_pointer, TER>
getTxReserveSponsor(ReadView const& view, STTx const& tx)
{
auto const sponsorID = getTxReserveSponsorAccountID(tx);
if (sponsorID)
{
auto sle = view.read(keylet::account(*sponsorID));
std::expected<SLE::const_pointer, TER>
getTxReserveSponsor(ReadView const& view, STTx const& tx);
// already checked in Transactor::checkSponsor
if (!sle)
return std::unexpected(tecINTERNAL);
return sle;
}
return SLE::pointer();
}
std::optional<AccountID>
getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor);
inline std::optional<AccountID>
getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field = sfSponsor)
{
if (sle->isFieldPresent(field))
return sle->getAccountID(field);
return {};
}
inline SLE::pointer
SLE::pointer
getLedgerEntryReserveSponsor(
ApplyView& view,
SLE::const_ref sle,
SF_ACCOUNT const& field = sfSponsor)
{
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
if (sponsorID)
return view.peek(keylet::account(*sponsorID));
return {};
}
SF_ACCOUNT const& field = sfSponsor);
inline SLE::const_pointer
SLE::const_pointer
getLedgerEntryReserveSponsor(
ReadView const& view,
SLE::const_ref sle,
SF_ACCOUNT const& field = sfSponsor)
{
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
if (sponsorID)
return view.read(keylet::account(*sponsorID));
return {};
}
SF_ACCOUNT const& field = sfSponsor);
inline void
void
addSponsorToLedgerEntry(
SLE::ref sle,
SLE::const_ref sponsorSle,
SF_ACCOUNT const& field = sfSponsor)
{
XRPL_ASSERT(
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
"addSponsorToLedgerEntry : Invalid field to the LedgerEntry");
if (sponsorSle)
sle->setAccountID(field, sponsorSle->getAccountID(sfAccount));
}
SF_ACCOUNT const& field = sfSponsor);
inline void
removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor)
{
XRPL_ASSERT(
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
"removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry");
if (sle->isFieldPresent(field))
sle->makeFieldAbsent(field);
}
void
removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field = sfSponsor);
template <typename T>
inline std::optional<AccountID>

View File

@@ -141,6 +141,9 @@ public:
[[nodiscard]] std::vector<uint256> const&
getBatchTransactionIDs() const;
[[nodiscard]] AccountID
getFeePayerID() const;
private:
/** Check the signature.
@param rules The current ledger rules.

View File

@@ -137,7 +137,8 @@ enum class FeePayerType {
struct FeePayer
{
Keylet entry;
AccountID id;
Keylet keylet;
SF_AMOUNT const& balanceField;
FeePayerType type{FeePayerType::Account};
};
@@ -317,6 +318,7 @@ public:
static NotTEC
checkSponsor(ReadView const& view, STTx const& tx);
/////////////////////////////////////////////////////
// Interface used by AccountDelete

View File

@@ -347,7 +347,7 @@ accountReserve(ReadView const& view, SLE::const_ref sle, beast::Journal j, Adjus
}
TER
checkInsufficientReserve(
checkReserve(
ApplyViewContext ctx,
// NOTE: we still need the accSle to be passed in as the ctx.txReserveContext.accountSle may be
// outdated

View File

@@ -206,8 +206,8 @@ authorizeMPToken(
// budget), so this check always runs for sponsored transactions.
if (isSponsored || ownerCount(sleAcct, journal) >= 2)
{
if (auto const ret = checkInsufficientReserve(
ctx, sleAcct, priorBalance, {.ownerCountDelta = 1}, journal);
if (auto const ret =
checkReserve(ctx, sleAcct, priorBalance, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return ret;
}

View File

@@ -662,8 +662,7 @@ addEmptyHolding(
return tecDUPLICATE;
// Can the account cover the trust line reserve ?
if (auto const ret =
checkInsufficientReserve(ctx, sleDst, priorBalance, {.ownerCountDelta = 1}, journal);
if (auto const ret = checkReserve(ctx, sleDst, priorBalance, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return tecNO_LINE_INSUF_RESERVE;

View File

@@ -0,0 +1,93 @@
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <expected>
#include <optional>
namespace xrpl {
std::optional<AccountID>
getTxReserveSponsorAccountID(STTx const& tx)
{
if (tx.isFieldPresent(sfSponsor) && isReserveSponsored(tx))
{
return tx.getAccountID(sfSponsor);
}
return {};
}
std::expected<SLE::const_pointer, TER>
getTxReserveSponsor(ReadView const& view, STTx const& tx)
{
auto const sponsorID = getTxReserveSponsorAccountID(tx);
if (sponsorID)
{
auto sle = view.read(keylet::account(*sponsorID));
// already checked in Transactor::checkSponsor
if (!sle)
return std::unexpected(tecINTERNAL);
return sle;
}
return SLE::pointer();
}
std::optional<AccountID>
getLedgerEntryReserveSponsorAccountID(SLE::const_ref sle, SF_ACCOUNT const& field)
{
if (sle->isFieldPresent(field))
return sle->getAccountID(field);
return {};
}
SLE::pointer
getLedgerEntryReserveSponsor(ApplyView& view, SLE::const_ref sle, SF_ACCOUNT const& field)
{
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
if (sponsorID)
return view.peek(keylet::account(*sponsorID));
return {};
}
SLE::const_pointer
getLedgerEntryReserveSponsor(ReadView const& view, SLE::const_ref sle, SF_ACCOUNT const& field)
{
auto const sponsorID = getLedgerEntryReserveSponsorAccountID(sle, field);
if (sponsorID)
return view.read(keylet::account(*sponsorID));
return {};
}
void
addSponsorToLedgerEntry(SLE::ref sle, SLE::const_ref sponsorSle, SF_ACCOUNT const& field)
{
XRPL_ASSERT(
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
"addSponsorToLedgerEntry : Invalid field to the LedgerEntry");
if (sponsorSle)
sle->setAccountID(field, sponsorSle->getAccountID(sfAccount));
}
void
removeSponsorFromLedgerEntry(SLE::ref sle, SF_ACCOUNT const& field)
{
XRPL_ASSERT(
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
"removeSponsorFromLedgerEntry : Invalid field to the LedgerEntry");
if (sle->isFieldPresent(field))
sle->makeFieldAbsent(field);
}
} // namespace xrpl

View File

@@ -28,6 +28,7 @@
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/jss.h>
@@ -606,6 +607,15 @@ STTx::getBatchTransactionIDs() const
return *batchTxnIds_;
}
AccountID
STTx::getFeePayerID() const
{
if (isFieldPresent(sfSponsor) && ((getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u))
return at(sfSponsor);
return getInitiator();
}
//------------------------------------------------------------------------------
static bool

View File

@@ -578,7 +578,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
return tesSUCCESS;
auto const feePayer = getFeePayer(ctx.view, ctx.tx);
auto const payerSle = ctx.view.read(feePayer.entry);
auto const payerSle = ctx.view.read(feePayer.keylet);
if (!payerSle)
{
@@ -653,9 +653,9 @@ Transactor::payFee()
auto const feePaid = ctx_.tx[sfFee].xrp();
auto const feePayer = getFeePayer(view(), ctx_.tx);
auto const sle = view().peek(feePayer.entry);
auto const sle = view().peek(feePayer.keylet);
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.entry.key);
JLOG(j_.trace()) << "Fee payer: " + to_string(feePayer.id);
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -1306,7 +1306,7 @@ Transactor::reset(XRPAmount fee)
return {tefINTERNAL, beast::kZero};
auto const feePayer = getFeePayer(view(), ctx_.tx);
auto const payerSle = view().peek(feePayer.entry);
auto const payerSle = view().peek(feePayer.keylet);
if (!payerSle)
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
@@ -1375,31 +1375,39 @@ Transactor::getFeePayer(ReadView const& view, STTx const& tx)
{
auto const sponsorID = tx.getAccountID(sfSponsor);
auto const sponseeID = tx.getInitiator();
auto const hasSponsorSignature = tx.isFieldPresent(sfSponsorSignature);
auto const sponsorshipKeylet = keylet::sponsorship(sponsorID, sponseeID);
// if pre-funded sponsorship exists, prefer it
if (hasSponsorSignature && !view.exists(sponsorshipKeylet))
if (view.exists(sponsorshipKeylet))
{
// co-signed
// pre funded
return FeePayer{
.entry = keylet::account(sponsorID),
.balanceField = sfBalance,
.type = FeePayerType::SponsorCoSigned};
.id = sponsorID,
.keylet = sponsorshipKeylet,
.balanceField = sfFeeAmount,
.type = FeePayerType::SponsorPreFunded};
}
// pre funded
// Checked in Transactor::checkSponsor
XRPL_ASSERT(
tx.isFieldPresent(sfSponsorSignature),
"xrpl::getFeePayer has sponsor signature without a sponsorship object");
// co-signed
return FeePayer{
.entry = sponsorshipKeylet,
.balanceField = sfFeeAmount,
.type = FeePayerType::SponsorPreFunded};
.id = sponsorID,
.keylet = keylet::account(sponsorID),
.balanceField = sfBalance,
.type = FeePayerType::SponsorCoSigned};
}
auto const payerAccountKeylet = keylet::account(tx.getInitiator());
AccountID const payerID = tx.getInitiator();
auto const payerAccountKeylet = keylet::account(payerID);
auto const payerType =
tx.isFieldPresent(sfDelegate) ? FeePayerType::Delegate : FeePayerType::Account;
return FeePayer{.entry = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
return FeePayer{
.id = payerID, .keylet = payerAccountKeylet, .balanceField = sfBalance, .type = payerType};
}
// The sole purpose of this function is to provide a convenient, named

View File

@@ -224,7 +224,7 @@ SponsorshipSet::doApply()
if (hasPositiveFeeAmount)
sponsorBalanceAfterFee -= *feeAmount;
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
applyViewContext,
sponsorAccSle,
sponsorBalanceAfterFee.xrp(),
@@ -294,7 +294,7 @@ SponsorshipSet::doApply()
STAmount sponsorBalanceAfterFee = (*sponsorAccSle)[sfBalance];
sponsorBalanceAfterFee -= feeAmountDelta;
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sponsorAccSle,
sponsorBalanceAfterFee.xrp(),

View File

@@ -112,12 +112,18 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
// sfSponsorFlags should not be present if it is ending sponsorship
// sfSponsorFlags should not be present if it is ending sponsorship.
if (ctx.tx.isFieldPresent(sfSponsorFlags))
{
JLOG(ctx.j.debug())
<< "preflight: sfSponsorFlags should not be present when ending sponsorship";
// Unreachable: reaching here means sfSponsor is absent, which is already checked above,
// and preflight1Sponsor already rejects sfSponsorFlags present without sfSponsor with
// temINVALID_FLAG. Keep this as a defensive check.
// LCOV_EXCL_START
UNREACHABLE(
"xrpl::SponsorshipTransfer::preflight : sfSponsorFlags present without sfSponsor "
"when ending sponsorship");
return temINVALID_FLAG;
// LCOV_EXCL_STOP
}
if (ctx.tx.isFieldPresent(sfSponsee) &&
@@ -149,102 +155,79 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx)
TER
SponsorshipTransfer::preclaim(PreclaimContext const& ctx)
{
auto const index = ctx.tx[~sfObjectID];
auto const objectID = ctx.tx[~sfObjectID];
auto const newSponsorSleExpected = getTxReserveSponsor(ctx.view, ctx.tx);
if (!newSponsorSleExpected)
return newSponsorSleExpected.error(); // LCOV_EXCL_LINE
auto const newSponsorSle = *newSponsorSleExpected;
bool const isObjectSponsor = !!index;
auto const account = ctx.tx[sfAccount];
auto const sponseeID = ctx.tx[~sfSponsee].value_or(account);
auto const sponseeSle = ctx.view.read(keylet::account(sponseeID));
if (!sponseeSle)
return tecINTERNAL; // LCOV_EXCL_LINE
if (isObjectSponsor)
{
auto const sle = ctx.view.read(keylet::unchecked(*index));
if (!sle)
// If it is ending sponsorship, sfSponsee is user input, return terNO_ACCOUNT if it does not
// exist.
if (ctx.tx.isFieldPresent(sfSponsee))
return terNO_ACCOUNT;
// If it is creating or reassigning sponsorship, sfSponsee is the account itself, which is
// always present by the time preclaim runs. Return tecINTERNAL if it does not exist.
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Default setup with an account sponsorship transfer. If it is an object transfer, they will be
// overridden to the object SLE and its type-specific sponsor field:
// sfHighSponsor/sfLowSponsor for a RippleState, sfSponsor for other object types.
SLE::const_pointer targetSle = sponseeSle;
auto const* sponsorField = &sfSponsor;
if (objectID.has_value())
{
auto const objectSle = ctx.view.read(keylet::unchecked(*objectID));
if (!objectSle)
return tecNO_ENTRY;
if (!isLedgerEntrySupportedBySponsorship(sle))
if (!isLedgerEntrySupportedBySponsorship(objectSle))
return tecNO_PERMISSION;
auto const owner = getLedgerEntryOwner(ctx.view, sle, sponseeID);
auto const owner = getLedgerEntryOwner(ctx.view, objectSle, sponseeID);
if (!owner.has_value() || owner.value() != sponseeID)
return tecNO_PERMISSION;
auto const& sponsorField = getLedgerEntrySponsorField(sle, owner.value());
if (ctx.tx.isFlag(tfSponsorshipCreate))
{
if (!newSponsorSle)
return tecNO_PERMISSION;
// check that the object is not sponsored yet
if (sle->isFieldPresent(sponsorField))
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipReassign))
{
if (!newSponsorSle)
return tecNO_PERMISSION;
// check object is already ctx.sponsored
if (!sle->isFieldPresent(sponsorField))
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipEnd))
{
if (newSponsorSle)
return tecNO_PERMISSION;
// check object is sponsored
if (!sle->isFieldPresent(sponsorField))
return tecNO_PERMISSION;
// only the sponsor or sponsee can end sponsorship
auto const sponsor = sle->getAccountID(sponsorField);
if (account != sponsor && account != sponseeID)
return tecNO_PERMISSION;
}
// Object transfer: the target is the object, and its sponsor field
// depends on the object type, a RippleState stores the sponsor in
// sfHighSponsor/sfLowSponsor, while other object type uses sfSponsor.
targetSle = objectSle;
sponsorField = &getLedgerEntrySponsorField(objectSle, owner.value());
}
else
bool const isSponsored = targetSle->isFieldPresent(*sponsorField);
if (ctx.tx.isFlag(tfSponsorshipCreate))
{
if (ctx.tx.isFlag(tfSponsorshipCreate))
{
if (!newSponsorSle)
return tecNO_PERMISSION;
// Creating a new sponsorship: needs a new reserve sponsor, and the
// target must not already be sponsored
if (!newSponsorSle || isSponsored)
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipReassign))
{
// Reassigning sponsorship: needs a new reserve sponsor, and the target must already
// be sponsored
if (!newSponsorSle || !isSponsored)
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipEnd))
{
// Ending sponsorship: no new reserve sponsor, the target must be sponsored.
if (newSponsorSle || !isSponsored)
return tecNO_PERMISSION;
// check account is not sponsored yet
if (sponseeSle->isFieldPresent(sfSponsor))
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipReassign))
{
if (!newSponsorSle)
return tecNO_PERMISSION;
// check account is already sponsored
if (!sponseeSle->isFieldPresent(sfSponsor))
return tecNO_PERMISSION;
}
else if (ctx.tx.isFlag(tfSponsorshipEnd))
{
if (newSponsorSle)
return tecNO_PERMISSION;
// check account is sponsored
if (!sponseeSle->isFieldPresent(sfSponsor))
return tecNO_PERMISSION;
// only the sponsor or sponsee can end sponsorship
auto const sponsor = sponseeSle->getAccountID(sfSponsor);
if (account != sponsor && account != sponseeID)
return tecNO_PERMISSION;
}
// Only the sponsor or sponsee can end sponsorship.
auto const sponsor = targetSle->getAccountID(*sponsorField);
if (account != sponsor && account != sponseeID)
return tecNO_PERMISSION;
}
return tesSUCCESS;
@@ -344,7 +327,7 @@ SponsorshipTransfer::doApply()
// check new sponsor have sufficient balance
// NOLINTNEXTLINE(readability-suspicious-call-argument)
if (auto const ter = checkInsufficientReserve(
if (auto const ter = checkReserve(
ctx_.getApplyViewContext(),
sponseeSle,
sponseeSle->getFieldAmount(sfBalance),
@@ -396,7 +379,7 @@ SponsorshipTransfer::doApply()
// check new sponsor have sufficient balance
// NOLINTNEXTLINE(readability-suspicious-call-argument)
if (auto const ter = checkInsufficientReserve(
if (auto const ter = checkReserve(
ctx_.getApplyViewContext(),
sponseeSle,
sponseeSle->getFieldAmount(sfBalance),
@@ -443,7 +426,7 @@ SponsorshipTransfer::doApply()
// The owner takes the reserve burden back when the object is
// no longer sponsored.
if (auto const ter = checkInsufficientReserve(
if (auto const ter = checkReserve(
ctx_.getApplyViewContext(),
ownerSle,
balanceBeforeFee(ownerSle),
@@ -482,7 +465,7 @@ SponsorshipTransfer::doApply()
if (!newSponsorSle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (auto const ter = checkInsufficientReserve(
if (auto const ter = checkReserve(
ctx_.getApplyViewContext(),
sponseeSle,
sponseeSle->getFieldAmount(sfBalance),
@@ -509,7 +492,7 @@ SponsorshipTransfer::doApply()
if (!newSponsorSle)
return tefINTERNAL; // LCOV_EXCL_LINE
if (auto const ter = checkInsufficientReserve(
if (auto const ter = checkReserve(
ctx_.getApplyViewContext(),
sponseeSle,
sponseeSle->getFieldAmount(sfBalance),
@@ -544,7 +527,7 @@ SponsorshipTransfer::doApply()
// The sponsee must be able to hold its own account reserve after
// the sponsorship is removed.
if (auto const ter = checkInsufficientReserve(
if (auto const ter = checkReserve(
ctx_.getApplyViewContext(),
sponseeSle,
balanceBeforeFee(sponseeSle),

View File

@@ -321,7 +321,7 @@ SignerListSet::replaceSignerList()
// with TicketCreate.
auto const applyViewContext = ctx_.getApplyViewContext();
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sle,
preFeeBalance_,

View File

@@ -393,11 +393,11 @@ CheckCash::doApply()
// Check reserve. Return destination account SLE if enough reserve,
// otherwise return nullptr.
auto checkReserve = [&]() -> SLE::pointer {
auto checkDstReserve = [&]() -> SLE::pointer {
auto sleDst = psb.peek(keylet::account(accountID_));
// Can the account cover the trust line's or MPT reserve?
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
applyViewContext, sleDst, preFeeBalance_, {.ownerCountDelta = 1}, j_);
!isTesSuccess(ret))
{
@@ -433,7 +433,7 @@ CheckCash::doApply()
// a. this (destination) account and
// b. issuing account (not sending account).
auto const sleDst = checkReserve();
auto const sleDst = checkDstReserve();
if (sleDst == nullptr)
return tecNO_LINE_INSUF_RESERVE;
@@ -500,7 +500,7 @@ CheckCash::doApply()
auto const mptokenKey = keylet::mptoken(mptID, accountID_);
if (!psb.exists(mptokenKey))
{
auto sleDst = checkReserve();
auto sleDst = checkDstReserve();
if (sleDst == nullptr)
return tecINSUFFICIENT_RESERVE;

View File

@@ -197,7 +197,7 @@ CheckCreate::doApply()
// reserve to pay fees.
auto const applyViewContext = ctx_.getApplyViewContext();
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(), sle, preFeeBalance_, {.ownerCountDelta = 1}, ctx_.journal);
!isTesSuccess(ret))
return ret;

View File

@@ -100,7 +100,7 @@ DelegateSet::doApply()
return tecINTERNAL; // LCOV_EXCL_LINE
auto const applyViewContext = ctx_.getApplyViewContext();
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sleOwner,
preFeeBalance_,

View File

@@ -443,8 +443,8 @@ EscrowCreate::doApply()
// validates the sponsor's reserve + remaining credit. When
// unsponsored this hits the source branch and validates the
// source's pre-lock balance against base + (currentOC+1)*increment.
if (auto const ret = checkInsufficientReserve(
ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
if (auto const ret =
checkReserve(ctx_.getApplyViewContext(), sle, balance, {.ownerCountDelta = 1}, j_);
!isTesSuccess(ret))
return ret;
@@ -455,7 +455,7 @@ EscrowCreate::doApply()
// is always the source's own balance against the source's own
// reserve — the sponsor's reserve was already validated above, and
// a sponsor never covers the locked funds. We compare directly
// (rather than via checkInsufficientReserve) because that helper
// (rather than via checkReserve) because that helper
// diverts to the sponsor's balance when a sponsor is present and
// would ignore the source's post-lock balance entirely.
// ownerCountDelta differs by case:

View File

@@ -163,7 +163,7 @@ DepositPreauth::doApply()
// A preauth counts against the reserve of the issuing account, but we
// check the starting balance because we want to allow dipping into the
// reserve to pay fees.
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_);
!isTesSuccess(ret))
return ret;
@@ -209,7 +209,7 @@ DepositPreauth::doApply()
// A preauth counts against the reserve of the issuing account, but we
// check the starting balance because we want to allow dipping into the
// reserve to pay fees.
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_);
!isTesSuccess(ret))
return ret;

View File

@@ -691,9 +691,8 @@ Payment::doApply()
// reserve.
auto const reserve = accountReserve(view(), sleSrc, j_);
// In a delegated payment, the fee payer is the delegated account,
// not the source account (accountID_).
bool const accountIsPayer = (ctx_.tx.getInitiator() == accountID_);
// In a delegated / fee sponsored payment, the fee payer is not the source account (accountID_).
bool const accountIsPayer = ctx_.tx.getFeePayerID() == accountID_;
// preFeeBalance_ is the balance on the source account (accountID_) BEFORE the fees
// were charged. If source account is the fee payer, it must also cover the fee.

View File

@@ -144,8 +144,8 @@ PaymentChannelCreate::doApply()
// validates the sponsor's reserve + remaining credit. When
// unsponsored this hits the source branch and validates the
// source's pre-lock balance against base + (currentOC+1)*increment.
if (auto const ret = checkInsufficientReserve(
applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_);
if (auto const ret =
checkReserve(applyViewContext, sle, preFeeBalance_, {.ownerCountDelta = 1}, j_);
!isTesSuccess(ret))
return ret;
@@ -154,7 +154,7 @@ PaymentChannelCreate::doApply()
// source's own balance against the source's own reserve — the
// sponsor's reserve was already validated above, and a sponsor
// never covers the locked funds. We compare directly (rather than
// via checkInsufficientReserve) because that helper diverts to the
// via checkReserve) because that helper diverts to the
// sponsor's balance when a sponsor is present and would ignore the
// source's post-lock balance entirely. ownerCountDelta differs by
// case:

View File

@@ -90,12 +90,12 @@ PaymentChannelFund::doApply()
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const applyViewContext = ctx_.getApplyViewContext();
if (auto const ret = checkInsufficientReserve(applyViewContext, sle, balance, {}, j_);
if (auto const ret = checkReserve(applyViewContext, sle, balance, {}, j_);
!isTesSuccess(ret))
return ret;
if (auto const ret = checkInsufficientReserve(
applyViewContext, sle, balance - ctx_.tx[sfAmount], {}, j_);
if (auto const ret =
checkReserve(applyViewContext, sle, balance - ctx_.tx[sfAmount], {}, j_);
!isTesSuccess(ret))
return tecUNFUNDED;
}

View File

@@ -128,8 +128,8 @@ MPTokenIssuanceCreate::create(
if (args.priorBalance)
{
if (auto const ret = checkInsufficientReserve(
ctx, acct, *(args.priorBalance), {.ownerCountDelta = 1}, journal);
if (auto const ret =
checkReserve(ctx, acct, *(args.priorBalance), {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return std::unexpected(ret); // tecINSUFFICIENT_RESERVE
}

View File

@@ -330,12 +330,18 @@ TrustSet::doApply()
auto const applyViewContext = ctx_.getApplyViewContext();
auto const sponsorSle = applyViewContext.txReserveContext.sponsorSle;
std::uint32_t const uOwnerCount = ownerCount(sponsorSle ? sponsorSle : sle, j_);
auto const sponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext());
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto getSponsor = [&sponsorSle, this](AccountID const& account) {
return (sponsorSle && account == accountID_) ? sponsorSle : SLE::pointer();
};
// 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;
bool const freeTrustLine = !sponsorSle && (ownerCount(sle, j_) < 2);
std::uint32_t const uQualityIn(bQualityIn ? ctx_.tx.getFieldU32(sfQualityIn) : 0);
std::uint32_t uQualityOut(bQualityOut ? ctx_.tx.getFieldU32(sfQualityOut) : 0);
@@ -533,10 +539,12 @@ TrustSet::doApply()
if (bLowReserveSet && !bLowReserved)
{
SLE::pointer const lowSponsor = getSponsor(uLowAccountID);
// should be checked PreFunded Sponsor before increaseOwnerCount()
// For PreFunded sponsors, we need to check if there are sufficient reserves before
// calling increaseOwnerCount().
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sleLowAccount,
preFeeBalance_,
@@ -548,12 +556,12 @@ TrustSet::doApply()
// Set reserve for low account.
increaseOwnerCount(
view(),
ReserveContext::makeFromAccount(view(), sleLowAccount, sponsorSle),
ReserveContext::makeFromAccount(view(), sleLowAccount, lowSponsor),
1,
viewJ);
uFlagsOut |= lsfLowReserve;
addSponsorToLedgerEntry(sleRippleState, sponsorSle, sfLowSponsor);
addSponsorToLedgerEntry(sleRippleState, lowSponsor, sfLowSponsor);
if (!bHigh)
bReserveIncrease = true;
@@ -574,27 +582,29 @@ TrustSet::doApply()
if (bHighReserveSet && !bHighReserved)
{
SLE::pointer const highSponsor = getSponsor(uHighAccountID);
// should be checked PreFunded Sponsor before increaseOwnerCount()
// For PreFunded sponsors, we need to check if there are sufficient reserves before
// calling increaseOwnerCount().
if (auto const ret = checkInsufficientReserve(
if (auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sleHighAccount,
preFeeBalance_,
{.ownerCountDelta = 1},
j_);
sponsorSle && !isTesSuccess(ret))
highSponsor && !isTesSuccess(ret))
return tecINSUF_RESERVE_LINE;
// Set reserve for high account.
increaseOwnerCount(
view(),
ReserveContext::makeFromAccount(view(), sleHighAccount, sponsorSle),
ReserveContext::makeFromAccount(view(), sleHighAccount, highSponsor),
1,
viewJ);
uFlagsOut |= lsfHighReserve;
addSponsorToLedgerEntry(sleRippleState, sponsorSle, sfHighSponsor);
addSponsorToLedgerEntry(sleRippleState, highSponsor, sfHighSponsor);
if (bHigh)
bReserveIncrease = true;
@@ -624,8 +634,7 @@ TrustSet::doApply()
}
// Reserve is not scaled by load.
else if (
auto const ret =
checkInsufficientReserve(ctx_.getApplyViewContext(), sle, preFeeBalance_, {}, j_);
auto const ret = checkReserve(ctx_.getApplyViewContext(), sle, preFeeBalance_, {}, j_);
!freeTrustLine && bReserveIncrease && !isTesSuccess(ret))
{
JLOG(j_.trace()) << "Delay transaction: Insufficent reserve to "
@@ -655,7 +664,7 @@ TrustSet::doApply()
return tecNO_LINE_REDUNDANT;
}
else if (
auto const ret = checkInsufficientReserve(
auto const ret = checkReserve(
ctx_.getApplyViewContext(),
sle,
preFeeBalance_,

View File

@@ -921,6 +921,14 @@ public:
sponsor::SponseeAcc(alice),
Ter(tecNO_PERMISSION));
}
{
// The provided sfSponsee account does not exist
// when ending sponsorship.
Account const ghost("ghost"); // never funded, absent from ledger
env(sponsor::transfer(sponsor, tfSponsorshipEnd),
sponsor::SponseeAcc(ghost),
Ter(terNO_ACCOUNT));
}
}
{
@@ -1112,6 +1120,13 @@ public:
Ter(tecNO_PERMISSION));
env.close();
// Reassign an object that is not sponsored yet
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
sponsor::As(sponsor1, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor1),
Ter(tecNO_PERMISSION));
env.close();
// Valid Owner
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
sponsor::As(sponsor1, spfSponsorReserve),
@@ -1129,6 +1144,13 @@ public:
BEAST_EXPECT(sle1->isFieldPresent(sfSponsor));
BEAST_EXPECT(sle1->getAccountID(sfSponsor) == sponsor1.id());
// Create on an object that is already sponsored
env(sponsor::transfer(alice, tfSponsorshipCreate, checkId),
sponsor::As(sponsor2, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor2),
Ter(tecNO_PERMISSION));
env.close();
// transfer sponsor
env(sponsor::transfer(alice, tfSponsorshipReassign, checkId),
sponsor::As(sponsor2, spfSponsorReserve),
@@ -4398,6 +4420,151 @@ public:
testTrustSet(cosigning);
}
void
testZeroBalanceSponsoredPaymentFeePayerCheck()
{
// Zero-balance sponsored Payment: getFeePayer() consistency check
testcase("Sponsored Payment: minimal-balance account with sponsor-pays-fee");
using namespace jtx;
Env env{*this, testableAmendments()};
Account const alice("alice");
Account const sponsor("sponsor");
Account const dest("dest");
auto const baseFee = env.current()->fees().base;
auto const baseReserve = env.current()->fees().reserve;
// Fund sponsor and dest generously, alice with base reserve + 1 XRP for payment
env.fund(XRP(10000), sponsor, dest);
env.fund(baseReserve + XRP(1), alice);
env.close();
// Precondition: alice has base reserve + 1 XRP (enough for payment but not fee)
BEAST_EXPECT(env.balance(alice) == baseReserve + XRP(1));
// Alice tries to send a Payment to dest where sponsor pays the fee via spfSponsorFee.
// Passed even alice balance doesn't have enough to pay fee.
auto const preDest = env.balance(dest);
auto const preSponsor = env.balance(sponsor);
// Alice sends 1 XRP to dest, sponsor pays the fee
env(pay(alice, dest, XRP(1)),
sponsor::As(sponsor, spfSponsorFee),
Sig(sfSponsorSignature, sponsor),
Fee(baseFee));
env.close();
// Payment succeeded
// Alice's balance decreased by 1 XRP (the payment amount, NOT the fee)
BEAST_EXPECT(env.balance(alice) == baseReserve);
// Dest received 1 XRP
BEAST_EXPECT(env.balance(dest) == preDest + XRP(1));
// Sponsor paid the fee (NOT alice)
BEAST_EXPECT(env.balance(sponsor) == preSponsor - baseFee);
}
void
testTrustSetCounterpartySponsorMisroute()
{
// TrustSet's modify path applies the tx-level reserve sponsor to whichever
// side has its reserve gate trip on this update, regardless of whether that
// side belongs to the tx submitter. trustCreate only sets the submitter's
// reserve flag and snapshots the counterparty's asfDefaultRipple state into
// the line's NoRipple bit; if the counterparty later toggles asfDefaultRipple
// (the canonical issuer flow), the line and account flags disagree and on
// the submitter's next TrustSet the counterparty-side gate fires. Sponsor still will be
// checked if it can be applied to that end of the trustLine.
testcase("TrustSet modify with sponsor does not misroute onto counterparty side");
using namespace test::jtx;
Env env(*this);
Account const alice{"alice_t2178"};
Account const bob{"bob_t2178"};
Account const carol{"carol_t2178"};
// Fund without auto-setting asfDefaultRipple
env.fund(XRP(100'000), alice, bob, carol);
env.close();
// Determine account ordering
bool const aliceIsHigh = alice.id() > bob.id();
// To trigger the bug, we need the COUNTERPARTY's reserve gate to trip
// We use issuer/holder terminology where:
// - holder creates the trust line (their reserve is set first)
// - issuer enables DefaultRipple after (creates flag mismatch)
// - holder's second TrustSet triggers issuer's reserve gate
auto const issuer = aliceIsHigh ? bob : alice;
auto const holder = aliceIsHigh ? alice : bob;
auto const usd = issuer["USD"];
// Issuer must NOT have DefaultRipple set initially
// Clear it explicitly (env.fund may have set it)
env(fclear(issuer, asfDefaultRipple));
env.close();
// Holder creates the trust line first (holder's reserve flag is set)
// At this point, issuer does NOT have DefaultRipple set, so
// the NoRipple bit on issuer's side is set according to issuer's current flag
env(trust(holder, usd(1'000)));
env.close();
// Issuer now enables asfDefaultRipple (canonical issuer flow)
// This creates a mismatch: issuer's account flag says DefaultRipple=true
// but the trust line's NoRipple bit on issuer's side is still set
env(fset(issuer, asfDefaultRipple));
env.close();
SF_ACCOUNT const& issuerSponsorField = aliceIsHigh ? sfLowSponsor : sfHighSponsor;
SF_ACCOUNT const& holderSponsorField = aliceIsHigh ? sfHighSponsor : sfLowSponsor;
auto const lineKey = keylet::trustLine(alice, bob, usd.currency);
auto const sleLineBefore = env.le(lineKey);
if (!BEAST_EXPECT(sleLineBefore))
return;
BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfLowSponsor));
BEAST_EXPECT(!sleLineBefore->isFieldPresent(sfHighSponsor));
auto const carolBefore = sponsoringOwnerCount(env, carol);
BEAST_EXPECT(carolBefore == 0);
auto const issuerSponsoredBefore = sponsoredOwnerCount(env, issuer);
BEAST_EXPECT(issuerSponsoredBefore == 0);
// Holder modifies the trust line with Carol as sponsor
// This should trigger the issuer's reserve gate because of the DefaultRipple mismatch
// Carol (sponsor) should NOT be applied to issuer's side (issuer != tx submitter)
env(trust(holder, usd(2'000)),
sponsor::As(carol, spfSponsorReserve),
Sig(sfSponsorSignature, carol),
Ter(tesSUCCESS));
env.close();
auto const sleLineAfter = env.le(lineKey);
if (!BEAST_EXPECT(sleLineAfter))
return;
// Carol only agreed to back the holder, not the issuer
BEAST_EXPECT(!sleLineAfter->isFieldPresent(issuerSponsorField));
// Holder's side also has no sponsor because holder's reserve flag was
// already set on the FIRST TrustSet (no sponsor in scope then)
BEAST_EXPECT(!sleLineAfter->isFieldPresent(holderSponsorField));
// Carol's sponsoring count should remain unchanged (no misroute)
auto const carolAfter = sponsoringOwnerCount(env, carol);
BEAST_EXPECT(carolAfter == carolBefore);
// Issuer's sponsored count should remain unchanged (no misroute)
auto const issuerSponsoredAfter = sponsoredOwnerCount(env, issuer);
BEAST_EXPECT(issuerSponsoredAfter == issuerSponsoredBefore);
}
protected:
void
testSponsor()
@@ -4432,6 +4599,9 @@ protected:
testSponsoredTrustLineNoFreeReserve();
testCoSignReserveBoundedBySponsorshipBudget();
testReserveSponsorGate();
testZeroBalanceSponsoredPaymentFeePayerCheck();
testTrustSetCounterpartySponsorMisroute();
}
void