From 4eea76ca92bc3fac8cf2f9fce3e79b6ca2ac6502 Mon Sep 17 00:00:00 2001 From: tequ Date: Sat, 13 Sep 2025 22:34:54 +0900 Subject: [PATCH] Create Sponsored Account / AccountDelete with ltSponsorship, Sponsored Account --- include/xrpl/protocol/TxFlags.h | 3 +- src/test/app/Sponsor_test.cpp | 113 +++++++++++++++++---- src/xrpld/app/tx/detail/DeleteAccount.cpp | 25 +++++ src/xrpld/app/tx/detail/Payment.cpp | 49 ++++++--- src/xrpld/app/tx/detail/SponsorshipSet.cpp | 8 +- 5 files changed, 161 insertions(+), 37 deletions(-) diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index bcc869830c..3c77c79885 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -113,8 +113,9 @@ constexpr std::uint32_t tfOfferCreateMask = constexpr std::uint32_t tfNoRippleDirect = 0x00010000; constexpr std::uint32_t tfPartialPayment = 0x00020000; constexpr std::uint32_t tfLimitQuality = 0x00040000; +constexpr std::uint32_t tfSponsorCreatedAccount = 0x00080000; constexpr std::uint32_t tfPaymentMask = - ~(tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect); + ~(tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect | tfSponsorCreatedAccount); constexpr std::uint32_t tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment); // TrustSet flags: diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 319f9f6ba2..f0b2626b22 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -527,16 +527,48 @@ public: Account const alice("alice"); Account const sponsor("sponsor"); + Account const bob("bob"); + Account const charlie("charlie"); env.fund(XRP(10000), alice, sponsor); - Account const bob("bob"); - env(pay(alice, bob, STAmount(env.current()->fees().accountReserve(0))), - sponsor::as(sponsor, tfSponsorReserve), - sponsor::sig(sponsor)); - env.close(); + // Account is not sponsored by normal Sponsor specification + { + env(pay(alice, + bob, + STAmount(env.current()->fees().accountReserve(0))), + sponsor::as(sponsor, tfSponsorReserve), + sponsor::sig(sponsor)); + env.close(); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1); + auto const bobSle = env.le(keylet::account(bob)); + BEAST_EXPECT(!bobSle->isFieldPresent(sfSponsorAccount)); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 0); + } + + // Use tfSponsorCreatedAccount to sponsor an account + { + // to funded accoutn + env(pay(sponsor, + bob, + STAmount(env.current()->fees().accountReserve(0))), + txflags(tfSponsorCreatedAccount), + ter(tecNO_SPONSOR_PERMISSION)); + + // to non-funded account + env(pay(sponsor, + charlie, + STAmount(env.current()->fees().accountReserve(0))), + txflags(tfSponsorCreatedAccount)); + env.close(); + + auto const charlieSle = env.le(keylet::account(charlie)); + BEAST_EXPECT(charlieSle->isFieldPresent(sfSponsorAccount)); + BEAST_EXPECT( + charlieSle->getAccountID(sfSponsorAccount) == sponsor.id()); + BEAST_EXPECT(sponsoredOwnerCount(env, charlie) == 0); + BEAST_EXPECT(sponsoringAccountCount(env, sponsor) == 1); + } } void @@ -926,25 +958,66 @@ public: { testcase("AccountDelete"); using namespace test::jtx; - Env env{*this, testable_amendments()}; Account const alice("alice"); Account const bob("bob"); Account const sponsor("sponsor"); - env.fund(XRP(1000000), alice, bob, sponsor); - env.close(); + { + // Delete Account with ltSponsorship + Env env{*this, testable_amendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); - // set sponsor - env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), ter(tesSUCCESS)); - env.close(); + // set sponsor + env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), + ter(tesSUCCESS)); + env.close(); - // AccountDelete - env(acctdelete(alice, bob)); - env.close(); + incLgrSeqForAccDel(env, alice); - BEAST_EXPECT(ownerCount(env, alice) == 0); - BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); - BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + auto const keylet = keylet::sponsor(sponsor, alice); + auto const sponsorObj = env.le(keylet); + BEAST_EXPECT(sponsorObj); + + // AccountDelete + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), fee(requiredFee), ter(tesSUCCESS)); + env.close(); + } + + { + // Delete SponsoredAccount + Env env{*this, testable_amendments()}; + env.memoize(alice); + env.fund(XRP(1000000), bob, sponsor); + env.close(); + + // create SponsoredAccount + env(pay(sponsor, alice, XRP(10000)), + txflags(tfSponsorCreatedAccount)); + env.close(); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = non-sponsor + auto const requiredFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), + fee(requiredFee), + ter(tecNO_SPONSOR_PERMISSION)); + + auto const sponsorSle = env.le(keylet::account(sponsor)); + BEAST_EXPECT( + sponsorSle->getFieldU32(sfSponsoringAccountCount) == 1); + + incLgrSeqForAccDel(env, alice); + + // AccountDelete: destination = sponsor + env(acctdelete(alice, sponsor), fee(requiredFee), ter(tesSUCCESS)); + + auto const sponsorSle2 = env.le(keylet::account(sponsor)); + BEAST_EXPECT( + !sponsorSle2->isFieldPresent(sfSponsoringAccountCount)); + } } void @@ -986,7 +1059,7 @@ public: testSponsorReserve(); testDisallowIncoming(); - // testAccountDelete(); + testAccountDelete(); } }; diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 33f7a108f9..45c496730a 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -299,6 +299,15 @@ DeleteAccount::preclaim(PreclaimContext const& ctx) return tecHAS_OBLIGATIONS; } + if (sleAccount->isFieldPresent(sfSponsorAccount)) + { + if (dst != sleAccount->getAccountID(sfSponsorAccount)) + return tecNO_SPONSOR_PERMISSION; + } + if (sleAccount->isFieldPresent(sfSponsoringOwnerCount) || + sleAccount->isFieldPresent(sfSponsoringAccountCount)) + return tecHAS_OBLIGATIONS; + // We don't allow an account to be deleted if its sequence number // is within 256 of the current ledger. This prevents replay of old // transactions if this account is resurrected after it is deleted. @@ -430,6 +439,22 @@ DeleteAccount::doApply() (*src)[sfBalance] = (*src)[sfBalance] - mSourceBalance; ctx_.deliver(mSourceBalance); + if (src->isFieldPresent(sfSponsorAccount)) + { + auto const sponsorAcc = src->getAccountID(sfSponsorAccount); + auto sponsorSle = view().peek(keylet::account(sponsorAcc)); + + auto const sponsoringAccountCount = + sponsorSle->getFieldU32(sfSponsoringAccountCount); + + if (sponsoringAccountCount == 1) + sponsorSle->makeFieldAbsent(sfSponsoringAccountCount); + else + sponsorSle->setFieldU32( + sfSponsoringAccountCount, sponsoringAccountCount - 1); + view().update(sponsorSle); + } + XRPL_ASSERT( (*src)[sfBalance] == XRPAmount(0), "ripple::DeleteAccount::doApply : source balance is zero"); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 78ace009c9..e8a1c7f964 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -98,6 +98,15 @@ Payment::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (txFlags & tfSponsorCreatedAccount) + { + if (!ctx.rules.enabled(featureSponsor)) + return temINVALID_FLAG; + + if (!dstAmount.native()) + return temMALFORMED; + } + if (mptDirect && ctx.tx.isFieldPresent(sfPaths)) return temMALFORMED; @@ -330,19 +339,24 @@ Payment::preclaim(PreclaimContext const& ctx) return tecNO_DST_INSUF_XRP; } } - else if ( - (sleDst->getFlags() & lsfRequireDestTag) && - !ctx.tx.isFieldPresent(sfDestinationTag)) + else { - // The tag is basically account-specific information we don't - // understand, but we can require someone to fill it in. + if (txFlags & tfSponsorCreatedAccount) + return tecNO_SPONSOR_PERMISSION; - // We didn't make this test for a newly-formed account because there's - // no way for this field to be set. - JLOG(ctx.j.trace()) - << "Malformed transaction: DestinationTag required."; + if ((sleDst->getFlags() & lsfRequireDestTag) && + !ctx.tx.isFieldPresent(sfDestinationTag)) + { + // The tag is basically account-specific information we don't + // understand, but we can require someone to fill it in. - return tecDST_TAG_NEEDED; + // We didn't make this test for a newly-formed account because + // there's no way for this field to be set. + JLOG(ctx.j.trace()) + << "Malformed transaction: DestinationTag required."; + + return tecDST_TAG_NEEDED; + } } // Payment with at least one intermediate step and uses transitive balances. @@ -415,12 +429,19 @@ Payment::doApply() sleDst->setAccountID(sfAccount, dstAccountID); sleDst->setFieldU32(sfSequence, seqno); - auto const sponsor = getTxReserveSponsor(view(), ctx_.tx); - if (sponsor.has_value()) + if (txFlags & tfSponsorCreatedAccount) { + auto const sponsor = view().peek(keylet::account(account_)); + if (!sponsor) + return tecINTERNAL; // LCOV_EXCL_LINE + auto const currentSponsoringAccountCount = + sponsor->getFieldU32(sfSponsoringAccountCount); + sponsor->setFieldU32( + sfSponsoringAccountCount, currentSponsoringAccountCount + 1); + view().update(sponsor); + addSponsorToLedgerEntry(sleDst, sponsor); - sponsor.value()->setFieldU32(sfSponsoringAccountCount, 1); - view().update(sponsor.value()); + view().update(sponsor); } view().insert(sleDst); diff --git a/src/xrpld/app/tx/detail/SponsorshipSet.cpp b/src/xrpld/app/tx/detail/SponsorshipSet.cpp index 923c421a34..b30209bb7a 100644 --- a/src/xrpld/app/tx/detail/SponsorshipSet.cpp +++ b/src/xrpld/app/tx/detail/SponsorshipSet.cpp @@ -276,7 +276,7 @@ SponsorshipSet::deleteSponsorship( std::shared_ptr const& sle, beast::Journal j) { - auto const sponsor = sle->getAccountID(sfSponsorAccount); + auto const sponsor = sle->getAccountID(sfAccount); auto const sponsee = sle->getAccountID(sfSponsee); // adjust balance @@ -285,9 +285,13 @@ SponsorshipSet::deleteSponsorship( return tecINTERNAL; auto const feeAmount = sle->getFieldAmount(sfFeeAmount); - (*sponsorAccSle)[sfBalance] += feeAmount; + auto const reserveSponsor = getLedgerEntryReserveSponsor(view, sle); + adjustOwnerCount(view, sponsorAccSle, reserveSponsor, -1, j); + + view.update(sponsorAccSle); + // delete sponsor node view.dirRemove( keylet::ownerDir(sponsor), (*sle)[sfSponsorNode], sle->key(), false);