From 4abd94e7acd206199b86f43f5256297fda92be6d Mon Sep 17 00:00:00 2001 From: tequ Date: Mon, 15 Sep 2025 11:04:47 +0900 Subject: [PATCH] MaxFee --- .../xrpl/protocol/detail/ledger_entries.macro | 5 +- include/xrpl/protocol/detail/sfields.macro | 1 + .../xrpl/protocol/detail/transactions.macro | 1 + src/test/app/Sponsor_test.cpp | 8 ++ src/xrpld/app/tx/detail/SponsorshipSet.cpp | 27 ++++- src/xrpld/app/tx/detail/Transactor.cpp | 99 +++++++++++++++---- src/xrpld/app/tx/detail/applySteps.cpp | 10 +- 7 files changed, 121 insertions(+), 30 deletions(-) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 2300bcf1e4..1457d0f5a5 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -513,10 +513,11 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ LEDGER_ENTRY(ltSPONSORSHIP, 0x0085, Sponsorship, sponsorship, ({ {sfOwner, soeREQUIRED}, {sfSponsee, soeREQUIRED}, + {sfFeeAmount, soeOPTIONAL}, + {sfMaxFee, soeOPTIONAL}, + {sfReserveCount, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfSponseeNode, soeREQUIRED}, - {sfFeeAmount, soeOPTIONAL}, - {sfReserveCount, soeOPTIONAL}, })) #undef EXPAND diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 8dbc1ad28c..274d5e6d9b 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -248,6 +248,7 @@ TYPED_SFIELD(sfSignatureReward, AMOUNT, 29) TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) +TYPED_SFIELD(sfMaxFee, AMOUNT, 33) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index b6c114f5a2..b66f09f684 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -536,6 +536,7 @@ TRANSACTION(ttSPONSORSHIP_SET, 73, SponsorshipSet, Delegation::notDelegatable, ( {sfSponsorAccount, soeOPTIONAL}, {sfSponsee, soeREQUIRED}, {sfFeeAmount, soeOPTIONAL}, + {sfMaxFee, soeOPTIONAL}, {sfReserveCount, soeOPTIONAL}, })) diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index f0b2626b22..ff96b236e5 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -137,6 +137,7 @@ public: env(sponsor::set(sponsor, alice, tfDeleteObject, 1), ter(temMALFORMED)); env(sponsor::set(sponsor, alice, tfDeleteObject, std::nullopt, XRP(1)), ter(temMALFORMED)); + // TODO: test MaxFee with tfDeleteObject // // preclaim @@ -492,6 +493,13 @@ public: env.fund(XRP(10000), alice, sponsor); env.close(); + // not yet funded + env(noop(alice), + fee(drops(500)), + sponsor::as(sponsor, tfSponsorFee), + ter(tecNO_SPONSOR_PERMISSION)); + + // set sponsorship env(sponsor::set(sponsor, alice, 0, std::nullopt, XRP(1)), ter(tesSUCCESS)); env.close(); diff --git a/src/xrpld/app/tx/detail/SponsorshipSet.cpp b/src/xrpld/app/tx/detail/SponsorshipSet.cpp index f709cbf2a1..2725578984 100644 --- a/src/xrpld/app/tx/detail/SponsorshipSet.cpp +++ b/src/xrpld/app/tx/detail/SponsorshipSet.cpp @@ -94,6 +94,18 @@ SponsorshipSet::preflight(PreflightContext const& ctx) return temBAD_AMOUNT; } + if (ctx.tx.isFieldPresent(sfMaxFee)) + { + auto const maxFee = ctx.tx.getFieldAmount(sfMaxFee); + if (!isXRP(maxFee)) + return temBAD_AMOUNT; + + if (maxFee.xrp().drops() <= 0) + return temBAD_AMOUNT; + + // TODO: check maxFee > basefee + } + if (ctx.tx.isFieldPresent(sfReserveCount)) { if (ctx.tx.getFlags() & tfSponsorshipClearRequireSignForReserve) @@ -108,7 +120,8 @@ SponsorshipSet::preflight(PreflightContext const& ctx) if (ctx.tx.isFlag(tfDeleteObject)) { if (ctx.tx.isFieldPresent(sfFeeAmount) || - ctx.tx.isFieldPresent(sfReserveCount)) + ctx.tx.isFieldPresent(sfReserveCount) || + ctx.tx.isFieldPresent(sfMaxFee)) return temMALFORMED; } @@ -188,6 +201,7 @@ SponsorshipSet::doApply() } auto const feeAmount = ctx_.tx[~sfFeeAmount]; + auto const maxFee = ctx_.tx[~sfMaxFee]; auto const reserveCount = ctx_.tx[~sfReserveCount]; auto reserveSponsorAccSle = getTxReserveSponsor(view(), ctx_.tx); @@ -214,6 +228,10 @@ SponsorshipSet::doApply() (*sponsorAccSle)[sfBalance] -= *feeAmount; (*newSle)[sfFeeAmount] = *feeAmount; } + if (maxFee) + { + (*newSle)[sfMaxFee] = *maxFee; + } if (reserveCount) { (*newSle)[sfReserveCount] = *reserveCount; @@ -244,11 +262,16 @@ SponsorshipSet::doApply() (*sponsorObjSle)[sfFeeAmount] += *feeAmount; } + if (maxFee) + { + (*sponsorObjSle)[sfMaxFee] = *maxFee; + } + if (reserveCount) (*sponsorObjSle)[sfReserveCount] = (*sponsorObjSle)[sfReserveCount] + *reserveCount; - // TODO: update Flags? + // update Flags auto flags = sponsorObjSle->getFieldU32(sfFlags); if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) flags |= lsfSponsorshipRequireSignForFee; diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index c6c42cc57a..d98209686d 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -259,25 +259,35 @@ Transactor::checkSponsor(ReadView const& view, STTx const& tx) auto const sponsorAcc = txSponsor.getAccountID(sfAccount); auto const sponseeAcc = tx.getAccountID(sfAccount); - auto const sponsorSle = view.read(keylet::sponsor(sponsorAcc, sponseeAcc)); - if (!sponsorSle) - return tesSUCCESS; - auto const hasSignature = txSponsor.isFieldPresent(sfTxnSignature) || !txSponsor.getFieldVL(sfSigningPubKey).empty() || txSponsor.isFieldPresent(sfSigners); - if (txSponsor.isFlag(tfSponsorFee) && - sponsorSle->isFlag(lsfSponsorshipRequireSignForFee)) + auto const sponsorSle = view.read(keylet::sponsor(sponsorAcc, sponseeAcc)); + if (!hasSignature) { - if (!hasSignature) + // pre funded + if (!sponsorSle) return tecNO_SPONSOR_PERMISSION; } - if (txSponsor.isFlag(tfSponsorReserve) && - sponsorSle->isFlag(lsfSponsorshipRequireSignForReserve)) + else { - if (!hasSignature) - return tecNO_SPONSOR_PERMISSION; + // co-signed + if (!sponsorSle) + return tesSUCCESS; + + if (txSponsor.isFlag(tfSponsorFee) && + sponsorSle->isFlag(lsfSponsorshipRequireSignForFee)) + { + if (!hasSignature) + return tecNO_SPONSOR_PERMISSION; + } + if (txSponsor.isFlag(tfSponsorReserve) && + sponsorSle->isFlag(lsfSponsorshipRequireSignForReserve)) + { + if (!hasSignature) + return tecNO_SPONSOR_PERMISSION; + } } return tesSUCCESS; @@ -359,21 +369,68 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (feePaid == beast::zero) return tesSUCCESS; - auto const id = ctx.tx.getFeePayer(); - JLOG(ctx.j.trace()) << "Fee payer: " + to_string(id); - auto const sle = ctx.view.read(keylet::account(id)); - if (!sle) - return terNO_ACCOUNT; + std::optional availableBalance; - auto const balance = (*sle)[sfBalance].xrp(); + if (ctx.tx.isFieldPresent(sfSponsor)) + { + auto const txSponsor = ctx.tx.getFieldObject(sfSponsor); + if (txSponsor.isFlag(tfSponsorFee) && + (!txSponsor.isFieldPresent(sfTxnSignature) && + !txSponsor.isFieldPresent(sfSigners))) + { + // use prefunded fee sponsor + auto const keylet = keylet::sponsor( + txSponsor.getAccountID(sfAccount), ctx.tx[sfAccount]); + auto const sponsorSle = ctx.view.read(keylet); + if (!sponsorSle) + return tecNO_SPONSOR_PERMISSION; - if (balance < feePaid) + XRPAmount const maxFee = sponsorSle->isFieldPresent(sfMaxFee) + ? sponsorSle->getFieldAmount(sfMaxFee).xrp() + : INITIAL_XRP; + + XRPAmount const feeAmount = sponsorSle->isFieldPresent(sfFeeAmount) + ? sponsorSle->getFieldAmount(sfFeeAmount).xrp() + : XRPAmount(0); + + // feePaid should <= maxFee + if (feePaid > maxFee) + return tecNO_SPONSOR_PERMISSION; + + // feePaid should <= feeAmount + if (feePaid > feeAmount) + return tecNO_SPONSOR_PERMISSION; + + availableBalance = feeAmount; + } + else + { + // proceed to use fee payer + } + } + + if (!availableBalance) + { + auto const id = ctx.tx.getFeePayer(); + JLOG(ctx.j.trace()) << "Fee payer: " + to_string(id); + auto const sle = ctx.view.read(keylet::account(id)); + if (!sle) + return terNO_ACCOUNT; + + availableBalance = (*sle)[sfBalance].xrp(); + } + + XRPL_ASSERT( + availableBalance, + "ripple::Transactor::checkFee : could not get balance for fee"); + + if (*availableBalance < feePaid) { JLOG(ctx.j.trace()) - << "Insufficient balance:" << " balance=" << to_string(balance) - << " paid=" << to_string(feePaid); + << "Insufficient balance:" << " balance=" + << to_string(*availableBalance) << " paid=" << to_string(feePaid); - if ((balance > beast::zero) && !ctx.view.open()) + if ((*availableBalance > beast::zero) && !ctx.view.open()) { // Closed ledger, non-zero balance, less than fee return tecINSUFF_FEE; diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index cb6371891d..9b315462b2 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -197,6 +197,11 @@ invoke_preclaim(PreclaimContext const& ctx) result = T::checkPriorTxAndLastLedger(ctx); + if (result != tesSUCCESS) + return result; + + result = T::checkSponsor(ctx.view, ctx.tx); + if (result != tesSUCCESS) return result; @@ -207,11 +212,6 @@ invoke_preclaim(PreclaimContext const& ctx) result = T::checkPermission(ctx.view, ctx.tx); - if (result != tesSUCCESS) - return result; - - result = T::checkSponsor(ctx.view, ctx.tx); - if (result != tesSUCCESS) return result;