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