diff --git a/include/xrpl/ledger/helpers/FundingSource.h b/include/xrpl/ledger/helpers/FundingSource.h index 9a5f936fa8..4204318569 100644 --- a/include/xrpl/ledger/helpers/FundingSource.h +++ b/include/xrpl/ledger/helpers/FundingSource.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -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(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 diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 9935a19044..d70914f744 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -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( diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp b/src/libxrpl/tx/transactors/did/DIDSet.cpp index 094d0548f3..4819b54d15 100644 --- a/src/libxrpl/tx/transactors/did/DIDSet.cpp +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -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;