v2. SponsorSet

This commit is contained in:
tequ
2025-09-13 09:36:38 +09:00
parent 02d8f9fbef
commit e589b71ee0
25 changed files with 840 additions and 47 deletions

View File

@@ -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

View File

@@ -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,
};
//------------------------------------------------------------------------------

View File

@@ -362,6 +362,7 @@ enum TECcodes : TERUnderlyingType {
tecPSEUDO_ACCOUNT = 196,
tecPRECISION_LOSS = 197,
tecNO_DELEGATE_PERMISSION = 198,
tecNO_SPONSOR_PERMISSION = 199,
};
//------------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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
{

View File

@@ -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."),

View File

@@ -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();
}
};

View File

@@ -22,6 +22,7 @@
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/Sponsor.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
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<uint32_t> reserveCount,
std::optional<STAmount> 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<uint256> const& index)
{
@@ -37,10 +68,16 @@ transfer(jtx::Account const& account, std::optional<uint256> 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<STObject> st;

View File

@@ -21,8 +21,7 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include "test/jtx/SignerUtils.h"
#include <test/jtx/SignerUtils.h>
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<std::uint32_t> reserveCount = std::nullopt,
std::optional<STAmount> feeAmount = std::nullopt);
Json::Value
del(jtx::Account const& account, jtx::Account const& sponsee);
Json::Value
transfer(
jtx::Account const& account,
std::optional<uint256> 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:

View File

@@ -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

View File

@@ -25,6 +25,7 @@
#include <xrpld/app/tx/detail/DepositPreauth.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/app/tx/detail/SponsorSet.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
@@ -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<SLE> 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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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 <xrpld/app/tx/detail/SponsorSet.h>
#include <xrpld/ledger/View.h>
#include <xrpl/protocol/TxFlags.h>
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<SLE>(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

View File

@@ -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 <xrpld/app/tx/detail/Transactor.h>
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

View File

@@ -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_));

View File

@@ -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

View File

@@ -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

View File

@@ -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()};
}
}
}

View File

@@ -62,6 +62,7 @@
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpld/app/tx/detail/SetSignerList.h>
#include <xrpld/app/tx/detail/SetTrust.h>
#include <xrpld/app/tx/detail/SponsorSet.h>
#include <xrpld/app/tx/detail/SponsorTransfer.h>
#include <xrpld/app/tx/detail/VaultClawback.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
@@ -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;

View File

@@ -629,6 +629,27 @@ parseVault(Json::Value const& params, Json::StaticString const fieldName)
return keylet::vault(*id, *seq).key;
}
static Expected<uint256, Json::Value>
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<uint256, Json::Value>
parseXChainOwnedClaimID(
Json::Value const& claim_id,