From e589b71ee0183d42e6b09c74f3cef7615d3ca44a Mon Sep 17 00:00:00 2001 From: tequ Date: Sat, 13 Sep 2025 09:36:38 +0900 Subject: [PATCH] v2. SponsorSet --- include/xrpl/protocol/Indexes.h | 4 + include/xrpl/protocol/LedgerFormats.h | 6 + include/xrpl/protocol/TER.h | 1 + include/xrpl/protocol/TxFlags.h | 12 +- .../xrpl/protocol/detail/ledger_entries.macro | 12 + include/xrpl/protocol/detail/sfields.macro | 6 + .../xrpl/protocol/detail/transactions.macro | 14 +- include/xrpl/protocol/jss.h | 3 +- src/libxrpl/protocol/Indexes.cpp | 9 + src/libxrpl/protocol/TER.cpp | 1 + src/test/app/Sponsor_test.cpp | 251 ++++++++++++++-- src/test/jtx/impl/sponsor.cpp | 40 ++- src/test/jtx/sponsor.h | 26 +- src/test/rpc/AccountSet_test.cpp | 3 +- src/xrpld/app/tx/detail/DeleteAccount.cpp | 15 + src/xrpld/app/tx/detail/InvariantCheck.cpp | 9 + src/xrpld/app/tx/detail/SetAccount.cpp | 8 + src/xrpld/app/tx/detail/SponsorSet.cpp | 273 ++++++++++++++++++ src/xrpld/app/tx/detail/SponsorSet.h | 48 +++ src/xrpld/app/tx/detail/SponsorTransfer.cpp | 8 +- src/xrpld/app/tx/detail/Transactor.cpp | 91 +++++- src/xrpld/app/tx/detail/Transactor.h | 3 + src/xrpld/app/tx/detail/apply.cpp | 17 +- src/xrpld/app/tx/detail/applySteps.cpp | 6 + src/xrpld/rpc/handlers/LedgerEntry.cpp | 21 ++ 25 files changed, 840 insertions(+), 47 deletions(-) create mode 100644 src/xrpld/app/tx/detail/SponsorSet.cpp create mode 100644 src/xrpld/app/tx/detail/SponsorSet.h diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 3e3f2843c1..5baaaf5b86 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -174,6 +174,10 @@ static ticket_t const ticket{}; Keylet signers(AccountID const& account) noexcept; +/** A Sponsor */ +Keylet +sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept; + /** A Check */ /** @{ */ Keylet diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index e3efe8fec2..dcca9544b3 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -149,6 +149,8 @@ enum LedgerSpecificFlags { 0x40000000, // True, enable trustline locking lsfAllowTrustLineClawback = 0x80000000, // True, enable clawback + lsfDisallowIncomingSponsor = + 0x00004000, // True, reject new sponsor // ltOFFER lsfPassive = 0x00010000, @@ -196,6 +198,10 @@ enum LedgerSpecificFlags { // ltVAULT lsfVaultPrivate = 0x00010000, + + // ltSPONSORSHIP + lsfSponsorshipRequireSignForFee = 0x00010000, + lsfSponsorshipRequireSignForReserve = 0x00020000, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 9ace6b80f8..95e9c27816 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -362,6 +362,7 @@ enum TECcodes : TERUnderlyingType { tecPSEUDO_ACCOUNT = 196, tecPRECISION_LOSS = 197, tecNO_DELEGATE_PERMISSION = 198, + tecNO_SPONSOR_PERMISSION = 199, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index b43283f22a..1216bfe791 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -62,9 +62,10 @@ constexpr std::uint32_t tfInnerBatchTxn = 0x40000000; constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn; constexpr std::uint32_t tfUniversalMask = ~tfUniversal; -// Sponsor flags: +// Sponsor flags (Global): constexpr std::uint32_t tfSponsorFee = 0x00000001; constexpr std::uint32_t tfSponsorReserve = 0x00000002; +constexpr std::uint32_t tfSponsorMask = tfSponsorFee | tfSponsorReserve; // AccountSet flags: constexpr std::uint32_t tfRequireDestTag = 0x00010000; @@ -97,6 +98,7 @@ constexpr std::uint32_t asfDisallowIncomingPayChan = 14; constexpr std::uint32_t asfDisallowIncomingTrustline = 15; constexpr std::uint32_t asfAllowTrustLineClawback = 16; constexpr std::uint32_t asfAllowTrustLineLocking = 17; +constexpr std::uint32_t asfDisallowIncomingSponsor = 19; // OfferCreate flags: constexpr std::uint32_t tfPassive = 0x00010000; @@ -253,6 +255,14 @@ constexpr std::uint32_t tfIndependent = 0x00080000; constexpr std::uint32_t const tfBatchMask = ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; +// SponsorSet flags: +constexpr std::uint32_t tfSponsorshipSetRequireSignForFee = 0x00010000; +constexpr std::uint32_t tfSponsorshipClearRequireSignForFee = 0x00020000; +constexpr std::uint32_t tfSponsorshipSetRequireSignForReserve = 0x00040000; +constexpr std::uint32_t tfSponsorshipClearRequireSignForReserve = 0x00080000; +constexpr std::uint32_t tfDeleteObject = 0x00100000; +constexpr std::uint32_t tfSponsorSetMask = ~(tfUniversal | tfSponsorshipSetRequireSignForFee | tfSponsorshipClearRequireSignForFee | tfSponsorshipSetRequireSignForReserve | tfSponsorshipClearRequireSignForReserve | tfDeleteObject); + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 66914d92a1..f59b18d910 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -507,5 +507,17 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) +/** A ledger object representing a sponsorship. + \sa keylet::sponsor + */ +LEDGER_ENTRY(ltSPONSORSHIP, 0x0085, Sponsorship, sponsorship, ({ + {sfAccount, soeREQUIRED}, + {sfSponsee, soeREQUIRED}, + {sfSponsorNode, soeREQUIRED}, + {sfSponseeNode, soeREQUIRED}, + {sfFeeAmount, soeOPTIONAL}, + {sfReserveCount, soeOPTIONAL}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index f6b6417bdc..5fe30502c1 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -117,6 +117,7 @@ TYPED_SFIELD(sfPermissionValue, UINT32, 52) TYPED_SFIELD(sfSponsoredOwnerCount, UINT32, 53) TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 54) TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 55) +TYPED_SFIELD(sfReserveCount, UINT32, 56) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -148,6 +149,8 @@ TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfIssuerNode, UINT64, 27) TYPED_SFIELD(sfSubjectNode, UINT64, 28) TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default) +TYPED_SFIELD(sfSponsorNode, UINT64, 30) +TYPED_SFIELD(sfSponseeNode, UINT64, 31) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -200,6 +203,7 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35) TYPED_SFIELD(sfParentBatchID, UINT256, 36) +TYPED_SFIELD(sfObjectID, UINT256, 37) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -244,6 +248,7 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28) TYPED_SFIELD(sfSignatureReward, AMOUNT, 29) TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) +TYPED_SFIELD(sfFeeAmount, AMOUNT, 32) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) @@ -293,6 +298,7 @@ TYPED_SFIELD(sfEmitCallback, ACCOUNT, 10) TYPED_SFIELD(sfHolder, ACCOUNT, 11) TYPED_SFIELD(sfDelegate, ACCOUNT, 12) TYPED_SFIELD(sfSponsorAccount, ACCOUNT, 13) +TYPED_SFIELD(sfSponsee, ACCOUNT, 14) // account (uncommon) TYPED_SFIELD(sfHookAccount, ACCOUNT, 16) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index d281a5ab4b..997e70013f 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -526,9 +526,17 @@ TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({ {sfBatchSigners, soeOPTIONAL}, })) -/** This transaction transfer sponsor */ -TRANSACTION(ttSPONSOR_TRANSFER, 72, SponsorTransfer, Delegation::notDelegatable, ({ - {sfLedgerIndex, soeOPTIONAL}, +/** This transaction transfer sponsorship */ +TRANSACTION(ttSPONSORSHIP_TRANSFER, 72, SponsorTransfer, Delegation::notDelegatable, ({ + {sfObjectID, soeOPTIONAL}, +})) + +/** This transaction create sponsorship object */ +TRANSACTION(ttSPONSORSHIP_SET, 73, SponsorSet, Delegation::notDelegatable, ({ + {sfSponsorAccount, soeOPTIONAL}, + {sfSponsee, soeREQUIRED}, + {sfFeeAmount, soeOPTIONAL}, + {sfReserveCount, soeOPTIONAL}, })) /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 68d2497aca..ecad01fa48 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -540,7 +540,7 @@ JSS(reserve_inc_xrp); // out: NetworkOPs JSS(response); // websocket JSS(result); // RPC JSS(ripple_lines); // out: NetworkOPs -JSS(ripple_state); // in: LedgerEntr +JSS(ripple_state); // in: LedgerEntry JSS(ripplerpc); // ripple RPC version JSS(role); // out: Ping.cpp JSS(rpc); @@ -580,6 +580,7 @@ JSS(source_account); // in: PathRequest, RipplePathFind JSS(source_amount); // in: PathRequest, RipplePathFind JSS(source_currencies); // in: PathRequest, RipplePathFind JSS(source_tag); // out: AccountChannels +JSS(sponsee); // in: LedgerEntry JSS(stand_alone); // out: NetworkOPs JSS(standard_deviation); // out: get_aggregate_price JSS(start); // in: TxHistory diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 486945992a..969b737c0c 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -96,6 +96,7 @@ enum class LedgerNameSpace : std::uint16_t { PERMISSIONED_DOMAIN = 'm', DELEGATE = 'E', VAULT = 'V', + SPONSORSHIP = 'N', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -332,6 +333,14 @@ signers(AccountID const& account) noexcept return signers(account, 0); } +Keylet +sponsor(AccountID const& sponsor, AccountID const& sponsee) noexcept +{ + return { + ltSPONSORSHIP, + indexHash(LedgerNameSpace::SPONSORSHIP, sponsor, sponsee)}; +} + Keylet check(AccountID const& id, std::uint32_t seq) noexcept { diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index a396949afe..1093d8fc6d 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -128,6 +128,7 @@ transResults() MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."), MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecNO_DELEGATE_PERMISSION, "Delegated account lacks permission to perform this transaction."), + MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Does not have permission to sponsored this transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index bb0c64edbd..68fb63ee88 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -49,6 +49,110 @@ public: ter(temDISABLED)); env(sponsor::transfer(alice), ter(temDISABLED)); + env(sponsor::set(sponsor, alice, 0), ter(temDISABLED)); + } + + void + testInvalidSponsorSet() + { + testcase("Invalid SponsorSet"); + using namespace test::jtx; + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const noFunded("noFunded"); + Account const gw("gw"); + + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, sponsor, gw); + env.close(); + + // + // preflight + // + + // Invalid flags + { + env(sponsor::set( + sponsor, alice, ~tfSponsorSetMask - tfInnerBatchTxn), + ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + alice, + tfSponsorshipSetRequireSignForFee | + tfSponsorshipClearRequireSignForFee), + ter(temINVALID_FLAG)); + + env(sponsor::set( + sponsor, + alice, + tfSponsorshipSetRequireSignForReserve | + tfSponsorshipClearRequireSignForReserve), + ter(temINVALID_FLAG)); + + for (auto flag : + {tfSponsorshipSetRequireSignForFee, + tfSponsorshipClearRequireSignForFee, + tfSponsorshipSetRequireSignForReserve, + tfSponsorshipClearRequireSignForReserve}) + { + env(sponsor::set(sponsor, alice, tfDeleteObject | flag), + ter(temINVALID_FLAG)); + } + } + + // invalid SponsorAccount + env(sponsor::set(alice, sponsor, tfDeleteObject), + sponsor::sponsorAcc(alice), + ter(temMALFORMED)); + env(sponsor::set(alice, sponsor, tfDeleteObject), + sponsor::sponsorAcc(bob), + ter(temMALFORMED)); + env(sponsor::set(alice, alice, 0), + sponsor::sponsorAcc(sponsor), + ter(temMALFORMED)); + + // Invalid Sponsee + env(sponsor::set(sponsor, sponsor, 0), ter(temMALFORMED)); + + // Invalid feeAmount + env(sponsor::set( + sponsor, alice, tfSponsorshipClearRequireSignForFee, 0, XRP(1)), + ter(temMALFORMED)); + + for (auto amt : {XRP(-1), XRP(0), USD(1)}) + { + env(sponsor::set(sponsor, alice, 0, 1, amt), ter(temBAD_AMOUNT)); + } + + // Invalid reserveCount + env(sponsor::set( + sponsor, alice, tfSponsorshipClearRequireSignForReserve, 1), + ter(temMALFORMED)); + env(sponsor::set(sponsor, alice, 0, 0), ter(temMALFORMED)); + + // Invalid Delete operation + env(sponsor::set(sponsor, alice, tfDeleteObject, 1), ter(temMALFORMED)); + env(sponsor::set(sponsor, alice, tfDeleteObject, std::nullopt, XRP(1)), + ter(temMALFORMED)); + + // + // preclaim + // + + // Invalid Sponsee + env(sponsor::set(sponsor, noFunded, 0), ter(tecNO_DST)); + + // Invalid Delete operation (not found) + env(sponsor::set(sponsor, alice, tfDeleteObject), ter(tecNO_ENTRY)); + + // DisallowIncomingSponsor: tested in other testcase + + // create sponsor to use above tests + env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), ter(tesSUCCESS)); + env.close(); } void @@ -116,12 +220,6 @@ public: env(signers(sponsor, 1, {{signer1, 1}, {signer2, 1}})); env.close(); - // Signature doesn't exist - env(noop(alice), - fee(XRP(1)), - sponsor::as(sponsor, tfSponsorReserve), - ter(telENV_RPC_FAILED)); - // Invalid signature auto tx = noop(alice); auto& signers1 = @@ -367,19 +465,57 @@ public: using namespace test::jtx; testcase("Sponsor Fee"); - Env env{*this, testable_amendments()}; - Account const alice("alice"); - Account const sponsor("sponsor"); - env.fund(XRP(10000), alice, sponsor); - env(noop(alice), - fee(XRP(1)), - sponsor::as(sponsor, tfSponsorFee), - sponsor::sig(sponsor)); - env.close(); + { + // co-signing + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); - BEAST_EXPECT(env.balance(alice) == XRP(10000)); - BEAST_EXPECT(env.balance(sponsor) == XRP(9999)); + env(noop(alice), + fee(XRP(1)), + sponsor::as(sponsor, tfSponsorFee), + sponsor::sig(sponsor), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == XRP(10000)); + BEAST_EXPECT(env.balance(sponsor) == XRP(9999)); + } + { + // pre funded + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + env.fund(XRP(10000), alice, sponsor); + env.close(); + + env(sponsor::set(sponsor, alice, 0, std::nullopt, XRP(1)), + ter(tesSUCCESS)); + env.close(); + + auto const sle = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT(sle->getFieldAmount(sfFeeAmount) == XRP(1)); + BEAST_EXPECT(!sle->isFieldPresent(sfReserveCount)); + + auto const sponsorBalanceBefore = env.balance(sponsor); + auto const aliceBalanceBefore = env.balance(alice); + + env(noop(alice), + fee(drops(500)), + sponsor::as(sponsor, tfSponsorFee), + ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(env.balance(alice) == aliceBalanceBefore); + BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore); + + auto const sle2 = env.le(keylet::sponsor(sponsor, alice)); + BEAST_EXPECT( + sle2->getFieldAmount(sfFeeAmount) == XRP(1) - drops(500)); + } } void @@ -418,6 +554,7 @@ public: env.fund(XRP(10000), alice, bob); env.fund(drops(reserve) + drops(increment) - drops(1), sponsor); + env.close(); // check sponsor balance env(check::create(alice, bob, XRP(1)), @@ -483,6 +620,7 @@ public: auto USD = gw["USD"]; env.fund(XRP(10000), alice, gw, sponsor); + env.close(); // OfferCreate auto const seq = env.seq(alice); @@ -518,6 +656,7 @@ public: Account const sponsor("master"); env.fund(XRP(1000000), alice, sponsor); + env.close(); // TicketCreate std::uint32_t const ticketSeq{env.seq(alice) + 1}; @@ -551,6 +690,7 @@ public: Account const sponsor("sponsor"); env.fund(XRP(1000000), issuer, subject, sponsor); + env.close(); // CredentialsCreate env(credentials::create(subject, issuer, "credType"), @@ -599,6 +739,7 @@ public: Account const sponsor("sponsor"); env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); // DelegateSet env(delegate::set(alice, bob, {"Payment"}), @@ -634,6 +775,7 @@ public: Account const sponsor("sponsor"); env.fund(XRP(1000000), alice, sponsor); + env.close(); // DIDSet env(did::set(alice), @@ -700,6 +842,7 @@ public: Account const sponsor("sponsor"); env.fund(XRP(1000000), alice, sponsor); + env.close(); Account const bob("bob"); @@ -737,6 +880,73 @@ public: { } + void + testDisallowIncoming() + { + testcase("DisallowIncoming"); + using namespace test::jtx; + Env env{*this, testable_amendments()}; + Account const alice("alice"); + Account const sponsor("sponsor"); + + env.fund(XRP(1000000), alice, sponsor); + env.close(); + + // set DisallowIncomingSponsor + env(fset(alice, asfDisallowIncomingSponsor)); + env.close(); + + // Create sponsor should fail + env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), + ter(tecNO_PERMISSION)); + env.close(); + + // clear flag + env(fclear(alice, asfDisallowIncomingSponsor)); + env.close(); + + // Create sponsor + env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // set flag + env(fset(alice, asfDisallowIncomingSponsor)); + env.close(); + + // Update sponsor should success + env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // Delete sponsor shoud success + env(sponsor::set(sponsor, alice, tfDeleteObject), ter(tesSUCCESS)); + env.close(); + } + void + testAccountDelete() + { + 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(); + + // set sponsor + env(sponsor::set(sponsor, alice, 0, 100, XRP(100)), ter(tesSUCCESS)); + env.close(); + + // AccountDelete + env(acctdelete(alice, bob)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + void testSponsorReserve() { @@ -764,12 +974,19 @@ public: run() override { testDisabled(); + testInvalidSponsorSet(); + testSingleSigning(); testMultiSigning(); + // testInvalidSigninig(); // borh TxnSignature and Signers are present + // -> error testTransferSponsor(); testSponsorFee(); testSponsorAccount(); testSponsorReserve(); + testDisallowIncoming(); + + // testAccountDelete(); } }; diff --git a/src/test/jtx/impl/sponsor.cpp b/src/test/jtx/impl/sponsor.cpp index 4d8340db96..f28c643b46 100644 --- a/src/test/jtx/impl/sponsor.cpp +++ b/src/test/jtx/impl/sponsor.cpp @@ -22,6 +22,7 @@ #include #include +#include #include namespace ripple { @@ -30,6 +31,36 @@ namespace jtx { namespace sponsor { +Json::Value +set(jtx::Account const& account, + jtx::Account const& sponsee, + uint32_t flags, + std::optional reserveCount, + std::optional feeAmount) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::SponsorSet; + jv[jss::Account] = account.human(); + jv[sfSponsee.jsonName] = sponsee.human(); + jv[sfFlags.jsonName] = flags; + if (reserveCount) + jv[sfReserveCount.jsonName] = *reserveCount; + if (feeAmount) + jv[sfFeeAmount.jsonName] = feeAmount->getJson(JsonOptions::none); + return jv; +} + +Json::Value +del(jtx::Account const& account, jtx::Account const& sponsee) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::SponsorSet; + jv[jss::Account] = account.human(); + jv[sfSponsee.jsonName] = sponsee.human(); + jv[sfFlags.jsonName] = tfDeleteObject; + return jv; +} + Json::Value transfer(jtx::Account const& account, std::optional const& index) { @@ -37,10 +68,16 @@ transfer(jtx::Account const& account, std::optional const& index) jv[jss::TransactionType] = jss::SponsorTransfer; jv[jss::Account] = account.human(); if (index) - jv[sfLedgerIndex.jsonName] = to_string(*index); + jv[sfObjectID.jsonName] = to_string(*index); return jv; } +void +sponsorAcc::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSponsorAccount.jsonName] = sponsor_.human(); +} + void as::operator()(Env& env, JTx& jt) const { @@ -85,7 +122,6 @@ sig::operator()(Env& env, JTx& jt) const void msig::operator()(Env& env, JTx& jt) const { - jt.jv[sfSponsor.jsonName][sfSigningPubKey.jsonName] = ""; auto const mySigners = signers; jt.sponsorSigner = [mySigners, &env](Env&, JTx& jtx) { std::optional st; diff --git a/src/test/jtx/sponsor.h b/src/test/jtx/sponsor.h index 683c2e0ae4..bd603c0d41 100644 --- a/src/test/jtx/sponsor.h +++ b/src/test/jtx/sponsor.h @@ -21,8 +21,7 @@ #include #include - -#include "test/jtx/SignerUtils.h" +#include namespace ripple { namespace test { @@ -30,11 +29,34 @@ namespace jtx { namespace sponsor { +Json::Value +set(jtx::Account const& account, + jtx::Account const& sponsee, + std::uint32_t flags, + std::optional reserveCount = std::nullopt, + std::optional feeAmount = std::nullopt); + +Json::Value +del(jtx::Account const& account, jtx::Account const& sponsee); + Json::Value transfer( jtx::Account const& account, std::optional const& index = std::nullopt); +struct sponsorAcc +{ +private: + jtx::Account sponsor_; + +public: + sponsorAcc(jtx::Account const& account) : sponsor_(account) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const; +}; struct as { private: diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index 3615a715cd..2383434b9d 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -87,7 +87,8 @@ public: if (flag == asfDisallowIncomingCheck || flag == asfDisallowIncomingPayChan || flag == asfDisallowIncomingNFTokenOffer || - flag == asfDisallowIncomingTrustline) + flag == asfDisallowIncomingTrustline || + flag == asfDisallowIncomingSponsor) { // These flags are part of the DisallowIncoming amendment // and are tested elsewhere diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index 4311aa79a8..d7398e3fae 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -194,6 +195,18 @@ removeDelegateFromLedger( return DelegateSet::deleteDelegate(view, sleDel, account, j); } +TER +removeSponsorshipFromLedger( + Application& app, + ApplyView& view, + AccountID const&, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return SponsorSet::deleteSponsorship(view, sleDel, j); +} + // Return nullptr if the LedgerEntryType represents an obligation that can't // be deleted. Otherwise return the pointer to the function that can delete // the non-obligation @@ -220,6 +233,8 @@ nonObligationDeleter(LedgerEntryType t) return removeCredentialFromLedger; case ltDELEGATE: return removeDelegateFromLedger; + case ltSPONSORSHIP: + return removeSponsorshipFromLedger; default: return nullptr; } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index da0dfc117f..fd1ab52f00 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -112,6 +112,10 @@ XRPNotCreated::visitEntry( if (isXRP((*before)[sfAmount])) drops_ -= (*before)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (before->isFieldPresent(sfFeeAmount)) + drops_ -= (*before)[sfFeeAmount].xrp().drops(); + break; default: break; } @@ -134,6 +138,10 @@ XRPNotCreated::visitEntry( if (!isDelete && isXRP((*after)[sfAmount])) drops_ += (*after)[sfAmount].xrp().drops(); break; + case ltSPONSORSHIP: + if (!isDelete && after->isFieldPresent(sfFeeAmount)) + drops_ += (*after)[sfFeeAmount].xrp().drops(); + break; default: break; } @@ -543,6 +551,7 @@ LedgerEntryTypesMatch::visitEntry( case ltCREDENTIAL: case ltPERMISSIONED_DOMAIN: case ltVAULT: + case ltSPONSORSHIP: break; default: invalidTypeAdded_ = true; diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index ec618981c1..55154b1aa6 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -648,6 +648,14 @@ SetAccount::doApply() uFlagsOut |= lsfDisallowIncomingTrustline; else if (uClearFlag == asfDisallowIncomingTrustline) uFlagsOut &= ~lsfDisallowIncomingTrustline; + + if (ctx_.view().rules().enabled(featureSponsor)) + { + if (uSetFlag == asfDisallowIncomingSponsor) + uFlagsOut |= lsfDisallowIncomingSponsor; + else if (uClearFlag == asfDisallowIncomingSponsor) + uFlagsOut &= ~lsfDisallowIncomingSponsor; + } } // Set or clear flags for disallowing escrow diff --git a/src/xrpld/app/tx/detail/SponsorSet.cpp b/src/xrpld/app/tx/detail/SponsorSet.cpp new file mode 100644 index 0000000000..4281dae7c9 --- /dev/null +++ b/src/xrpld/app/tx/detail/SponsorSet.cpp @@ -0,0 +1,273 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include + +namespace ripple { + +NotTEC +SponsorSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureSponsor)) + return temDISABLED; + + if (auto const ter = preflight1(ctx)) + return ter; + + // check Flags + { + if (ctx.tx.getFlags() & tfSponsorSetMask) + return temINVALID_FLAG; + + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForFee) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + return temINVALID_FLAG; + + if (ctx.tx.isFlag(tfSponsorshipSetRequireSignForReserve) && + ctx.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + return temINVALID_FLAG; + + if (ctx.tx.isFlag(tfDeleteObject)) + { + // check Flags + if (ctx.tx.getFlags() & + (tfSponsorshipSetRequireSignForFee | + tfSponsorshipSetRequireSignForReserve | + tfSponsorshipClearRequireSignForFee | + tfSponsorshipClearRequireSignForReserve)) + return temINVALID_FLAG; + } + } + + if (ctx.tx.isFieldPresent(sfSponsorAccount)) + { + // SponsorAccount is used when sponsee Deleting ltSponsorship + // Account => Sponsee of Sponsorshop + // SponsorAccount => Sponsor of Sponsorshop + // Sponsee => Sponsee of Sponsorshop + if (ctx.tx.getAccountID(sfAccount) == + ctx.tx.getAccountID(sfSponsorAccount) || + ctx.tx.getAccountID(sfSponsee) != + ctx.tx.getAccountID(sfSponsorAccount) || + !ctx.tx.isFlag(tfDeleteObject)) + return temMALFORMED; + } + + auto const sponsor = ctx.tx.isFieldPresent(sfSponsorAccount) + ? ctx.tx.getAccountID(sfSponsorAccount) + : ctx.tx.getAccountID(sfAccount); + auto const sponsee = ctx.tx.getAccountID(sfSponsee); + + if (sponsee == sponsor) + return temMALFORMED; + + if (ctx.tx.isFieldPresent(sfFeeAmount)) + { + if (ctx.tx.getFlags() & tfSponsorshipClearRequireSignForFee) + return temMALFORMED; + + auto const feeAmount = ctx.tx.getFieldAmount(sfFeeAmount); + + if (!isXRP(feeAmount)) + return temBAD_AMOUNT; + + if (feeAmount.xrp().drops() <= 0) + return temBAD_AMOUNT; + } + + if (ctx.tx.isFieldPresent(sfReserveCount)) + { + if (ctx.tx.getFlags() & tfSponsorshipClearRequireSignForReserve) + return temMALFORMED; + + auto const reserveCount = ctx.tx.getFieldU32(sfReserveCount); + // TODO: max reserveCount? + if (reserveCount < 1) + return temMALFORMED; + } + + if (ctx.tx.isFlag(tfDeleteObject)) + { + if (ctx.tx.isFieldPresent(sfFeeAmount) || + ctx.tx.isFieldPresent(sfReserveCount)) + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +SponsorSet::preclaim(PreclaimContext const& ctx) +{ + auto const sponsor = ctx.tx.isFieldPresent(sfSponsorAccount) + ? ctx.tx.getAccountID(sfSponsorAccount) + : ctx.tx.getAccountID(sfAccount); + auto const sponsee = ctx.tx[sfSponsee]; + + // check Sponsee + auto const sponseeSle = ctx.view.read(keylet::account(sponsee)); + if (!sponseeSle) + return tecNO_DST; + + // check if object exists + auto const sponsorObjSle = ctx.view.read(keylet::sponsor(sponsor, sponsee)); + + if (ctx.tx.isFlag(tfDeleteObject) && !sponsorObjSle) + return tecNO_ENTRY; + + if (sponseeSle->isFlag(lsfDisallowIncomingSponsor) && !sponsorObjSle) + // new sponsor creation is not allowed by disallowIncomingSponsor flag + return tecNO_PERMISSION; + + return tesSUCCESS; +} + +TER +SponsorSet::doApply() +{ + auto const sponseeAcc = ctx_.tx[sfSponsee]; + auto const keylet = keylet::sponsor(account_, sponseeAcc); + + auto const sponsorAcc = ctx_.tx.isFieldPresent(sfSponsorAccount) + ? ctx_.tx.getAccountID(sfSponsorAccount) + : account_; + + auto const sponsorAccSle = ctx_.view().peek(keylet::account(sponsorAcc)); + if (!sponsorAccSle) + return tecINTERNAL; + + auto const sponsorObjSle = ctx_.view().peek(keylet); + + if (ctx_.tx.isFlag(tfDeleteObject)) + { + // Delete + if (!sponsorObjSle) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sponsor = + getLedgerEntryReserveSponsor(ctx_.view(), sponsorObjSle); + adjustOwnerCount(ctx_.view(), sponsorAccSle, sponsor, -1, ctx_.journal); + + ctx_.view().dirRemove( + keylet::ownerDir(sponsorAcc), + (*sponsorObjSle)[sfSponsorNode], + sponsorObjSle->key(), + false); + ctx_.view().dirRemove( + keylet::ownerDir(sponseeAcc), + (*sponsorObjSle)[sfSponseeNode], + sponsorObjSle->key(), + false); + + // transfer feeAmount from ledger entry + auto const feeAmount = sponsorObjSle->getFieldAmount(sfFeeAmount); + (*sponsorAccSle)[sfBalance] += feeAmount; + + ctx_.view().erase(sponsorObjSle); + + return tesSUCCESS; + } + + auto const feeAmount = ctx_.tx[~sfFeeAmount]; + auto const reserveCount = ctx_.tx[~sfReserveCount]; + + auto reserveSponsorAccSle = getTxReserveSponsor(view(), ctx_.tx); + + if (!sponsorObjSle) + { + // Create + auto newSle = std::make_shared(keylet); + + if (auto const ret = checkInsufficientReserve( + ctx_.view(), + sponsorAccSle, + mPriorBalance, + reserveSponsorAccSle, + 1); + !isTesSuccess(ret)) + return tecUNFUNDED; + + (*newSle)[sfAccount] = sponsorAcc; + (*newSle)[sfSponsee] = sponseeAcc; + (*newSle)[sfFlags] = ctx_.tx.getFlags(); + if (feeAmount) + { + (*sponsorAccSle)[sfBalance] -= *feeAmount; + (*newSle)[sfFeeAmount] = *feeAmount; + } + if (reserveCount) + { + (*newSle)[sfReserveCount] = *reserveCount; + } + + auto const sponsorPage = view().dirInsert( + keylet::ownerDir(sponsorAcc), keylet, describeOwnerDir(sponsorAcc)); + (*newSle)[sfSponsorNode] = *sponsorPage; + + auto const sponseePage = view().dirInsert( + keylet::ownerDir(sponseeAcc), keylet, describeOwnerDir(sponseeAcc)); + (*newSle)[sfSponseeNode] = *sponseePage; + + auto viewJ = ctx_.app.journal("View"); + + adjustOwnerCount(view(), sponsorAccSle, reserveSponsorAccSle, 1, viewJ); + addSponsorToLedgerEntry(newSle, reserveSponsorAccSle); + + ctx_.view().insert(newSle); + return tesSUCCESS; + } + + // Update + if (feeAmount) + { + // TODO: transfer feeAmount to ledger entry + (*sponsorAccSle)[sfBalance] -= *feeAmount; + (*sponsorObjSle)[sfFeeAmount] += *feeAmount; + } + + if (reserveCount) + (*sponsorObjSle)[sfReserveCount] = + (*sponsorObjSle)[sfReserveCount] + *reserveCount; + + // TODO: update Flags? + auto flags = sponsorObjSle->getFieldU32(sfFlags); + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForFee)) + flags |= lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForFee)) + flags &= ~lsfSponsorshipRequireSignForFee; + + if (ctx_.tx.isFlag(tfSponsorshipSetRequireSignForReserve)) + flags |= lsfSponsorshipRequireSignForReserve; + + if (ctx_.tx.isFlag(tfSponsorshipClearRequireSignForReserve)) + flags &= ~lsfSponsorshipRequireSignForReserve; + + if (flags != (*sponsorObjSle)[sfFlags]) + (*sponsorObjSle)[sfFlags] = flags; + + ctx_.view().update(sponsorObjSle); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/SponsorSet.h b/src/xrpld/app/tx/detail/SponsorSet.h new file mode 100644 index 0000000000..f2ed220bb0 --- /dev/null +++ b/src/xrpld/app/tx/detail/SponsorSet.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_SPONSORSET_H_INCLUDED +#define RIPPLE_TX_SPONSORSET_H_INCLUDED + +#include + +namespace ripple { + +class SponsorSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit SponsorSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/SponsorTransfer.cpp b/src/xrpld/app/tx/detail/SponsorTransfer.cpp index d58d0b511f..7dc0909a88 100644 --- a/src/xrpld/app/tx/detail/SponsorTransfer.cpp +++ b/src/xrpld/app/tx/detail/SponsorTransfer.cpp @@ -79,8 +79,8 @@ getLedgerEntryOwner( auto const signerList = view.read(keylet::signers(account)); if (!signerList) return std::nullopt; - if (signerList->getFieldH256(sfLedgerIndex) == - sle->getFieldH256(sfLedgerIndex)) + if (signerList->getFieldH256(sfObjectID) == + sle->getFieldH256(sfObjectID)) return account; return std::nullopt; } @@ -110,7 +110,7 @@ getLedgerEntryOwner( TER SponsorTransfer::preclaim(PreclaimContext const& ctx) { - auto const index = ctx.tx[~sfLedgerIndex]; + auto const index = ctx.tx[~sfObjectID]; auto const newSponsor = getTxReserveSponsor(ctx.view, ctx.tx); bool const isObjectSponsor = index != std::nullopt; @@ -198,7 +198,7 @@ SponsorTransfer::doApply() { auto const& tx = ctx_.tx; - auto const index = tx[~sfLedgerIndex]; + auto const index = tx[~sfObjectID]; bool const isObjectSponsor = index != std::nullopt; auto const accSle = view().peek(keylet::account(account_)); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 4ad45d13ac..c6c42cc57a 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -167,18 +167,11 @@ preflight1(PreflightContext const& ctx) JLOG(ctx.j.fatal()) << "preflight1: invalid sponsor account"; return temMALFORMED; } - if (!(sponsor[sfFlags] & tfSponsorFee) && - !(sponsor[sfFlags] & tfSponsorReserve)) + if (!(sponsor.getFlags() & tfSponsorMask)) { JLOG(ctx.j.fatal()) << "preflight1: invalid sponsor flags"; return temMALFORMED; } - if (!sponsor.isFieldPresent(sfTxnSignature) && - !sponsor.isFieldPresent(sfSigners)) - { - JLOG(ctx.j.fatal()) << "preflight1: no sfTxnSignature or sfSigners"; - return temMALFORMED; - } } return tesSUCCESS; @@ -255,6 +248,41 @@ Transactor::checkPermission(ReadView const& view, STTx const& tx) return checkTxPermission(sle, tx); } +TER +Transactor::checkSponsor(ReadView const& view, STTx const& tx) +{ + if (!tx.isFieldPresent(sfSponsor)) + return tesSUCCESS; + + auto const txSponsor = tx.getFieldObject(sfSponsor); + + 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)) + { + if (!hasSignature) + return tecNO_SPONSOR_PERMISSION; + } + if (txSponsor.isFlag(tfSponsorReserve) && + sponsorSle->isFlag(lsfSponsorshipRequireSignForReserve)) + { + if (!hasSignature) + return tecNO_SPONSOR_PERMISSION; + } + + return tesSUCCESS; +} + XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -263,6 +291,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) // The computation has two parts: // * The base fee, which is the same for most transactions. // * The additional cost of each multisignature on the transaction. + // * The additional cost of each multisignature on the sponsor. XRPAmount const baseFee = view.fees().base; // Each signer adds one more baseFee to the minimum required fee @@ -270,7 +299,16 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) std::size_t const signerCount = tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0; - return baseFee + (signerCount * baseFee); + std::size_t sponsorSignerCount = 0; + if (tx.isFieldPresent(sfSponsor)) + { + auto const sponsorObj = tx.getFieldObject(sfSponsor); + sponsorSignerCount += sponsorObj.isFieldPresent(sfSigners) + ? sponsorObj.getFieldArray(sfSigners).size() + : 0; + } + + return baseFee + ((signerCount + sponsorSignerCount) * baseFee); } XRPAmount @@ -352,6 +390,17 @@ Transactor::payFee() { auto const feePaid = ctx_.tx[sfFee].xrp(); + auto const isFeeSponsorObj = [&]() -> bool { + if (ctx_.tx.isFieldPresent(sfSponsor)) + { + auto const sponsor = ctx_.tx.getFieldObject(sfSponsor); + if (sponsor.getFieldVL(sfSigningPubKey).empty() && + !sponsor.isFieldPresent(sfSigners)) + return sponsor.getFlags() & tfSponsorFee; + } + return false; + }; + if (ctx_.tx.isFieldPresent(sfDelegate)) { // Delegated transactions are paid by the delegated account. @@ -364,6 +413,19 @@ Transactor::payFee() sfBalance, delegatedSle->getFieldAmount(sfBalance) - feePaid); view().update(delegatedSle); } + else if (isFeeSponsorObj()) + { + auto const sponsor = ctx_.tx.getFieldObject(sfSponsor); + auto const sponsorAcc = sponsor.getAccountID(sfAccount); + auto const sponsorSle = + view().peek(keylet::sponsor(sponsorAcc, account_)); + if (!sponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + sponsorSle->setFieldAmount( + sfFeeAmount, sponsorSle->getFieldAmount(sfFeeAmount) - feePaid); + view().update(sponsorSle); + } else { auto const id = ctx_.tx.getFeePayer(); @@ -649,8 +711,15 @@ Transactor::checkSign(PreclaimContext const& ctx) if (ctx.tx.isFieldPresent(sfSponsor)) { - if (auto const ret = checkSponsorSign(ctx); !isTesSuccess(ret)) - return ret; + auto const sponsorObj = ctx.tx.getFieldObject(sfSponsor); + auto const isCoSigned = sponsorObj.isFieldPresent(sfTxnSignature) || + !sponsorObj.getFieldVL(sfSigningPubKey).empty() || + sponsorObj.isFieldPresent(sfSigners); + if (isCoSigned) + { + if (auto const ret = checkSponsorSign(ctx); !isTesSuccess(ret)) + return ret; + } } // Check Single Sign diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 08a52cf226..d5beac5805 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -209,6 +209,9 @@ public: static TER checkPermission(ReadView const& view, STTx const& tx); + + static TER + checkSponsor(ReadView const& view, STTx const& tx); ///////////////////////////////////////////////////// // Interface used by DeleteAccount diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index e455675bb8..6b91314ad0 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -85,12 +85,19 @@ checkValidity( if (tx.isFieldPresent(sfSponsor) && rules.enabled(featureSponsor)) { - auto const sigVerify = - tx.checkSponsorSign(requireCanonicalSig, rules); - if (!sigVerify) + auto const sponsorObj = tx.getFieldObject(sfSponsor); + auto const isCoSigned = sponsorObj.isFieldPresent(sfTxnSignature) || + !sponsorObj.getFieldVL(sfSigningPubKey).empty() || + sponsorObj.isFieldPresent(sfSigners); + if (isCoSigned) { - router.setFlags(id, SF_SIGBAD); - return {Validity::SigBad, sigVerify.error()}; + auto const sigVerify = + tx.checkSponsorSign(requireCanonicalSig, rules); + if (!sigVerify) + { + router.setFlags(id, SF_SIGBAD); + return {Validity::SigBad, sigVerify.error()}; + } } } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index f47d47ebef..8f25145aa5 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -62,6 +62,7 @@ #include #include #include +#include #include #include #include @@ -206,6 +207,11 @@ 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; diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 61a7e2fb2c..930c54d49f 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -629,6 +629,27 @@ parseVault(Json::Value const& params, Json::StaticString const fieldName) return keylet::vault(*id, *seq).key; } +static Expected +parseSponsorship(Json::Value const& params, Json::StaticString const fieldName) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName); + } + + auto const id = LedgerEntryHelpers::requiredAccountID( + params, jss::owner, "malformedOwner"); + if (!id) + return Unexpected(id.error()); + + auto const sponsee = LedgerEntryHelpers::requiredAccountID( + params, jss::sponsee, "malformedAddress"); + if (!sponsee) + return Unexpected(sponsee.error()); + + return keylet::sponsor(*id, *sponsee).key; +} + static Expected parseXChainOwnedClaimID( Json::Value const& claim_id,