include reserve sponsorship

This commit is contained in:
Mayukha Vadari
2026-06-23 02:11:48 -04:00
parent a31f63f216
commit 3545261b45
3 changed files with 169 additions and 109 deletions

View File

@@ -4,6 +4,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
@@ -29,35 +30,27 @@ enum class FundingKind {
struct FundingSource
{
FundingKind kind{FundingKind::AccountRoot};
Keylet keylet{ltACCOUNT_ROOT, uint256{}};
// The AccountID that owns this source. For AccountRoot this is the
// account itself; for Sponsorship this is the sponsor.
AccountID accountId{};
// The source is a sponsor's account_root paying directly via tx signature
// (no Sponsorship SLE involved). Only meaningful when kind == AccountRoot.
bool isCoSigned{false};
// The source is the account, but the tx was authorized by a delegate.
// Only meaningful when kind == AccountRoot.
bool isDelegate{false};
FundingKind kind;
Keylet keylet;
AccountID accountId;
};
// Both funding sources for a transaction.
// Both funding sources for a transaction, plus tx-level flags that apply
// equally to fee and reserve sources.
struct TxPayers
{
AccountID initiator{}; // tx[sfAccount]
AccountID initiator; // tx[sfAccount]
FundingSource feeSource;
FundingSource reserveSource;
bool isCoSigned; // tx has sfSponsorSignature
bool isDelegate; // tx has sfDelegate
};
// The SField used to read or write the balance on this source's SLE.
inline SF_AMOUNT const&
balanceField(FundingSource const& src)
balanceField(FundingKind kind)
{
return src.kind == FundingKind::Sponsorship ? sfFeeAmount : sfBalance;
return kind == FundingKind::Sponsorship ? sfFeeAmount : sfBalance;
}
// Resolve fee + reserve funding sources for `tx` against `view`.
@@ -65,100 +58,99 @@ balanceField(FundingSource const& src)
inline TxPayers
resolveTxPayers(ReadView const& view, STTx const& tx)
{
TxPayers payers;
payers.initiator = tx.getAccountID(sfAccount);
auto const initiator = tx.getAccountID(sfAccount);
bool const hasSponsor = tx.isFieldPresent(sfSponsor);
bool const isCoSigned = tx.isFieldPresent(sfSponsorSignature);
bool const isDelegate = tx.isFieldPresent(sfDelegate);
// --- Fee source ---
bool const feeSponsored =
tx.isFieldPresent(sfSponsor) && (tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u;
auto const sponsorshipKeylet = hasSponsor
? keylet::sponsorship(tx.getAccountID(sfSponsor), initiator)
: Keylet{ltSPONSORSHIP, uint256{}};
if (feeSponsored)
{
auto const sponsorId = tx.getAccountID(sfSponsor);
auto const sponsorshipKeylet = keylet::sponsorship(sponsorId, payers.initiator);
bool const hasSponsorSig = tx.isFieldPresent(sfSponsorSignature);
// If a Sponsorship SLE exists, fee comes from it (pre-funded). A
// sponsor signature without an existing SLE means the sponsor is
// paying directly from their account.
if (hasSponsorSig && !view.exists(sponsorshipKeylet))
auto const buildFeeSource = [&]() -> FundingSource {
bool const feeSponsored =
hasSponsor && (tx.getFieldU32(sfSponsorFlags) & spfSponsorFee) != 0u;
if (!feeSponsored)
{
payers.feeSource = FundingSource{
// tx.getFeePayer() returns sfDelegate if present, else sfAccount.
auto const payerId = tx.getFeePayer();
return FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(payerId),
.accountId = payerId,
};
}
auto const sponsorId = tx.getAccountID(sfSponsor);
// Preserves existing dispatch: if the Sponsorship SLE exists the
// fee comes from it; otherwise a sponsor signature pays the fee
// from the sponsor's account_root.
if (isCoSigned && !view.exists(sponsorshipKeylet))
return FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(sponsorId),
.accountId = sponsorId,
.isCoSigned = true};
}
else
{
payers.feeSource = FundingSource{
.kind = FundingKind::Sponsorship,
.keylet = sponsorshipKeylet,
.accountId = sponsorId};
}
}
else
{
// tx.getFeePayer() returns sfDelegate if present, else sfAccount.
auto const payerId = tx.getFeePayer();
payers.feeSource = FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(payerId),
.accountId = payerId,
.isDelegate = tx.isFieldPresent(sfDelegate)};
}
};
return FundingSource{
.kind = FundingKind::Sponsorship,
.keylet = sponsorshipKeylet,
.accountId = sponsorId,
};
};
// --- Reserve source ---
// Today, reserve sponsorship is always to the sponsor's account_root.
// There is no Sponsorship-object reserve path.
if (auto const reserveSponsor = getTxReserveSponsorAccountID(tx))
{
payers.reserveSource = FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(*reserveSponsor),
.accountId = *reserveSponsor,
.isCoSigned = true};
}
else
{
payers.reserveSource = FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(payers.initiator),
.accountId = payers.initiator};
}
auto const buildReserveSource = [&]() -> FundingSource {
bool const reserveSponsored =
hasSponsor && (tx.getFieldU32(sfSponsorFlags) & spfSponsorReserve) != 0u;
if (!reserveSponsored)
return FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(initiator),
.accountId = initiator,
};
return payers;
auto const sponsorId = tx.getAccountID(sfSponsor);
return FundingSource{
.kind = FundingKind::AccountRoot,
.keylet = keylet::account(sponsorId),
.accountId = sponsorId,
};
};
return TxPayers{
.initiator = initiator,
.feeSource = buildFeeSource(),
.reserveSource = buildReserveSource(),
.isCoSigned = isCoSigned,
.isDelegate = isDelegate,
};
}
// Read the spendable balance from this source's SLE. Returns kZero if the
// SLE is null or the field is absent.
inline XRPAmount
sourceBalance(SLE::const_ref sle, FundingSource const& src)
// Read the spendable balance from a source's SLE. Returns kZero if the SLE
// is null or the balance field is absent.
[[nodiscard]] inline XRPAmount
sourceBalance(SLE::const_ref sle, FundingKind kind)
{
if (!sle)
return beast::kZero;
auto const& field = balanceField(src);
auto const& field = balanceField(kind);
if (!sle->isFieldPresent(field))
return beast::kZero;
return sle->getFieldAmount(field).xrp();
}
// Apply a Sponsorship object's sfMaxFee cap, if present. For AccountRoot
// sources this is a no-op and returns `fee` unchanged.
inline XRPAmount
capFeeBySource(SLE::const_ref sle, FundingSource const& src, XRPAmount fee)
// sources this is a no-op and returns `amount` unchanged.
[[nodiscard]] inline XRPAmount
capFeeBySource(SLE::const_ref sle, FundingKind kind, XRPAmount amount)
{
if (src.kind != FundingKind::Sponsorship || !sle || !sle->isFieldPresent(sfMaxFee))
return fee;
auto const cap = sle->getFieldAmount(sfMaxFee).xrp();
return std::min(fee, cap);
if (kind != FundingKind::Sponsorship || !sle || !sle->isFieldPresent(sfMaxFee))
return amount;
return std::min(amount, sle->getFieldAmount(sfMaxFee).xrp());
}
// Deduct `amount` from `src`'s balance field on the SLE held in `view`.
// Handles the Sponsorship case where draining sfFeeAmount to zero requires
// makeFieldAbsent (sfFeeAmount is soeOptional on ltSPONSORSHIP).
// Returns tefINTERNAL if the SLE is missing; tesSUCCESS otherwise.
// The caller is responsible for `view.update(sle)`.
// Deduct `amount` from `src`'s balance field. Handles the Sponsorship case
// where draining sfFeeAmount to zero requires makeFieldAbsent (sfFeeAmount
// is soeOptional on ltSPONSORSHIP). Updates the SLE in `view`.
[[nodiscard]] inline TER
debitSource(ApplyView& view, FundingSource const& src, XRPAmount amount)
{
@@ -166,7 +158,7 @@ debitSource(ApplyView& view, FundingSource const& src, XRPAmount amount)
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const& field = balanceField(src);
auto const& field = balanceField(src.kind);
auto const after = sle->getFieldAmount(field) - amount;
if (after == beast::kZero && src.kind == FundingKind::Sponsorship)
{
@@ -180,4 +172,73 @@ debitSource(ApplyView& view, FundingSource const& src, XRPAmount amount)
return tesSUCCESS;
}
// ----- Reserve operations against a resolved TxPayers --------------------
// These wrap the underlying AccountRootHelpers / SponsorHelpers so transactor
// call sites don't have to re-derive sponsor identity or branch on
// "is this sponsored" at every step.
// Verify the reserve payer has sufficient balance for the proposed owner-
// count and reserve-count deltas. When a Sponsorship SLE exists, also caps
// against its sfRemainingOwnerCount.
[[nodiscard]] inline TER
checkReserve(
ReadView const& view,
TxPayers const& payers,
std::int32_t ownerCountDelta,
std::int32_t reserveCountDelta,
beast::Journal j)
{
bool const reserveSponsored = payers.reserveSource.accountId != payers.initiator;
if (reserveSponsored && ownerCountDelta > 0)
{
auto const sponsorshipSle =
view.read(keylet::sponsorship(payers.reserveSource.accountId, payers.initiator));
if (sponsorshipSle)
{
auto const remaining = sponsorshipSle->getFieldU32(sfRemainingOwnerCount);
if (remaining < static_cast<std::uint32_t>(ownerCountDelta))
return tecINSUFFICIENT_RESERVE;
}
}
auto const reserveSle = view.read(payers.reserveSource.keylet);
if (!reserveSle)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const reserve = accountReserve(view, reserveSle, j, ownerCountDelta, reserveCountDelta);
auto const balance = reserveSle->getFieldAmount(sfBalance);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
return tesSUCCESS;
}
// Apply +/- delta to every owner-count field implicated by this tx:
// initiator.OwnerCount (always)
// initiator.SponsoredOwnerCount (if reserve-sponsored)
// reserveSponsor.SponsoringOwnerCount(if reserve-sponsored)
// Sponsorship.RemainingOwnerCount (-delta, when SLE exists and delta > 0)
inline void
adjustOwnerCount(ApplyView& view, TxPayers const& payers, std::int32_t delta, beast::Journal j)
{
auto const accountSle = view.peek(keylet::account(payers.initiator));
SLE::pointer const sponsorSle = (payers.reserveSource.accountId != payers.initiator)
? view.peek(payers.reserveSource.keylet)
: SLE::pointer{};
adjustOwnerCount(view, accountSle, sponsorSle, delta, j);
}
// Stamp the reserve sponsor's AccountID onto a newly created ledger entry.
// No-op when the tx is not reserve-sponsored.
inline void
stampReserveSponsor(SLE::ref sle, TxPayers const& payers, SF_ACCOUNT const& field = sfSponsor)
{
if (payers.reserveSource.accountId == payers.initiator)
return;
XRPL_ASSERT(
(sle->getType() == ltRIPPLE_STATE && (field == sfHighSponsor || field == sfLowSponsor)) ||
(sle->getType() != ltRIPPLE_STATE && field == sfSponsor),
"stampReserveSponsor : valid sponsor field for ledger entry type");
sle->setAccountID(field, payers.reserveSource.accountId);
}
} // namespace xrpl

View File

@@ -518,14 +518,17 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
if (payerSle->getType() != expectedType)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPAmount maxSpendable = sourceBalance(payerSle, src);
if (src.isCoSigned)
XRPAmount maxSpendable = sourceBalance(payerSle, src.kind);
// A sponsor paying directly (via signature, no Sponsorship SLE) must
// preserve their own reserve.
bool const sponsorPaysDirectly = payers.isCoSigned && src.kind == FundingKind::AccountRoot &&
src.accountId != payers.initiator;
if (sponsorPaysDirectly)
{
// Sponsor paying directly: must preserve their own reserve.
STAmount const sponsorReserve = accountReserve(ctx.view, payerSle, ctx.j);
maxSpendable -= sponsorReserve.xrp();
}
maxSpendable = capFeeBySource(payerSle, src, maxSpendable);
maxSpendable = capFeeBySource(payerSle, src.kind, maxSpendable);
// NOTE: Because preclaim evaluates against a static readview, it
// does not reflect fee deductions from other transactions paid by
@@ -1234,8 +1237,8 @@ Transactor::reset(XRPAmount fee)
if (!payerSle)
return {tefINTERNAL, beast::kZero}; // LCOV_EXCL_LINE
auto const balance = sourceBalance(payerSle, src);
fee = capFeeBySource(payerSle, src, fee);
auto const balance = sourceBalance(payerSle, src.kind);
fee = capFeeBySource(payerSle, src.kind, fee);
// balance should have already been checked in checkFee / preFlight.
XRPL_ASSERT(

View File

@@ -5,6 +5,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/FundingSource.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
@@ -70,20 +71,15 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner)
if (!sleAccount)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check reserve availability for new object creation
auto const sponsorSle = getTxReserveSponsor(ctx.view(), ctx.tx);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
if (auto const ret = checkInsufficientReserve(
ctx.view(), ctx.tx, sleAccount, balance, *sponsorSle, 1, 0, ctx.journal);
!isTesSuccess(ret))
auto const payers = resolveTxPayers(ctx.view(), ctx.tx);
// Reserve check spans initiator balance, sponsor balance, and the
// Sponsorship object's RemainingOwnerCount cap when applicable.
if (auto const ret = checkReserve(ctx.view(), payers, 1, 0, ctx.journal); !isTesSuccess(ret))
return ret;
// Add ledger object to ledger
ctx.view().insert(sle);
// Add ledger object to owner's page
{
auto page =
ctx.view().dirInsert(keylet::ownerDir(owner), sle->key(), describeOwnerDir(owner));
@@ -91,8 +87,8 @@ addSLE(ApplyContext& ctx, SLE::ref sle, AccountID const& owner)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*sle)[sfOwnerNode] = *page;
}
adjustOwnerCount(ctx.view(), sleAccount, *sponsorSle, 1, ctx.journal);
addSponsorToLedgerEntry(sle, *sponsorSle);
adjustOwnerCount(ctx.view(), payers, 1, ctx.journal);
stampReserveSponsor(sle, payers);
ctx.view().update(sleAccount);
return tesSUCCESS;