mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-27 17:10:46 +00:00
include reserve sponsorship
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user