diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index 2e7cb3b410f..66b4dd178c1 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -120,7 +120,7 @@ enum error_code_i { rpcSRC_ACT_MALFORMED = 65, rpcSRC_ACT_MISSING = 66, rpcSRC_ACT_NOT_FOUND = 67, - // unused 68, + rpcDELEGATE_ACT_NOT_FOUND = 68, rpcSRC_CUR_MALFORMED = 69, rpcSRC_ISR_MALFORMED = 70, rpcSTREAM_MALFORMED = 71, diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index bbed5395927..979a994c107 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -279,6 +279,10 @@ amm(Asset const& issue1, Asset const& issue2) noexcept; Keylet amm(uint256 const& amm) noexcept; +/** A keylet for Delegate object */ +Keylet +delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept; + Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType); diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h new file mode 100644 index 00000000000..eb2c733313e --- /dev/null +++ b/include/xrpl/protocol/Permissions.h @@ -0,0 +1,97 @@ +//------------------------------------------------------------------------------ +/* + 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_PROTOCOL_PERMISSION_H_INCLUDED +#define RIPPLE_PROTOCOL_PERMISSION_H_INCLUDED + +#include + +#include +#include +#include +#include + +namespace ripple { +/** + * We have both transaction type permissions and granular type permissions. + * Since we will reuse the TransactionFormats to parse the Transaction + * Permissions, only the GranularPermissionType is defined here. To prevent + * conflicts with TxType, the GranularPermissionType is always set to a value + * greater than the maximum value of uint16. + */ +enum GranularPermissionType : std::uint32_t { +#pragma push_macro("PERMISSION") +#undef PERMISSION + +#define PERMISSION(type, txType, value) type = value, + +#include + +#undef PERMISSION +#pragma pop_macro("PERMISSION") +}; + +enum Delegation { delegatable, notDelegatable }; + +class Permission +{ +private: + Permission(); + + std::unordered_map delegatableTx_; + + std::unordered_map + granularPermissionMap_; + + std::unordered_map granularNameMap_; + + std::unordered_map granularTxTypeMap_; + +public: + static Permission const& + getInstance(); + + Permission(const Permission&) = delete; + Permission& + operator=(const Permission&) = delete; + + std::optional + getGranularValue(std::string const& name) const; + + std::optional + getGranularName(GranularPermissionType const& value) const; + + std::optional + getGranularTxType(GranularPermissionType const& gpType) const; + + bool + isDelegatable(std::uint32_t const& permissionValue) const; + + // for tx level permission, permission value is equal to tx type plus one + uint32_t + txToPermissionType(const TxType& type) const; + + // tx type value is permission value minus one + TxType + permissionToTxType(uint32_t const& value) const; +}; + +} // namespace ripple + +#endif diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 1e8c76dbd8b..041b53d6cbc 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -155,6 +155,10 @@ std::size_t constexpr maxPriceScale = 20; */ std::size_t constexpr maxTrim = 25; +/** The maximum number of delegate permissions an account can grant + */ +std::size_t constexpr permissionMaxSize = 10; + } // namespace ripple #endif diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index e94e79aee5e..7a600676f80 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -120,6 +120,13 @@ constexpr std::uint32_t tfTrustSetMask = ~(tfUniversal | tfSetfAuth | tfSetNoRipple | tfClearNoRipple | tfSetFreeze | tfClearFreeze | tfSetDeepFreeze | tfClearDeepFreeze); +// valid flags for granular permission +constexpr std::uint32_t tfTrustSetGranularMask = tfSetfAuth | tfSetFreeze | tfClearFreeze; + +// bits representing supportedGranularMask are set to 0 and the bits +// representing other flags are set to 1 in tfPermissionMask. +constexpr std::uint32_t tfTrustSetPermissionMask = (~tfTrustSetMask) & (~tfTrustSetGranularMask); + // EnableAmendment flags: constexpr std::uint32_t tfGotMajority = 0x00010000; constexpr std::uint32_t tfLostMajority = 0x00020000; @@ -155,6 +162,8 @@ constexpr std::uint32_t const tfMPTokenAuthorizeMask = ~(tfUniversal | tfMPTUna constexpr std::uint32_t const tfMPTLock = 0x00000001; constexpr std::uint32_t const tfMPTUnlock = 0x00000002; constexpr std::uint32_t const tfMPTokenIssuanceSetMask = ~(tfUniversal | tfMPTLock | tfMPTUnlock); +constexpr std::uint32_t const tfMPTokenIssuanceSetGranularMask = tfMPTLock | tfMPTUnlock; +constexpr std::uint32_t const tfMPTokenIssuanceSetPermissionMask = (~tfMPTokenIssuanceSetMask) & (~tfMPTokenIssuanceSetGranularMask); // MPTokenIssuanceDestroy flags: constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal; diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index 7eb6fb72f7a..70b721a3d7d 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -59,7 +59,7 @@ enum TxType : std::uint16_t #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, fields) tag = value, +#define TRANSACTION(tag, value, name, delegatable, fields) tag = value, #include diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index ac393eae985..31b5c25d912 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) // Check flags in Credential transactions XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 5a652baf4f7..66573eaf4af 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -460,6 +460,18 @@ LEDGER_ENTRY(ltPERMISSIONED_DOMAIN, 0x0082, PermissionedDomain, permissioned_dom {sfPreviousTxnLgrSeq, soeREQUIRED}, })) +/** A ledger object representing permissions an account has delegated to another account. + \sa keylet::delegate + */ +LEDGER_ENTRY(ltDELEGATE, 0x0083, Delegate, delegate, ({ + {sfAccount, soeREQUIRED}, + {sfAuthorize, soeREQUIRED}, + {sfPermissions, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/permissions.macro b/include/xrpl/protocol/detail/permissions.macro new file mode 100644 index 00000000000..ec19c5767f3 --- /dev/null +++ b/include/xrpl/protocol/detail/permissions.macro @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#if !defined(PERMISSION) +#error "undefined macro: PERMISSION" +#endif + +/** + * PERMISSION(name, type, txType, value) + * + * This macro defines a permission: + * name: the name of the permission. + * type: the GranularPermissionType enum. + * txType: the corresponding TxType for this permission. + * value: the uint32 numeric value for the enum type. + */ + +/** This permission grants the delegated account the ability to authorize a trustline. */ +PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537) + +/** This permission grants the delegated account the ability to freeze a trustline. */ +PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538) + +/** This permission grants the delegated account the ability to unfreeze a trustline. */ +PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539) + +/** This permission grants the delegated account the ability to set Domain. */ +PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540) + +/** This permission grants the delegated account the ability to set EmailHashSet. */ +PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541) + +/** This permission grants the delegated account the ability to set MessageKey. */ +PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542) + +/** This permission grants the delegated account the ability to set TransferRate. */ +PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543) + +/** This permission grants the delegated account the ability to set TickSize. */ +PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544) + +/** This permission grants the delegated account the ability to mint payment, which means sending a payment for a currency where the sending account is the issuer. */ +PERMISSION(PaymentMint, ttPAYMENT, 65545) + +/** This permission grants the delegated account the ability to burn payment, which means sending a payment for a currency where the destination account is the issuer */ +PERMISSION(PaymentBurn, ttPAYMENT, 65546) + +/** This permission grants the delegated account the ability to lock MPToken. */ +PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547) + +/** This permission grants the delegated account the ability to unlock MPToken. */ +PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3217bab9134..e98709c8c38 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -112,6 +112,7 @@ TYPED_SFIELD(sfEmitGeneration, UINT32, 46) TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) +TYPED_SFIELD(sfPermissionValue, UINT32, 52) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -278,6 +279,7 @@ TYPED_SFIELD(sfRegularKey, ACCOUNT, 8) TYPED_SFIELD(sfNFTokenMinter, ACCOUNT, 9) TYPED_SFIELD(sfEmitCallback, ACCOUNT, 10) TYPED_SFIELD(sfHolder, ACCOUNT, 11) +TYPED_SFIELD(sfDelegate, ACCOUNT, 12) // account (uncommon) TYPED_SFIELD(sfHookAccount, ACCOUNT, 16) @@ -327,6 +329,7 @@ UNTYPED_SFIELD(sfSignerEntry, OBJECT, 11) UNTYPED_SFIELD(sfNFToken, OBJECT, 12) UNTYPED_SFIELD(sfEmitDetails, OBJECT, 13) UNTYPED_SFIELD(sfHook, OBJECT, 14) +UNTYPED_SFIELD(sfPermission, OBJECT, 15) // inner object (uncommon) UNTYPED_SFIELD(sfSigner, OBJECT, 16) @@ -377,3 +380,4 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) +UNTYPED_SFIELD(sfPermissions, ARRAY, 29) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dd3ac42325d..61479611aad 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -22,14 +22,14 @@ #endif /** - * TRANSACTION(tag, value, name, fields) + * TRANSACTION(tag, value, name, delegatable, fields) * * You must define a transactor class in the `ripple` namespace named `name`, * and include its header in `src/xrpld/app/tx/detail/applySteps.cpp`. */ /** This transaction type executes a payment. */ -TRANSACTION(ttPAYMENT, 0, Payment, ({ +TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, {sfSendMax, soeOPTIONAL, soeMPTSupported}, @@ -41,7 +41,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, ({ })) /** This transaction type creates an escrow object. */ -TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({ +TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, Delegation::delegatable, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfCondition, soeOPTIONAL}, @@ -51,7 +51,7 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({ })) /** This transaction type completes an existing escrow. */ -TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({ +TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, Delegation::delegatable, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, @@ -61,7 +61,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({ /** This transaction type adjusts various account settings. */ -TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({ +TRANSACTION(ttACCOUNT_SET, 3, AccountSet, Delegation::notDelegatable, ({ {sfEmailHash, soeOPTIONAL}, {sfWalletLocator, soeOPTIONAL}, {sfWalletSize, soeOPTIONAL}, @@ -75,20 +75,20 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({ })) /** This transaction type cancels an existing escrow. */ -TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, ({ +TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, Delegation::delegatable, ({ {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, })) /** This transaction type sets or clears an account's "regular key". */ -TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, ({ +TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, Delegation::notDelegatable, ({ {sfRegularKey, soeOPTIONAL}, })) // 6 deprecated /** This transaction type creates an offer to trade one asset for another. */ -TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({ +TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, ({ {sfTakerPays, soeREQUIRED}, {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -96,14 +96,14 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, ({ })) /** This transaction type cancels existing offers to trade one asset for another. */ -TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, ({ +TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, Delegation::delegatable, ({ {sfOfferSequence, soeREQUIRED}, })) // 9 deprecated /** This transaction type creates a new set of tickets. */ -TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({ +TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, Delegation::delegatable, ({ {sfTicketCount, soeREQUIRED}, })) @@ -112,13 +112,13 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({ /** This transaction type modifies the signer list associated with an account. */ // The SignerEntries are optional because a SignerList is deleted by // setting the SignerQuorum to zero and omitting SignerEntries. -TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, ({ +TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, Delegation::notDelegatable, ({ {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, })) /** This transaction type creates a new unidirectional XRP payment channel. */ -TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({ +TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, Delegation::delegatable, ({ {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfSettleDelay, soeREQUIRED}, @@ -128,14 +128,14 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({ })) /** This transaction type funds an existing unidirectional XRP payment channel. */ -TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({ +TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, Delegation::delegatable, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, })) /** This transaction type submits a claim against an existing unidirectional payment channel. */ -TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({ +TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, Delegation::delegatable, ({ {sfChannel, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfBalance, soeOPTIONAL}, @@ -145,7 +145,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({ })) /** This transaction type creates a new check. */ -TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ +TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, Delegation::delegatable, ({ {sfDestination, soeREQUIRED}, {sfSendMax, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -154,19 +154,19 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({ })) /** This transaction type cashes an existing check. */ -TRANSACTION(ttCHECK_CASH, 17, CheckCash, ({ +TRANSACTION(ttCHECK_CASH, 17, CheckCash, Delegation::delegatable, ({ {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL}, })) /** This transaction type cancels an existing check. */ -TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, ({ +TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, Delegation::delegatable, ({ {sfCheckID, soeREQUIRED}, })) /** This transaction type grants or revokes authorization to transfer funds. */ -TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({ +TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, Delegation::delegatable, ({ {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, {sfAuthorizeCredentials, soeOPTIONAL}, @@ -174,14 +174,14 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({ })) /** This transaction type modifies a trustline between two accounts. */ -TRANSACTION(ttTRUST_SET, 20, TrustSet, ({ +TRANSACTION(ttTRUST_SET, 20, TrustSet, Delegation::delegatable, ({ {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, {sfQualityOut, soeOPTIONAL}, })) /** This transaction type deletes an existing account. */ -TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({ +TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, Delegation::notDelegatable, ({ {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, {sfCredentialIDs, soeOPTIONAL}, @@ -190,7 +190,7 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, ({ // 22 reserved /** This transaction mints a new NFT. */ -TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, ({ +TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, Delegation::delegatable, ({ {sfNFTokenTaxon, soeREQUIRED}, {sfTransferFee, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, @@ -201,13 +201,13 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, ({ })) /** This transaction burns (i.e. destroys) an existing NFT. */ -TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, ({ +TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, Delegation::delegatable, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction creates a new offer to buy or sell an NFT. */ -TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({ +TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, Delegation::delegatable, ({ {sfNFTokenID, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfDestination, soeOPTIONAL}, @@ -216,25 +216,25 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({ })) /** This transaction cancels an existing offer to buy or sell an existing NFT. */ -TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, ({ +TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, Delegation::delegatable, ({ {sfNFTokenOffers, soeREQUIRED}, })) /** This transaction accepts an existing offer to buy or sell an existing NFT. */ -TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, ({ +TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, Delegation::delegatable, ({ {sfNFTokenBuyOffer, soeOPTIONAL}, {sfNFTokenSellOffer, soeOPTIONAL}, {sfNFTokenBrokerFee, soeOPTIONAL}, })) /** This transaction claws back issued tokens. */ -TRANSACTION(ttCLAWBACK, 30, Clawback, ({ +TRANSACTION(ttCLAWBACK, 30, Clawback, Delegation::delegatable, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, {sfHolder, soeOPTIONAL}, })) /** This transaction claws back tokens from an AMM pool. */ -TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({ +TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, Delegation::delegatable, ({ {sfHolder, soeREQUIRED}, {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, @@ -242,14 +242,14 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, ({ })) /** This transaction type creates an AMM instance */ -TRANSACTION(ttAMM_CREATE, 35, AMMCreate, ({ +TRANSACTION(ttAMM_CREATE, 35, AMMCreate, Delegation::delegatable, ({ {sfAmount, soeREQUIRED}, {sfAmount2, soeREQUIRED}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type deposits into an AMM instance */ -TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ +TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, Delegation::delegatable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -260,7 +260,7 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, ({ })) /** This transaction type withdraws from an AMM instance */ -TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({ +TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, Delegation::delegatable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfAmount, soeOPTIONAL}, @@ -270,14 +270,14 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, ({ })) /** This transaction type votes for the trading fee */ -TRANSACTION(ttAMM_VOTE, 38, AMMVote, ({ +TRANSACTION(ttAMM_VOTE, 38, AMMVote, Delegation::delegatable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfTradingFee, soeREQUIRED}, })) /** This transaction type bids for the auction slot */ -TRANSACTION(ttAMM_BID, 39, AMMBid, ({ +TRANSACTION(ttAMM_BID, 39, AMMBid, Delegation::delegatable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, {sfBidMin, soeOPTIONAL}, @@ -286,20 +286,20 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, ({ })) /** This transaction type deletes AMM in the empty state */ -TRANSACTION(ttAMM_DELETE, 40, AMMDelete, ({ +TRANSACTION(ttAMM_DELETE, 40, AMMDelete, Delegation::delegatable, ({ {sfAsset, soeREQUIRED}, {sfAsset2, soeREQUIRED}, })) /** This transactions creates a crosschain sequence number */ -TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, ({ +TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, {sfOtherChainSource, soeREQUIRED}, })) /** This transactions initiates a crosschain transaction */ -TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, ({ +TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -307,7 +307,7 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, ({ })) /** This transaction completes a crosschain transaction */ -TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, ({ +TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfXChainClaimID, soeREQUIRED}, {sfDestination, soeREQUIRED}, @@ -316,7 +316,7 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, ({ })) /** This transaction initiates a crosschain account create transaction */ -TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, ({ +TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfDestination, soeREQUIRED}, {sfAmount, soeREQUIRED}, @@ -324,7 +324,7 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, ({ })) /** This transaction adds an attestation to a claim */ -TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, ({ +TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfAttestationSignerAccount, soeREQUIRED}, @@ -340,7 +340,7 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, ({ })) /** This transaction adds an attestation to an account */ -TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, ({ +TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfAttestationSignerAccount, soeREQUIRED}, @@ -357,31 +357,31 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateA })) /** This transaction modifies a sidechain */ -TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, ({ +TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeOPTIONAL}, {sfMinAccountCreateAmount, soeOPTIONAL}, })) /** This transactions creates a sidechain */ -TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, ({ +TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, Delegation::delegatable, ({ {sfXChainBridge, soeREQUIRED}, {sfSignatureReward, soeREQUIRED}, {sfMinAccountCreateAmount, soeOPTIONAL}, })) /** This transaction type creates or updates a DID */ -TRANSACTION(ttDID_SET, 49, DIDSet, ({ +TRANSACTION(ttDID_SET, 49, DIDSet, Delegation::delegatable, ({ {sfDIDDocument, soeOPTIONAL}, {sfURI, soeOPTIONAL}, {sfData, soeOPTIONAL}, })) /** This transaction type deletes a DID */ -TRANSACTION(ttDID_DELETE, 50, DIDDelete, ({})) +TRANSACTION(ttDID_DELETE, 50, DIDDelete, Delegation::delegatable, ({})) /** This transaction type creates an Oracle instance */ -TRANSACTION(ttORACLE_SET, 51, OracleSet, ({ +TRANSACTION(ttORACLE_SET, 51, OracleSet, Delegation::delegatable, ({ {sfOracleDocumentID, soeREQUIRED}, {sfProvider, soeOPTIONAL}, {sfURI, soeOPTIONAL}, @@ -391,18 +391,18 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, ({ })) /** This transaction type deletes an Oracle instance */ -TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, ({ +TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, Delegation::delegatable, ({ {sfOracleDocumentID, soeREQUIRED}, })) /** This transaction type fixes a problem in the ledger state */ -TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, ({ +TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, Delegation::notDelegatable, ({ {sfLedgerFixType, soeREQUIRED}, {sfOwner, soeOPTIONAL}, })) /** This transaction type creates a MPTokensIssuance instance */ -TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, ({ +TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, Delegation::delegatable, ({ {sfAssetScale, soeOPTIONAL}, {sfTransferFee, soeOPTIONAL}, {sfMaximumAmount, soeOPTIONAL}, @@ -410,24 +410,24 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, ({ })) /** This transaction type destroys a MPTokensIssuance instance */ -TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, ({ +TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, Delegation::delegatable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, })) /** This transaction type sets flags on a MPTokensIssuance or MPToken instance */ -TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, ({ +TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, Delegation::delegatable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) /** This transaction type authorizes a MPToken instance */ -TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, ({ +TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, Delegation::delegatable, ({ {sfMPTokenIssuanceID, soeREQUIRED}, {sfHolder, soeOPTIONAL}, })) /** This transaction type create an Credential instance */ -TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, ({ +TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, Delegation::delegatable, ({ {sfSubject, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, @@ -435,41 +435,47 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, ({ })) /** This transaction type accept an Credential object */ -TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, ({ +TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, Delegation::delegatable, ({ {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, })) /** This transaction type delete an Credential object */ -TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, ({ +TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, Delegation::delegatable, ({ {sfSubject, soeOPTIONAL}, {sfIssuer, soeOPTIONAL}, {sfCredentialType, soeREQUIRED}, })) /** This transaction type modify a NFToken */ -TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, ({ +TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, Delegation::delegatable, ({ {sfNFTokenID, soeREQUIRED}, {sfOwner, soeOPTIONAL}, {sfURI, soeOPTIONAL}, })) /** This transaction type creates or modifies a Permissioned Domain */ -TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, ({ +TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, Delegation::delegatable, ({ {sfDomainID, soeOPTIONAL}, {sfAcceptedCredentials, soeREQUIRED}, })) /** This transaction type deletes a Permissioned Domain */ -TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, ({ +TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, Delegation::delegatable, ({ {sfDomainID, soeREQUIRED}, })) +/** This transaction type delegates authorized account specified permissions */ +TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, Delegation::notDelegatable, ({ + {sfAuthorize, soeREQUIRED}, + {sfPermissions, soeREQUIRED}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html */ -TRANSACTION(ttAMENDMENT, 100, EnableAmendment, ({ +TRANSACTION(ttAMENDMENT, 100, EnableAmendment, Delegation::notDelegatable, ({ {sfLedgerSequence, soeREQUIRED}, {sfAmendment, soeREQUIRED}, })) @@ -477,7 +483,7 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, ({ /** This system-generated transaction type is used to update the network's fee settings. For details, see: https://xrpl.org/fee-voting.html */ -TRANSACTION(ttFEE, 101, SetFee, ({ +TRANSACTION(ttFEE, 101, SetFee, Delegation::notDelegatable, ({ {sfLedgerSequence, soeOPTIONAL}, // Old version uses raw numbers {sfBaseFee, soeOPTIONAL}, @@ -494,7 +500,7 @@ TRANSACTION(ttFEE, 101, SetFee, ({ For details, see: https://xrpl.org/negative-unl.html */ -TRANSACTION(ttUNL_MODIFY, 102, UNLModify, ({ +TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, ({ {sfUNLModifyDisabling, soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 483b69a962f..bb2ffa7bb06 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -145,6 +145,7 @@ JSS(attestations); JSS(attestation_reward_account); JSS(auction_slot); // out: amm_info JSS(authorized); // out: AccountLines +JSS(authorize); // out: delegate JSS(authorized_credentials); // in: ledger_entry DepositPreauth JSS(auth_accounts); // out: amm_info JSS(auth_change); // out: AccountInfo @@ -699,7 +700,7 @@ JSS(write_load); // out: GetCounts #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, fields) JSS(name); +#define TRANSACTION(tag, value, name, delegatable, fields) JSS(name); #include diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index 93e30f24bea..b3d1b812b59 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -107,6 +107,7 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcSRC_ACT_MALFORMED, "srcActMalformed", "Source account is malformed.", 400}, {rpcSRC_ACT_MISSING, "srcActMissing", "Source account not provided.", 400}, {rpcSRC_ACT_NOT_FOUND, "srcActNotFound", "Source account not found.", 404}, + {rpcDELEGATE_ACT_NOT_FOUND, "delegateActNotFound", "Delegate account not found.", 404}, {rpcSRC_CUR_MALFORMED, "srcCurMalformed", "Source currency is malformed.", 400}, {rpcSRC_ISR_MALFORMED, "srcIsrMalformed", "Source issuer is malformed.", 400}, {rpcSTREAM_MALFORMED, "malformedStream", "Stream malformed.", 400}, diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index fe8f0c4778d..8256c7a77c6 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -94,6 +94,7 @@ enum class LedgerNameSpace : std::uint16_t { MPTOKEN = 't', CREDENTIAL = 'D', PERMISSIONED_DOMAIN = 'm', + DELEGATE = 'E', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -452,6 +453,14 @@ amm(uint256 const& id) noexcept return {ltAMM, id}; } +Keylet +delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept +{ + return { + ltDELEGATE, + indexHash(LedgerNameSpace::DELEGATE, account, authorizedAccount)}; +} + Keylet bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType) { diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 87abcc23516..ecfca9743dd 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -154,6 +154,10 @@ InnerObjectFormats::InnerObjectFormats() {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, }); + + add(sfPermission.jsonName.c_str(), + sfPermission.getCode(), + {{sfPermissionValue, soeREQUIRED}}); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp new file mode 100644 index 00000000000..dbe5325a4e3 --- /dev/null +++ b/src/libxrpl/protocol/Permissions.cpp @@ -0,0 +1,148 @@ +//------------------------------------------------------------------------------ +/* + 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 { + +Permission::Permission() +{ + delegatableTx_ = { +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegatable, fields) {value, delegatable}, + +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + }; + + granularPermissionMap_ = { +#pragma push_macro("PERMISSION") +#undef PERMISSION + +#define PERMISSION(type, txType, value) {#type, type}, + +#include + +#undef PERMISSION +#pragma pop_macro("PERMISSION") + }; + + granularNameMap_ = { +#pragma push_macro("PERMISSION") +#undef PERMISSION + +#define PERMISSION(type, txType, value) {type, #type}, + +#include + +#undef PERMISSION +#pragma pop_macro("PERMISSION") + }; + + granularTxTypeMap_ = { +#pragma push_macro("PERMISSION") +#undef PERMISSION + +#define PERMISSION(type, txType, value) {type, txType}, + +#include + +#undef PERMISSION +#pragma pop_macro("PERMISSION") + }; + + for ([[maybe_unused]] auto const& permission : granularPermissionMap_) + XRPL_ASSERT( + permission.second > UINT16_MAX, + "ripple::Permission::granularPermissionMap_ : granular permission " + "value must not exceed the maximum uint16_t value."); +} + +Permission const& +Permission::getInstance() +{ + static Permission const instance; + return instance; +} + +std::optional +Permission::getGranularValue(std::string const& name) const +{ + auto const it = granularPermissionMap_.find(name); + if (it != granularPermissionMap_.end()) + return static_cast(it->second); + + return std::nullopt; +} + +std::optional +Permission::getGranularName(GranularPermissionType const& value) const +{ + auto const it = granularNameMap_.find(value); + if (it != granularNameMap_.end()) + return it->second; + + return std::nullopt; +} + +std::optional +Permission::getGranularTxType(GranularPermissionType const& gpType) const +{ + auto const it = granularTxTypeMap_.find(gpType); + if (it != granularTxTypeMap_.end()) + return it->second; + + return std::nullopt; +} + +bool +Permission::isDelegatable(std::uint32_t const& permissionValue) const +{ + auto const granularPermission = + getGranularName(static_cast(permissionValue)); + if (granularPermission) + // granular permissions are always allowed to be delegated + return true; + + auto const it = delegatableTx_.find(permissionValue - 1); + if (it != delegatableTx_.end() && it->second == Delegation::notDelegatable) + return false; + + return true; +} + +uint32_t +Permission::txToPermissionType(TxType const& type) const +{ + return static_cast(type) + 1; +} + +TxType +Permission::permissionToTxType(uint32_t const& value) const +{ + return static_cast(value - 1); +} + +} // namespace ripple \ No newline at end of file diff --git a/src/libxrpl/protocol/STInteger.cpp b/src/libxrpl/protocol/STInteger.cpp index bc5b7e855e2..a90e21491c0 100644 --- a/src/libxrpl/protocol/STInteger.cpp +++ b/src/libxrpl/protocol/STInteger.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -177,6 +178,27 @@ template <> Json::Value STUInt32::getJson(JsonOptions) const { + if (getFName() == sfPermissionValue) + { + auto const permissionValue = + static_cast(value_); + auto const granular = + Permission::getInstance().getGranularName(permissionValue); + + if (granular) + { + return *granular; + } + else + { + auto const txType = + Permission::getInstance().permissionToTxType(value_); + auto item = TxFormats::getInstance().findByType(txType); + if (item != nullptr) + return item->getName(); + } + } + return value_; } diff --git a/src/libxrpl/protocol/STParsedJSON.cpp b/src/libxrpl/protocol/STParsedJSON.cpp index 0488189a667..e7568c6818b 100644 --- a/src/libxrpl/protocol/STParsedJSON.cpp +++ b/src/libxrpl/protocol/STParsedJSON.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -373,10 +374,35 @@ parseLeaf( { if (value.isString()) { - ret = detail::make_stvar( - field, - beast::lexicalCastThrow( - value.asString())); + if (field == sfPermissionValue) + { + std::string const strValue = value.asString(); + auto const granularPermission = + Permission::getInstance().getGranularValue( + strValue); + if (granularPermission) + { + ret = detail::make_stvar( + field, *granularPermission); + } + else + { + auto const& txType = + TxFormats::getInstance().findTypeByName( + strValue); + ret = detail::make_stvar( + field, + Permission::getInstance().txToPermissionType( + txType)); + } + } + else + { + ret = detail::make_stvar( + field, + beast::lexicalCastThrow( + value.asString())); + } } else if (value.isInt()) { diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index b2dd3a656f5..a23475553df 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -46,6 +46,7 @@ TxFormats::TxFormats() {sfTxnSignature, soeOPTIONAL}, {sfSigners, soeOPTIONAL}, // submit_multisigned {sfNetworkID, soeOPTIONAL}, + {sfDelegate, soeOPTIONAL}, }; #pragma push_macro("UNWRAP") @@ -54,7 +55,7 @@ TxFormats::TxFormats() #undef TRANSACTION #define UNWRAP(...) __VA_ARGS__ -#define TRANSACTION(tag, value, name, fields) \ +#define TRANSACTION(tag, value, name, delegatable, fields) \ add(jss::name, tag, UNWRAP fields, commonFields); #include diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp new file mode 100644 index 00000000000..c8415a558aa --- /dev/null +++ b/src/test/app/Delegate_test.cpp @@ -0,0 +1,1437 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include + +namespace ripple { +namespace test { +class Delegate_test : public beast::unit_test::suite +{ + void + testFeatureDisabled() + { + testcase("test featurePermissionDelegation not enabled"); + using namespace jtx; + + Env env{*this, supported_amendments() - featurePermissionDelegation}; + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(1000000), gw, alice, bob); + env.close(); + + // can not set Delegate when feature disabled + env(delegate::set(gw, alice, {"Payment"}), ter(temDISABLED)); + + // can not send delegating transaction when feature disabled + env(pay(alice, bob, XRP(100)), delegate::as(bob), ter(temDISABLED)); + } + + void + testDelegateSet() + { + testcase("test valid request creating, updating, deleting permissions"); + using namespace jtx; + + Env env(*this); + Account gw{"gateway"}; + Account alice{"alice"}; + env.fund(XRP(100000), gw, alice); + env.close(); + + // delegating an empty permission list when the delegate ledger object + // does not exist will not create the ledger object + env(delegate::set(gw, alice, std::vector{})); + env.close(); + auto const entry = delegate::entry(env, gw, alice); + BEAST_EXPECT(entry[jss::result][jss::error] == "entryNotFound"); + + auto const permissions = std::vector{ + "Payment", + "EscrowCreate", + "EscrowFinish", + "TrustlineAuthorize", + "CheckCreate"}; + env(delegate::set(gw, alice, permissions)); + env.close(); + + // this lambda function is used to compare the json value of ledger + // entry response with the given vector of permissions. + auto comparePermissions = + [&](Json::Value const& jle, + std::vector const& permissions, + Account const& account, + Account const& authorize) { + BEAST_EXPECT( + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node)); + BEAST_EXPECT( + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Delegate); + BEAST_EXPECT( + jle[jss::result][jss::node][jss::Account] == + account.human()); + BEAST_EXPECT( + jle[jss::result][jss::node][sfAuthorize.jsonName] == + authorize.human()); + + auto const& jPermissions = + jle[jss::result][jss::node][sfPermissions.jsonName]; + unsigned i = 0; + for (auto const& permission : permissions) + { + BEAST_EXPECT( + jPermissions[i][sfPermission.jsonName] + [sfPermissionValue.jsonName] == permission); + i++; + } + }; + + // get ledger entry with valid parameter + comparePermissions( + delegate::entry(env, gw, alice), permissions, gw, alice); + + // gw updates permission + auto const newPermissions = std::vector{ + "Payment", "AMMCreate", "AMMDeposit", "AMMWithdraw"}; + env(delegate::set(gw, alice, newPermissions)); + env.close(); + + // get ledger entry again, permissions should be updated to + // newPermissions + comparePermissions( + delegate::entry(env, gw, alice), newPermissions, gw, alice); + + // gw deletes all permissions delegated to alice, this will delete + // the + // ledger entry + env(delegate::set(gw, alice, {})); + env.close(); + auto const jle = delegate::entry(env, gw, alice); + BEAST_EXPECT(jle[jss::result][jss::error] == "entryNotFound"); + + // alice can delegate permissions to gw as well + env(delegate::set(alice, gw, permissions)); + env.close(); + comparePermissions( + delegate::entry(env, alice, gw), permissions, alice, gw); + auto const response = delegate::entry(env, gw, alice); + // alice has not been granted any permissions by gw + BEAST_EXPECT(response[jss::result][jss::error] == "entryNotFound"); + } + + void + testInvalidRequest() + { + testcase("test invalid DelegateSet"); + using namespace jtx; + + Env env(*this); + Account gw{"gateway"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), gw, alice, bob); + env.close(); + + // when permissions size exceeds the limit 10, should return + // temARRAY_TOO_LARGE + { + env(delegate::set( + gw, + alice, + {"Payment", + "EscrowCreate", + "EscrowFinish", + "EscrowCancel", + "CheckCreate", + "CheckCash", + "CheckCancel", + "DepositPreauth", + "TrustSet", + "NFTokenMint", + "NFTokenBurn"}), + ter(temARRAY_TOO_LARGE)); + } + + // alice can not authorize herself + { + env(delegate::set(alice, alice, {"Payment"}), ter(temMALFORMED)); + } + + // bad fee + { + Json::Value jv; + jv[jss::TransactionType] = jss::DelegateSet; + jv[jss::Account] = gw.human(); + jv[sfAuthorize.jsonName] = alice.human(); + Json::Value permissionsJson(Json::arrayValue); + Json::Value permissionValue; + permissionValue[sfPermissionValue.jsonName] = "Payment"; + Json::Value permissionObj; + permissionObj[sfPermission.jsonName] = permissionValue; + permissionsJson.append(permissionObj); + jv[sfPermissions.jsonName] = permissionsJson; + jv[sfFee.jsonName] = -1; + env(jv, ter(temBAD_FEE)); + } + + // when provided permissions contains duplicate values, should return + // temMALFORMED + { + env(delegate::set( + gw, + alice, + {"Payment", + "EscrowCreate", + "EscrowFinish", + "TrustlineAuthorize", + "CheckCreate", + "TrustlineAuthorize"}), + ter(temMALFORMED)); + } + + // when authorizing account which does not exist, should return + // terNO_ACCOUNT + { + env(delegate::set(gw, Account("unknown"), {"Payment"}), + ter(terNO_ACCOUNT)); + } + + // for security reasons, AccountSet, SetRegularKey, SignerListSet, + // AccountDelete, DelegateSet are prohibited to be delegated to + // other accounts. + { + env(delegate::set(gw, alice, {"SetRegularKey"}), + ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"AccountSet"}), + ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"SignerListSet"}), + ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"DelegateSet"}), + ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"SetRegularKey"}), + ter(tecNO_PERMISSION)); + } + } + + void + testReserve() + { + testcase("test reserve"); + using namespace jtx; + + // test reserve for DelegateSet + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + + env.fund(drops(env.current()->fees().accountReserve(0)), alice); + env.fund( + drops(env.current()->fees().accountReserve(1)), bob, carol); + env.close(); + + // alice does not have enough reserve to create Delegate + env(delegate::set(alice, bob, {"Payment"}), + ter(tecINSUFFICIENT_RESERVE)); + + // bob has enough reserve + env(delegate::set(bob, alice, {"Payment"})); + env.close(); + + // now bob create another Delegate, he does not have + // enough reserve + env(delegate::set(bob, carol, {"Payment"}), + ter(tecINSUFFICIENT_RESERVE)); + } + + // test reserve when sending transaction on behalf of other account + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + + env.fund(drops(env.current()->fees().accountReserve(1)), alice); + env.fund(drops(env.current()->fees().accountReserve(2)), bob); + env.close(); + + // alice gives bob permission + env(delegate::set(alice, bob, {"DIDSet", "DIDDelete"})); + env.close(); + + // bob set DID on behalf of alice, but alice does not have enough + // reserve + env(did::set(alice), + did::uri("uri"), + delegate::as(bob), + ter(tecINSUFFICIENT_RESERVE)); + + // bob can set DID for himself because he has enough reserve + env(did::set(bob), did::uri("uri")); + env.close(); + } + } + + void + testFee() + { + testcase("test fee"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(10000), alice, carol); + env.fund(XRP(1000), bob); + env.close(); + + { + // Fee should be checked before permission check, + // otherwise tecNO_PERMISSION returned when permission check fails + // could cause context reset to pay fee because it is tec error + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + + env(pay(alice, carol, XRP(100)), + fee(XRP(2000)), + delegate::as(bob), + ter(terINSUF_FEE_B)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance); + } + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + { + // Delegate pays the fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + + auto const sendAmt = XRP(100); + auto const feeAmt = XRP(10); + env(pay(alice, carol, sendAmt), fee(feeAmt), delegate::as(bob)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); + BEAST_EXPECT(env.balance(bob) == bobBalance - feeAmt); + BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt); + } + + { + // insufficient balance to pay fee + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + + env(pay(alice, carol, XRP(100)), + fee(XRP(2000)), + delegate::as(bob), + ter(terINSUF_FEE_B)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance); + } + + { + // fee is paid by Delegate + // on context reset (tec error) + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + auto const feeAmt = XRP(10); + + env(pay(alice, carol, XRP(20000)), + fee(feeAmt), + delegate::as(bob), + ter(tecUNFUNDED_PAYMENT)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance - feeAmt); + BEAST_EXPECT(env.balance(carol) == carolBalance); + } + } + + void + testSequence() + { + testcase("test sequence"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto aliceSeq = env.seq(alice); + auto bobSeq = env.seq(bob); + env(delegate::set(alice, bob, {"Payment"})); + env(delegate::set(bob, alice, {"Payment"})); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + aliceSeq = env.seq(alice); + bobSeq = env.seq(bob); + + for (auto i = 0; i < 20; ++i) + { + // bob is the delegated account, his sequence won't increment + env(pay(alice, carol, XRP(10)), fee(XRP(10)), delegate::as(bob)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.seq(bob) == bobSeq); + aliceSeq = env.seq(alice); + + // bob sends payment for himself, his sequence will increment + env(pay(bob, carol, XRP(10)), fee(XRP(10))); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + bobSeq = env.seq(bob); + + // alice is the delegated account, her sequence won't increment + env(pay(bob, carol, XRP(10)), fee(XRP(10)), delegate::as(alice)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + bobSeq = env.seq(bob); + + // alice sends payment for herself, her sequence will increment + env(pay(alice, carol, XRP(10)), fee(XRP(10))); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.seq(bob) == bobSeq); + aliceSeq = env.seq(alice); + } + } + + void + testAccountDelete() + { + testcase("test deleting account"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + BEAST_EXPECT( + env.closed()->exists(keylet::delegate(alice.id(), bob.id()))); + + for (std::uint32_t i = 0; i < 256; ++i) + env.close(); + + auto const aliceBalance = env.balance(alice); + auto const bobBalance = env.balance(bob); + + // alice deletes account, this will remove the Delegate object + auto const deleteFee = drops(env.current()->fees().increment); + env(acctdelete(alice, bob), fee(deleteFee)); + env.close(); + + BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id()))); + BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(alice.id()))); + BEAST_EXPECT(env.balance(bob) == bobBalance + aliceBalance - deleteFee); + + BEAST_EXPECT( + !env.closed()->exists(keylet::delegate(alice.id(), bob.id()))); + } + + void + testDelegateTransaction() + { + testcase("test delegate transaction"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + + XRPAmount const baseFee{env.current()->fees().base}; + + // use different initial amount to distinguish the source balance + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(30000), carol); + env.close(); + + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + auto carolBalance = env.balance(carol, XRP); + + // can not send transaction on one's own behalf + env(pay(alice, bob, XRP(50)), delegate::as(alice), ter(temBAD_SIGNER)); + env.require(balance(alice, aliceBalance)); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + env.require(balance(alice, aliceBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + + // bob pays 50 XRP to carol on behalf of alice + env(pay(alice, carol, XRP(50)), delegate::as(bob)); + env.close(); + env.require(balance(alice, aliceBalance - XRP(50))); + env.require(balance(carol, carolBalance + XRP(50))); + // bob pays the fee + env.require(balance(bob, bobBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + bobBalance = env.balance(bob, XRP); + carolBalance = env.balance(carol, XRP); + + // bob pays 50 XRP to bob self on behalf of alice + env(pay(alice, bob, XRP(50)), delegate::as(bob)); + env.close(); + env.require(balance(alice, aliceBalance - XRP(50))); + env.require(balance(bob, bobBalance + XRP(50) - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + bobBalance = env.balance(bob, XRP); + + // bob pay 50 XRP to alice herself on behalf of alice + env(pay(alice, alice, XRP(50)), delegate::as(bob), ter(temREDUNDANT)); + env.close(); + + // bob does not have permission to create check + env(check::create(alice, bob, XRP(10)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // carol does not have permission to create check + env(check::create(alice, bob, XRP(10)), + delegate::as(carol), + ter(tecNO_PERMISSION)); + } + + void + testPaymentGranular() + { + testcase("test payment granular"); + using namespace jtx; + + // test PaymentMint and PaymentBurn + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account gw{"gateway"}; + Account gw2{"gateway2"}; + auto const USD = gw["USD"]; + auto const EUR = gw2["EUR"]; + + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(40000), gw, gw2); + env.trust(USD(200), alice); + env.trust(EUR(400), gw); + env.close(); + + XRPAmount const baseFee{env.current()->fees().base}; + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + auto gwBalance = env.balance(gw, XRP); + auto gw2Balance = env.balance(gw2, XRP); + + // delegate ledger object is not created yet + env(pay(gw, alice, USD(50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // gw gives bob burn permission + env(delegate::set(gw, bob, {"PaymentBurn"})); + env.close(); + env.require(balance(gw, gwBalance - drops(baseFee))); + gwBalance = env.balance(gw, XRP); + + // bob sends a payment transaction on behalf of gw + env(pay(gw, alice, USD(50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // gw gives bob mint permission, alice gives bob burn permission + env(delegate::set(gw, bob, {"PaymentMint"})); + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + env.require(balance(alice, aliceBalance - drops(baseFee))); + env.require(balance(gw, gwBalance - drops(baseFee))); + aliceBalance = env.balance(alice, XRP); + gwBalance = env.balance(gw, XRP); + + // can not send XRP + env(pay(gw, alice, XRP(50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // mint 50 USD + env(pay(gw, alice, USD(50)), delegate::as(bob)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, gwBalance)); + env.require(balance(gw, alice["USD"](-50))); + env.require(balance(alice, USD(50))); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + bobBalance = env.balance(bob, XRP); + + // burn 30 USD + env(pay(alice, gw, USD(30)), delegate::as(bob)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, gwBalance)); + env.require(balance(gw, alice["USD"](-20))); + env.require(balance(alice, USD(20))); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + bobBalance = env.balance(bob, XRP); + + // bob has both mint and burn permissions + env(delegate::set(gw, bob, {"PaymentMint", "PaymentBurn"})); + env.close(); + env.require(balance(gw, gwBalance - drops(baseFee))); + gwBalance = env.balance(gw, XRP); + + // mint 100 USD for gw + env(pay(gw, alice, USD(100)), delegate::as(bob)); + env.close(); + env.require(balance(gw, alice["USD"](-120))); + env.require(balance(alice, USD(120))); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // gw2 pays gw 200 EUR + env(pay(gw2, gw, EUR(200))); + env.close(); + env.require(balance(gw2, gw2Balance - drops(baseFee))); + gw2Balance = env.balance(gw2, XRP); + env.require(balance(gw2, gw["EUR"](-200))); + env.require(balance(gw, EUR(200))); + + // burn 100 EUR for gw + env(pay(gw, gw2, EUR(100)), delegate::as(bob)); + env.close(); + env.require(balance(gw2, gw["EUR"](-100))); + env.require(balance(gw, EUR(100))); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, gwBalance)); + env.require(balance(gw2, gw2Balance)); + env.require(balance(alice, aliceBalance)); + } + + // test PaymentMint won't affect Payment transaction level delegation. + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account gw{"gateway"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice); + env.fund(XRP(20000), bob); + env.fund(XRP(40000), gw); + env.trust(USD(200), alice); + env.close(); + + XRPAmount const baseFee{env.current()->fees().base}; + + auto aliceBalance = env.balance(alice, XRP); + auto bobBalance = env.balance(bob, XRP); + auto gwBalance = env.balance(gw, XRP); + + // gw gives bob PaymentBurn permission + env(delegate::set(gw, bob, {"PaymentBurn"})); + env.close(); + env.require(balance(gw, gwBalance - drops(baseFee))); + gwBalance = env.balance(gw, XRP); + + // bob can not mint on behalf of gw because he only has burn + // permission + env(pay(gw, alice, USD(50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + bobBalance = env.balance(bob, XRP); + + // gw gives bob Payment permission as well + env(delegate::set(gw, bob, {"PaymentBurn", "Payment"})); + env.close(); + env.require(balance(gw, gwBalance - drops(baseFee))); + gwBalance = env.balance(gw, XRP); + + // bob now can mint on behalf of gw + env(pay(gw, alice, USD(50)), delegate::as(bob)); + env.close(); + env.require(balance(bob, bobBalance - drops(baseFee))); + env.require(balance(gw, gwBalance)); + env.require(balance(alice, aliceBalance)); + env.require(balance(gw, alice["USD"](-50))); + env.require(balance(alice, USD(50))); + BEAST_EXPECT(env.balance(bob, USD) == USD(0)); + } + } + + void + testTrustSetGranular() + { + testcase("test TrustSet granular permissions"); + using namespace jtx; + + // test TrustlineUnfreeze, TrustlineFreeze and TrustlineAuthorize + { + Env env(*this); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + env(fset(gw, asfRequireAuth)); + env.close(); + + env(delegate::set(alice, bob, {"TrustlineUnfreeze"})); + env.close(); + // bob can not create trustline on behalf of alice because he only + // has unfreeze permission + env(trust(alice, gw["USD"](50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + + // alice creates trustline by herself + env(trust(alice, gw["USD"](50))); + env.close(); + + // gw gives bob unfreeze permission + env(delegate::set(gw, bob, {"TrustlineUnfreeze"})); + env.close(); + + // unsupported flags + env(trust(alice, gw["USD"](50), tfSetNoRipple), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(trust(alice, gw["USD"](50), tfClearNoRipple), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + + // supported flags with wrong permission + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(trust(gw, gw["USD"](0), alice, tfSetFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + env(trust(gw, gw["USD"](0), alice, tfClearFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + // although trustline authorize is granted, bob can not change the + // limit number + env(trust(gw, gw["USD"](50), alice, tfSetfAuth), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env.close(); + + // supported flags with correct permission + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob)); + env.close(); + env(delegate::set( + gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"})); + env.close(); + env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob)); + env.close(); + env(delegate::set( + gw, bob, {"TrustlineAuthorize", "TrustlineUnfreeze"})); + env.close(); + env(trust(gw, gw["USD"](0), alice, tfClearFreeze), + delegate::as(bob)); + env.close(); + // but bob can not freeze trustline because he no longer has freeze + // permission + env(trust(gw, gw["USD"](0), alice, tfSetFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // cannot update LimitAmount with granular permission, both high and + // low account + env(trust(alice, gw["USD"](100)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(trust(gw, alice["USD"](100)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // can not set QualityIn or QualityOut + auto tx = trust(alice, gw["USD"](50)); + tx["QualityIn"] = "1000"; + env(tx, delegate::as(bob), ter(tecNO_PERMISSION)); + auto tx2 = trust(alice, gw["USD"](50)); + tx2["QualityOut"] = "1000"; + env(tx2, delegate::as(bob), ter(tecNO_PERMISSION)); + auto tx3 = trust(gw, alice["USD"](50)); + tx3["QualityIn"] = "1000"; + env(tx3, delegate::as(bob), ter(tecNO_PERMISSION)); + auto tx4 = trust(gw, alice["USD"](50)); + tx4["QualityOut"] = "1000"; + env(tx4, delegate::as(bob), ter(tecNO_PERMISSION)); + + // granting TrustSet can make it work + env(delegate::set(gw, bob, {"TrustSet"})); + env.close(); + auto tx5 = trust(gw, alice["USD"](50)); + tx5["QualityOut"] = "1000"; + env(tx5, delegate::as(bob)); + auto tx6 = trust(alice, gw["USD"](50)); + tx6["QualityOut"] = "1000"; + env(tx6, delegate::as(bob), ter(tecNO_PERMISSION)); + env(delegate::set(alice, bob, {"TrustSet"})); + env.close(); + env(tx6, delegate::as(bob)); + } + + // test mix of transaction level delegation and granular delegation + { + Env env(*this); + Account gw{"gw"}; + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + env(fset(gw, asfRequireAuth)); + env.close(); + + // bob does not have permission + env(trust(alice, gw["USD"](50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(delegate::set( + alice, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer"})); + env.close(); + // bob still does not have permission + env(trust(alice, gw["USD"](50)), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // add TrustSet permission and some unrelated permission + env(delegate::set( + alice, + bob, + {"TrustlineUnfreeze", + "NFTokenCreateOffer", + "TrustSet", + "AccountTransferRateSet"})); + env.close(); + env(trust(alice, gw["USD"](50)), delegate::as(bob)); + env.close(); + + env(delegate::set( + gw, + bob, + {"TrustlineUnfreeze", + "NFTokenCreateOffer", + "TrustSet", + "AccountTransferRateSet"})); + env.close(); + + // since bob has TrustSet permission, he does not need + // TrustlineFreeze granular permission to freeze the trustline + env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob)); + env(trust(gw, gw["USD"](0), alice, tfClearFreeze), + delegate::as(bob)); + // bob can perform all the operations regarding TrustSet + env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob)); + env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze), + delegate::as(bob)); + env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze), + delegate::as(bob)); + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob)); + env(trust(alice, gw["USD"](50), tfSetNoRipple), delegate::as(bob)); + env(trust(alice, gw["USD"](50), tfClearNoRipple), + delegate::as(bob)); + } + } + + void + testAccountSetGranular() + { + testcase("test AccountSet granular permissions"); + using namespace jtx; + + // test AccountDomainSet, AccountEmailHashSet, + // AccountMessageKeySet,AccountTransferRateSet, and AccountTickSizeSet + // granular permissions + { + Env env(*this); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // alice gives bob some random permission, which is not related to + // the AccountSet transaction + env(delegate::set(alice, bob, {"TrustlineUnfreeze"})); + env.close(); + + // bob does not have permission to set domain + // on behalf of alice + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain.fieldName] = strHex(domain); + jt[sfDelegate.fieldName] = bob.human(); + jt[sfFlags.fieldName] = tfFullyCanonicalSig; + + // add granular permission related to AccountSet but is not the + // correct permission for domain set + env(delegate::set( + alice, bob, {"TrustlineUnfreeze", "AccountEmailHashSet"})); + env.close(); + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountDomainSet to bob + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + // bob set account domain on behalf of alice + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // bob can reset domain + jt[sfDomain.fieldName] = ""; + env(jt); + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain)); + + // if flag is not equal to tfFullyCanonicalSig, which means bob + // is trying to set the flag at the same time, it will fail + std::string const failDomain = "fail_domain_update"; + jt[sfFlags.fieldName] = tfRequireAuth; + jt[sfDomain.fieldName] = strHex(failDomain); + env(jt, ter(tecNO_PERMISSION)); + // reset flag number + jt[sfFlags.fieldName] = tfFullyCanonicalSig; + + // bob tries to update domain and set email hash, + // but he does not have permission to set email hash + jt[sfDomain.fieldName] = strHex(domain); + std::string const mh("5F31A79367DC3137FADA860C05742EE6"); + jt[sfEmailHash.fieldName] = mh; + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountEmailHashSet to bob + env(delegate::set( + alice, bob, {"AccountDomainSet", "AccountEmailHashSet"})); + env.close(); + env(jt); + BEAST_EXPECT(to_string((*env.le(alice))[sfEmailHash]) == mh); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // bob does not have permission to set message key for alice + auto const rkp = randomKeyPair(KeyType::ed25519); + jt[sfMessageKey.fieldName] = strHex(rkp.first.slice()); + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountMessageKeySet to bob + env(delegate::set( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + // bob can set message key for alice + env(jt); + BEAST_EXPECT( + strHex((*env.le(alice))[sfMessageKey]) == + strHex(rkp.first.slice())); + jt[sfMessageKey.fieldName] = ""; + env(jt); + BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfMessageKey)); + + // bob does not have permission to set transfer rate for alice + env(rate(alice, 2.0), delegate::as(bob), ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountTransferRateSet to bob + env(delegate::set( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet", + "AccountTransferRateSet"})); + env.close(); + auto jtRate = rate(alice, 2.0); + jtRate[sfDelegate.fieldName] = bob.human(); + jtRate[sfFlags.fieldName] = tfFullyCanonicalSig; + env(jtRate, delegate::as(bob)); + BEAST_EXPECT((*env.le(alice))[sfTransferRate] == 2000000000); + + // bob does not have permission to set ticksize for alice + jt[sfTickSize.fieldName] = 8; + env(jt, ter(tecNO_PERMISSION)); + + // alice give granular permission of AccountTickSizeSet to bob + env(delegate::set( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet", + "AccountTransferRateSet", + "AccountTickSizeSet"})); + env.close(); + env(jt); + BEAST_EXPECT((*env.le(alice))[sfTickSize] == 8); + + // can not set asfRequireAuth flag for alice + env(fset(alice, asfRequireAuth), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // reset Delegate will delete the Delegate + // object + env(delegate::set(alice, bob, {})); + // bib still does not have permission to set asfRequireAuth for + // alice + env(fset(alice, asfRequireAuth), + delegate::as(bob), + ter(tecNO_PERMISSION)); + // alice can set for herself + env(fset(alice, asfRequireAuth)); + env.require(flags(alice, asfRequireAuth)); + env.close(); + + // can not update tick size because bob no longer has permission + jt[sfTickSize.fieldName] = 7; + env(jt, ter(tecNO_PERMISSION)); + + env(delegate::set( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + // bob does not have permission to set wallet locater for alice + std::string const locator = + "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF" + "05"; + auto jt2 = noop(alice); + jt2[sfDomain.fieldName] = strHex(domain); + jt2[sfDelegate.fieldName] = bob.human(); + jt2[sfWalletLocator.fieldName] = locator; + jt2[sfFlags.fieldName] = tfFullyCanonicalSig; + env(jt2, ter(tecNO_PERMISSION)); + } + + // can not set AccountSet flags on behalf of other account + { + Env env(*this); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto testSetClearFlag = [&](std::uint32_t flag) { + // bob can not set flag on behalf of alice + env(fset(alice, flag), + delegate::as(bob), + ter(tecNO_PERMISSION)); + // alice set by herself + env(fset(alice, flag)); + env.close(); + env.require(flags(alice, flag)); + // bob can not clear on behalf of alice + env(fclear(alice, flag), + delegate::as(bob), + ter(tecNO_PERMISSION)); + }; + + // testSetClearFlag(asfNoFreeze); + testSetClearFlag(asfRequireAuth); + testSetClearFlag(asfAllowTrustLineClawback); + + // alice gives some granular permissions to bob + env(delegate::set( + alice, + bob, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + testSetClearFlag(asfDefaultRipple); + testSetClearFlag(asfDepositAuth); + testSetClearFlag(asfDisallowIncomingCheck); + testSetClearFlag(asfDisallowIncomingNFTokenOffer); + testSetClearFlag(asfDisallowIncomingPayChan); + testSetClearFlag(asfDisallowIncomingTrustline); + testSetClearFlag(asfDisallowXRP); + testSetClearFlag(asfRequireDest); + testSetClearFlag(asfGlobalFreeze); + + // bob can not set asfAccountTxnID on behalf of alice + env(fset(alice, asfAccountTxnID), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(fset(alice, asfAccountTxnID)); + env.close(); + BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID)); + env(fclear(alice, asfAccountTxnID), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // bob can not set asfAuthorizedNFTokenMinter on behalf of alice + Json::Value jt = fset(alice, asfAuthorizedNFTokenMinter); + jt[sfDelegate.fieldName] = bob.human(); + jt[sfNFTokenMinter.fieldName] = bob.human(); + env(jt, ter(tecNO_PERMISSION)); + + // bob gives alice some permissions + env(delegate::set( + bob, + alice, + {"AccountDomainSet", + "AccountEmailHashSet", + "AccountMessageKeySet"})); + env.close(); + + // since we can not set asfNoFreeze if asfAllowTrustLineClawback is + // set, which can not be clear either. Test alice set asfNoFreeze on + // behalf of bob. + env(fset(alice, asfNoFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + env(fset(bob, asfNoFreeze)); + env.close(); + env.require(flags(bob, asfNoFreeze)); + // alice can not clear on behalf of bob + env(fclear(alice, asfNoFreeze), + delegate::as(bob), + ter(tecNO_PERMISSION)); + + // bob can not set asfDisableMaster on behalf of alice + Account const bobKey{"bobKey", KeyType::secp256k1}; + env(regkey(bob, bobKey)); + env.close(); + env(fset(alice, asfDisableMaster), + delegate::as(bob), + sig(bob), + ter(tecNO_PERMISSION)); + } + } + + void + testMPTokenIssuanceSetGranular() + { + testcase("test MPTokenIssuanceSet granular"); + using namespace jtx; + + // test MPTokenIssuanceUnlock and MPTokenIssuanceLock permissions + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // delegate ledger object is not created yet + mpt.set( + {.account = alice, + .flags = tfMPTLock, + .delegate = bob, + .err = tecNO_PERMISSION}); + + // alice gives granular permission to bob of MPTokenIssuanceUnlock + env(delegate::set(alice, bob, {"MPTokenIssuanceUnlock"})); + env.close(); + // bob does not have lock permission + mpt.set( + {.account = alice, + .flags = tfMPTLock, + .delegate = bob, + .err = tecNO_PERMISSION}); + // bob now has lock permission, but does not have unlock permission + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); + mpt.set( + {.account = alice, + .flags = tfMPTUnlock, + .delegate = bob, + .err = tecNO_PERMISSION}); + + // now bob can lock and unlock + env(delegate::set( + alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"})); + env.close(); + mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); + mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); + env.close(); + } + + // test mix of granular and transaction level permission + { + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + env.close(); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob of MPTokenIssuanceLock + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); + // bob does not have unlock permission + mpt.set( + {.account = alice, + .flags = tfMPTUnlock, + .delegate = bob, + .err = tecNO_PERMISSION}); + + // alice gives bob some unrelated permission with + // MPTokenIssuanceLock + env(delegate::set( + alice, + bob, + {"NFTokenMint", "MPTokenIssuanceLock", "NFTokenBurn"})); + env.close(); + // bob can not unlock + mpt.set( + {.account = alice, + .flags = tfMPTUnlock, + .delegate = bob, + .err = tecNO_PERMISSION}); + + // alice add MPTokenIssuanceSet to permissions + env(delegate::set( + alice, + bob, + {"NFTokenMint", + "MPTokenIssuanceLock", + "NFTokenBurn", + "MPTokenIssuanceSet"})); + mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); + // alice can lock by herself + mpt.set({.account = alice, .flags = tfMPTLock}); + mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); + mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); + } + } + + void + testSingleSign() + { + testcase("test single sign"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(100000), alice, bob, carol); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + + env(pay(alice, carol, XRP(100)), + fee(XRP(10)), + delegate::as(bob), + sig(bob)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); + BEAST_EXPECT(env.balance(bob) == bobBalance - XRP(10)); + BEAST_EXPECT(env.balance(carol) == carolBalance + XRP(100)); + } + + void + testSingleSignBadSecret() + { + testcase("test single sign with bad secret"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + env.fund(XRP(100000), alice, bob, carol); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + + env(pay(alice, carol, XRP(100)), + fee(XRP(10)), + delegate::as(bob), + sig(alice), + ter(tefBAD_AUTH)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance); + } + + void + testMultiSign() + { + testcase("test multi sign"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + Account daria{"daria"}; + Account edward{"edward"}; + env.fund(XRP(100000), alice, bob, carol, daria, edward); + env.close(); + + env(signers(bob, 2, {{daria, 1}, {edward, 1}})); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + auto dariaBalance = env.balance(daria); + auto edwardBalance = env.balance(edward); + + env(pay(alice, carol, XRP(100)), + fee(XRP(10)), + delegate::as(bob), + msig(daria, edward)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); + BEAST_EXPECT(env.balance(bob) == bobBalance - XRP(10)); + BEAST_EXPECT(env.balance(carol) == carolBalance + XRP(100)); + BEAST_EXPECT(env.balance(daria) == dariaBalance); + BEAST_EXPECT(env.balance(edward) == edwardBalance); + } + + void + testMultiSignQuorumNotMet() + { + testcase("test multi sign which does not meet quorum"); + using namespace jtx; + + Env env(*this); + Account alice{"alice"}; + Account bob{"bob"}; + Account carol{"carol"}; + Account daria = Account{"daria"}; + Account edward = Account{"edward"}; + Account fred = Account{"fred"}; + env.fund(XRP(100000), alice, bob, carol, daria, edward, fred); + env.close(); + + env(signers(bob, 3, {{daria, 1}, {edward, 1}, {fred, 1}})); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto aliceBalance = env.balance(alice); + auto bobBalance = env.balance(bob); + auto carolBalance = env.balance(carol); + auto dariaBalance = env.balance(daria); + auto edwardBalance = env.balance(edward); + + env(pay(alice, carol, XRP(100)), + fee(XRP(10)), + delegate::as(bob), + msig(daria, edward), + ter(tefBAD_QUORUM)); + env.close(); + BEAST_EXPECT(env.balance(alice) == aliceBalance); + BEAST_EXPECT(env.balance(bob) == bobBalance); + BEAST_EXPECT(env.balance(carol) == carolBalance); + BEAST_EXPECT(env.balance(daria) == dariaBalance); + BEAST_EXPECT(env.balance(edward) == edwardBalance); + } + + void + run() override + { + testFeatureDisabled(); + testDelegateSet(); + testInvalidRequest(); + testReserve(); + testFee(); + testSequence(); + testAccountDelete(); + testDelegateTransaction(); + testPaymentGranular(); + testTrustSetGranular(); + testAccountSetGranular(); + testMPTokenIssuanceSetGranular(); + testSingleSign(); + testSingleSignBadSecret(); + testMultiSign(); + testMultiSignQuorumNotMet(); + } +}; +BEAST_DEFINE_TESTSUITE(Delegate, app, ripple); +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/jtx/delegate.h b/src/test/jtx/delegate.h new file mode 100644 index 00000000000..9e8850fbe21 --- /dev/null +++ b/src/test/jtx/delegate.h @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#pragma once + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace delegate { + +Json::Value +set(jtx::Account const& account, + jtx::Account const& authorize, + std::vector const& permissions); + +Json::Value +entry( + jtx::Env& env, + jtx::Account const& account, + jtx::Account const& authorize); + +struct as +{ +private: + jtx::Account delegate_; + +public: + explicit as(jtx::Account const& account) : delegate_(account) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jtx) const + { + jtx.jv[sfDelegate.jsonName] = delegate_.human(); + } +}; + +} // namespace delegate +} // namespace jtx +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/jtx/flags.h b/src/test/jtx/flags.h index b5e82078034..09e5dac52ff 100644 --- a/src/test/jtx/flags.h +++ b/src/test/jtx/flags.h @@ -84,6 +84,18 @@ class flags_helper case asfAllowTrustLineClawback: mask_ |= lsfAllowTrustLineClawback; break; + case asfDisallowIncomingCheck: + mask_ |= lsfDisallowIncomingCheck; + break; + case asfDisallowIncomingNFTokenOffer: + mask_ |= lsfDisallowIncomingNFTokenOffer; + break; + case asfDisallowIncomingPayChan: + mask_ |= lsfDisallowIncomingPayChan; + break; + case asfDisallowIncomingTrustline: + mask_ |= lsfDisallowIncomingTrustline; + break; default: Throw("unknown flag"); } diff --git a/src/test/jtx/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index e006f268cf8..ecb2d62f43e 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -465,7 +465,9 @@ Env::autofill_sig(JTx& jt) return jt.signer(*this, jt); if (!jt.fill_sig) return; - auto const account = lookup(jv[jss::Account].asString()); + auto const account = jv.isMember(sfDelegate.jsonName) + ? lookup(jv[sfDelegate.jsonName].asString()) + : lookup(jv[jss::Account].asString()); if (!app().checkSigs()) { jv[jss::SigningPubKey] = strHex(account.pk().slice()); diff --git a/src/test/jtx/impl/delegate.cpp b/src/test/jtx/impl/delegate.cpp new file mode 100644 index 00000000000..3ceedff1901 --- /dev/null +++ b/src/test/jtx/impl/delegate.cpp @@ -0,0 +1,67 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace test { +namespace jtx { + +namespace delegate { + +Json::Value +set(jtx::Account const& account, + jtx::Account const& authorize, + std::vector const& permissions) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::DelegateSet; + jv[jss::Account] = account.human(); + jv[sfAuthorize.jsonName] = authorize.human(); + Json::Value permissionsJson(Json::arrayValue); + for (auto const& permission : permissions) + { + Json::Value permissionValue; + permissionValue[sfPermissionValue.jsonName] = permission; + Json::Value permissionObj; + permissionObj[sfPermission.jsonName] = permissionValue; + permissionsJson.append(permissionObj); + } + + jv[sfPermissions.jsonName] = permissionsJson; + + return jv; +} + +Json::Value +entry(jtx::Env& env, jtx::Account const& account, jtx::Account const& authorize) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::delegate][jss::account] = account.human(); + jvParams[jss::delegate][jss::authorize] = authorize.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +} // namespace delegate +} // namespace jtx +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index ead6a47c25e..51490ad21ec 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -233,6 +233,8 @@ MPTTester::set(MPTSet const& arg) } if (arg.holder) jv[sfHolder] = arg.holder->human(); + if (arg.delegate) + jv[sfDelegate] = arg.delegate->human(); if (submit(arg, jv) == tesSUCCESS && arg.flags.value_or(0)) { auto require = [&](std::optional const& holder, diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 12b9d74d27c..950ab0d409d 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -136,6 +136,7 @@ struct MPTSet std::optional ownerCount = std::nullopt; std::optional holderCount = std::nullopt; std::optional flags = std::nullopt; + std::optional delegate = std::nullopt; std::optional err = std::nullopt; }; diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index 8d4f7631254..cd26758c1f2 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2042,6 +2042,78 @@ static constexpr TxnTestData txnTestArray[] = { "Cannot specify differing 'Amount' and 'DeliverMax'", "Cannot specify differing 'Amount' and 'DeliverMax'"}}}, + {"Minimal delegated transaction.", + __LINE__, + R"({ + "command": "doesnt_matter", + "secret": "a", + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "TransactionType": "Payment", + "Delegate": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA" + } +})", + {{"", + "", + "Missing field 'account'.", + "Missing field 'tx_json.Sequence'."}}}, + + {"Delegate not well formed.", + __LINE__, + R"({ + "command": "doesnt_matter", + "secret": "a", + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi", + "TransactionType": "Payment", + "Delegate": "NotAnAccount" + } +})", + {{"Invalid field 'tx_json.Delegate'.", + "Invalid field 'tx_json.Delegate'.", + "Missing field 'account'.", + "Missing field 'tx_json.Sequence'."}}}, + + {"Delegate not in ledger.", + __LINE__, + R"({ + "command": "doesnt_matter", + "secret": "a", + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi", + "TransactionType": "Payment", + "Delegate": "rDg53Haik2475DJx8bjMDSDPj4VX7htaMd" + } +})", + {{"Delegate account not found.", + "Delegate account not found.", + "Missing field 'account'.", + "Missing field 'tx_json.Sequence'."}}}, + + {"Delegate and secret not match.", + __LINE__, + R"({ + "command": "doesnt_matter", + "secret": "aa", + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rJrxi4Wxev4bnAGVNP9YCdKPdAoKfAmcsi", + "TransactionType": "Payment", + "Delegate": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA" + } +})", + {{"Secret does not match account.", + "Secret does not match account.", + "Missing field 'account'.", + "Missing field 'tx_json.Sequence'."}}}, + }; class JSONRPC_test : public beast::unit_test::suite diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index 32332adb203..465d6c6631b 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -439,6 +440,116 @@ class LedgerEntry_test : public beast::unit_test::suite } } + void + testLedgerEntryDelegate() + { + testcase("ledger_entry Delegate"); + + using namespace test::jtx; + + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + env(delegate::set(alice, bob, {"Payment", "CheckCreate"})); + env.close(); + std::string const ledgerHash{to_string(env.closed()->info().hash)}; + std::string delegateIndex; + { + // Request by account and authorize + Json::Value jvParams; + jvParams[jss::delegate][jss::account] = alice.human(); + jvParams[jss::delegate][jss::authorize] = bob.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate); + BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human()); + delegateIndex = jrr[jss::node][jss::index].asString(); + } + { + // Request by index. + Json::Value jvParams; + jvParams[jss::delegate] = delegateIndex; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Delegate); + BEAST_EXPECT(jrr[jss::node][sfAccount.jsonName] == alice.human()); + BEAST_EXPECT(jrr[jss::node][sfAuthorize.jsonName] == bob.human()); + } + { + // Malformed request: delegate neither object nor string. + Json::Value jvParams; + jvParams[jss::delegate] = 5; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Malformed request: delegate not hex string. + Json::Value jvParams; + jvParams[jss::delegate] = "0123456789ABCDEFG"; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Malformed request: account not a string + Json::Value jvParams; + jvParams[jss::delegate][jss::account] = 5; + jvParams[jss::delegate][jss::authorize] = bob.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedAddress", ""); + } + { + // Malformed request: authorize not a string + Json::Value jvParams; + jvParams[jss::delegate][jss::account] = alice.human(); + jvParams[jss::delegate][jss::authorize] = 5; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedAddress", ""); + } + { + // this lambda function is used test malformed account and authroize + auto testMalformedAccount = + [&](std::optional const& account, + std::optional const& authorize, + std::string const& error) { + Json::Value jvParams; + jvParams[jss::ledger_hash] = ledgerHash; + if (account) + jvParams[jss::delegate][jss::account] = *account; + if (authorize) + jvParams[jss::delegate][jss::authorize] = *authorize; + auto const jrr = env.rpc( + "json", + "ledger_entry", + to_string(jvParams))[jss::result]; + checkErrorValue(jrr, error, ""); + }; + // missing account + testMalformedAccount(std::nullopt, bob.human(), "malformedRequest"); + // missing authorize + testMalformedAccount( + alice.human(), std::nullopt, "malformedRequest"); + // malformed account + testMalformedAccount("-", bob.human(), "malformedAddress"); + // malformed authorize + testMalformedAccount(alice.human(), "-", "malformedAddress"); + } + } + void testLedgerEntryDepositPreauth() { @@ -2266,6 +2377,7 @@ class LedgerEntry_test : public beast::unit_test::suite testLedgerEntryAccountRoot(); testLedgerEntryCheck(); testLedgerEntryCredentials(); + testLedgerEntryDelegate(); testLedgerEntryDepositPreauth(); testLedgerEntryDepositPreauthCred(); testLedgerEntryDirectory(); diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 78caecb945f..4e8d2964ca0 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index ebc28341097..b2c0007dc77 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -17,8 +17,8 @@ */ //============================================================================== -#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED -#define RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED +#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED +#define RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED #include @@ -127,4 +127,4 @@ isOnlyLiquidityProvider( } // namespace ripple -#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED +#endif // RIPPLE_APP_MISC_AMMUTILS_H_INCLUDED diff --git a/src/xrpld/app/misc/DelegateUtils.h b/src/xrpld/app/misc/DelegateUtils.h new file mode 100644 index 00000000000..cad3bed3764 --- /dev/null +++ b/src/xrpld/app/misc/DelegateUtils.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + 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_APP_MISC_DELEGATEUTILS_H_INCLUDED +#define RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +/** + * Check if the delegate account has permission to execute the transaction. + * @param delegate The delegate account. + * @param tx The transaction that the delegate account intends to execute. + * @return tesSUCCESS if the transaction is allowed, tecNO_PERMISSION if not. + */ +TER +checkTxPermission(std::shared_ptr const& delegate, STTx const& tx); + +/** + * Load the granular permissions granted to the delegate account for the + * specified transaction type + * @param delegate The delegate account. + * @param type Used to determine which granted granular permissions to load, + * based on the transaction type. + * @param granularPermissions Granted granular permissions tied to the + * transaction type. + */ +void +loadGranularPermission( + std::shared_ptr const& delegate, + TxType const& type, + std::unordered_set& granularPermissions); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_DELEGATEUTILS_H_INCLUDED diff --git a/src/xrpld/app/misc/detail/DelegateUtils.cpp b/src/xrpld/app/misc/detail/DelegateUtils.cpp new file mode 100644 index 00000000000..7b7021fe9e8 --- /dev/null +++ b/src/xrpld/app/misc/detail/DelegateUtils.cpp @@ -0,0 +1,66 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +TER +checkTxPermission(std::shared_ptr const& delegate, STTx const& tx) +{ + if (!delegate) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + auto const permissionArray = delegate->getFieldArray(sfPermissions); + auto const txPermission = tx.getTxnType() + 1; + + for (auto const& permission : permissionArray) + { + auto const permissionValue = permission[sfPermissionValue]; + if (permissionValue == txPermission) + return tesSUCCESS; + } + + return tecNO_PERMISSION; +} + +void +loadGranularPermission( + std::shared_ptr const& delegate, + TxType const& txType, + std::unordered_set& granularPermissions) +{ + if (!delegate) + return; // LCOV_EXCL_LINE + + auto const permissionArray = delegate->getFieldArray(sfPermissions); + for (auto const& permission : permissionArray) + { + auto const permissionValue = permission[sfPermissionValue]; + auto const granularValue = + static_cast(permissionValue); + auto const& type = + Permission::getInstance().getGranularTxType(granularValue); + if (type && *type == txType) + granularPermissions.insert(granularValue); + } +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/DelegateSet.cpp b/src/xrpld/app/tx/detail/DelegateSet.cpp new file mode 100644 index 00000000000..d93ed6fa96b --- /dev/null +++ b/src/xrpld/app/tx/detail/DelegateSet.cpp @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include +#include +#include + +namespace ripple { + +NotTEC +DelegateSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featurePermissionDelegation)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const& permissions = ctx.tx.getFieldArray(sfPermissions); + if (permissions.size() > permissionMaxSize) + return temARRAY_TOO_LARGE; + + // can not authorize self + if (ctx.tx[sfAccount] == ctx.tx[sfAuthorize]) + return temMALFORMED; + + std::unordered_set permissionSet; + + for (auto const& permission : permissions) + { + if (!permissionSet.insert(permission[sfPermissionValue]).second) + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +DelegateSet::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.exists(keylet::account(ctx.tx[sfAccount]))) + return terNO_ACCOUNT; // LCOV_EXCL_LINE + + if (!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))) + return terNO_ACCOUNT; + + auto const& permissions = ctx.tx.getFieldArray(sfPermissions); + for (auto const& permission : permissions) + { + auto const permissionValue = permission[sfPermissionValue]; + if (!Permission::getInstance().isDelegatable(permissionValue)) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +DelegateSet::doApply() +{ + auto const sleOwner = ctx_.view().peek(keylet::account(account_)); + if (!sleOwner) + return tefINTERNAL; // LCOV_EXCL_LINE + + auto const& authAccount = ctx_.tx[sfAuthorize]; + auto const delegateKey = keylet::delegate(account_, authAccount); + + auto sle = ctx_.view().peek(delegateKey); + if (sle) + { + auto const& permissions = ctx_.tx.getFieldArray(sfPermissions); + if (permissions.empty()) + // if permissions array is empty, delete the ledger object. + return deleteDelegate(view(), sle, account_, j_); + + sle->setFieldArray(sfPermissions, permissions); + ctx_.view().update(sle); + return tesSUCCESS; + } + + STAmount const reserve{ctx_.view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + + auto const& permissions = ctx_.tx.getFieldArray(sfPermissions); + if (!permissions.empty()) + { + sle = std::make_shared(delegateKey); + sle->setAccountID(sfAccount, account_); + sle->setAccountID(sfAuthorize, authAccount); + + sle->setFieldArray(sfPermissions, permissions); + auto const page = ctx_.view().dirInsert( + keylet::ownerDir(account_), + delegateKey, + describeOwnerDir(account_)); + + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + + (*sle)[sfOwnerNode] = *page; + ctx_.view().insert(sle); + adjustOwnerCount(ctx_.view(), sleOwner, 1, ctx_.journal); + } + + return tesSUCCESS; +} + +TER +DelegateSet::deleteDelegate( + ApplyView& view, + std::shared_ptr const& sle, + AccountID const& account, + beast::Journal j) +{ + if (!sle) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (!view.dirRemove( + keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), false)) + { + // LCOV_EXCL_START + JLOG(j.fatal()) << "Unable to delete Delegate from owner."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + + auto const sleOwner = view.peek(keylet::account(account)); + if (!sleOwner) + return tecINTERNAL; // LCOV_EXCL_LINE + + adjustOwnerCount(view, sleOwner, -1, j); + + view.erase(sle); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/DelegateSet.h b/src/xrpld/app/tx/detail/DelegateSet.h new file mode 100644 index 00000000000..6b01d632817 --- /dev/null +++ b/src/xrpld/app/tx/detail/DelegateSet.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + 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_DELEGATESET_H_INCLUDED +#define RIPPLE_TX_DELEGATESET_H_INCLUDED + +#include + +namespace ripple { + +class DelegateSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit DelegateSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + // Interface used by DeleteAccount + static TER + deleteDelegate( + ApplyView& view, + std::shared_ptr const& sle, + AccountID const& account, + beast::Journal j); +}; + +} // namespace ripple + +#endif \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/DeleteAccount.cpp b/src/xrpld/app/tx/detail/DeleteAccount.cpp index d5620694604..7aa47e05f38 100644 --- a/src/xrpld/app/tx/detail/DeleteAccount.cpp +++ b/src/xrpld/app/tx/detail/DeleteAccount.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -180,6 +181,18 @@ removeCredentialFromLedger( return credentials::deleteSLE(view, sleDel, j); } +TER +removeDelegateFromLedger( + Application& app, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const& sleDel, + beast::Journal j) +{ + return DelegateSet::deleteDelegate(view, sleDel, account, 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 @@ -204,6 +217,8 @@ nonObligationDeleter(LedgerEntryType t) return removeOracleFromLedger; case ltCREDENTIAL: return removeCredentialFromLedger; + case ltDELEGATE: + return removeDelegateFromLedger; default: return nullptr; } diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index b97a0c02eea..2441cb040ae 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -387,6 +387,7 @@ AccountRootsDeletedClean::finalize( view.rules().enabled(featureInvariantsV1_1); auto const objectExists = [&view, enforce, &j](auto const& keylet) { + (void)enforce; if (auto const sle = view.read(keylet)) { // Finding the object is bad @@ -463,6 +464,7 @@ LedgerEntryTypesMatch::visitEntry( switch (after->getType()) { case ltACCOUNT_ROOT: + case ltDELEGATE: case ltDIR_NODE: case ltRIPPLE_STATE: case ltTICKET: diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp index 12208dba1b6..85a1f6cf1a0 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -50,6 +51,43 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) return preflight2(ctx); } +TER +MPTokenIssuanceSet::checkPermission(ReadView const& view, STTx const& tx) +{ + auto const delegate = tx[~sfDelegate]; + if (!delegate) + return tesSUCCESS; + + auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); + auto const sle = view.read(delegateKey); + + if (!sle) + return tecNO_PERMISSION; + + if (checkTxPermission(sle, tx) == tesSUCCESS) + return tesSUCCESS; + + auto const txFlags = tx.getFlags(); + + // this is added in case more flags will be added for MPTokenIssuanceSet + // in the future. Currently unreachable. + if (txFlags & tfMPTokenIssuanceSetPermissionMask) + return tecNO_PERMISSION; // LCOV_EXCL_LINE + + std::unordered_set granularPermissions; + loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); + + if (txFlags & tfMPTLock && + !granularPermissions.contains(MPTokenIssuanceLock)) + return tecNO_PERMISSION; + + if (txFlags & tfMPTUnlock && + !granularPermissions.contains(MPTokenIssuanceUnlock)) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + TER MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h index 895be973120..5b3db0e75bc 100644 --- a/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h +++ b/src/xrpld/app/tx/detail/MPTokenIssuanceSet.h @@ -36,6 +36,9 @@ class MPTokenIssuanceSet : public Transactor static NotTEC preflight(PreflightContext const& ctx); + static TER + checkPermission(ReadView const& view, STTx const& tx); + static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index c2b7b23a6a5..f2f4ac4f7c9 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -238,6 +239,39 @@ Payment::preflight(PreflightContext const& ctx) return preflight2(ctx); } +TER +Payment::checkPermission(ReadView const& view, STTx const& tx) +{ + auto const delegate = tx[~sfDelegate]; + if (!delegate) + return tesSUCCESS; + + auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); + auto const sle = view.read(delegateKey); + + if (!sle) + return tecNO_PERMISSION; + + if (checkTxPermission(sle, tx) == tesSUCCESS) + return tesSUCCESS; + + std::unordered_set granularPermissions; + loadGranularPermission(sle, ttPAYMENT, granularPermissions); + + auto const& dstAmount = tx.getFieldAmount(sfAmount); + auto const& amountIssue = dstAmount.issue(); + + if (granularPermissions.contains(PaymentMint) && !isXRP(amountIssue) && + amountIssue.account == tx[sfAccount]) + return tesSUCCESS; + + if (granularPermissions.contains(PaymentBurn) && !isXRP(amountIssue) && + amountIssue.account == tx[sfDestination]) + return tesSUCCESS; + + return tecNO_PERMISSION; +} + TER Payment::preclaim(PreclaimContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/Payment.h b/src/xrpld/app/tx/detail/Payment.h index 775d4e8d46b..010a2453cf2 100644 --- a/src/xrpld/app/tx/detail/Payment.h +++ b/src/xrpld/app/tx/detail/Payment.h @@ -45,6 +45,9 @@ class Payment : public Transactor static NotTEC preflight(PreflightContext const& ctx); + static TER + checkPermission(ReadView const& view, STTx const& tx); + static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index d871cc32800..599819151a4 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -188,6 +189,61 @@ SetAccount::preflight(PreflightContext const& ctx) return preflight2(ctx); } +TER +SetAccount::checkPermission(ReadView const& view, STTx const& tx) +{ + // SetAccount is prohibited to be granted on a transaction level, + // but some granular permissions are allowed. + auto const delegate = tx[~sfDelegate]; + if (!delegate) + return tesSUCCESS; + + auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); + auto const sle = view.read(delegateKey); + + if (!sle) + return tecNO_PERMISSION; + + std::unordered_set granularPermissions; + loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions); + + auto const uSetFlag = tx.getFieldU32(sfSetFlag); + auto const uClearFlag = tx.getFieldU32(sfClearFlag); + auto const uTxFlags = tx.getFlags(); + // We don't support any flag based granular permission under + // AccountSet transaction. If any delegated account is trying to + // update the flag on behalf of another account, it is not + // authorized. + if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags != tfFullyCanonicalSig) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfEmailHash) && + !granularPermissions.contains(AccountEmailHashSet)) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfWalletLocator) || + tx.isFieldPresent(sfNFTokenMinter)) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfMessageKey) && + !granularPermissions.contains(AccountMessageKeySet)) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfDomain) && + !granularPermissions.contains(AccountDomainSet)) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfTransferRate) && + !granularPermissions.contains(AccountTransferRateSet)) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfTickSize) && + !granularPermissions.contains(AccountTickSizeSet)) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + TER SetAccount::preclaim(PreclaimContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/SetAccount.h b/src/xrpld/app/tx/detail/SetAccount.h index 4604a11a6c6..ed4242c250f 100644 --- a/src/xrpld/app/tx/detail/SetAccount.h +++ b/src/xrpld/app/tx/detail/SetAccount.h @@ -41,6 +41,9 @@ class SetAccount : public Transactor static NotTEC preflight(PreflightContext const& ctx); + static TER + checkPermission(ReadView const& view, STTx const& tx); + static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/SetTrust.cpp b/src/xrpld/app/tx/detail/SetTrust.cpp index 93abcdc4c4d..9fe267b8e1c 100644 --- a/src/xrpld/app/tx/detail/SetTrust.cpp +++ b/src/xrpld/app/tx/detail/SetTrust.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -127,6 +128,69 @@ SetTrust::preflight(PreflightContext const& ctx) return preflight2(ctx); } +TER +SetTrust::checkPermission(ReadView const& view, STTx const& tx) +{ + auto const delegate = tx[~sfDelegate]; + if (!delegate) + return tesSUCCESS; + + auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); + auto const sle = view.read(delegateKey); + + if (!sle) + return tecNO_PERMISSION; + + if (checkTxPermission(sle, tx) == tesSUCCESS) + return tesSUCCESS; + + std::uint32_t const txFlags = tx.getFlags(); + + // Currently we only support TrustlineAuthorize, TrustlineFreeze and + // TrustlineUnfreeze granular permission. Setting other flags returns + // error. + if (txFlags & tfTrustSetPermissionMask) + return tecNO_PERMISSION; + + if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut)) + return tecNO_PERMISSION; + + auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount); + auto const sleRippleState = view.read(keylet::line( + tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.getCurrency())); + + // if the trustline does not exist, granular permissions are + // not allowed to create trustline + if (!sleRippleState) + return tecNO_PERMISSION; + + std::unordered_set granularPermissions; + loadGranularPermission(sle, ttTRUST_SET, granularPermissions); + + if (txFlags & tfSetfAuth && + !granularPermissions.contains(TrustlineAuthorize)) + return tecNO_PERMISSION; + if (txFlags & tfSetFreeze && !granularPermissions.contains(TrustlineFreeze)) + return tecNO_PERMISSION; + if (txFlags & tfClearFreeze && + !granularPermissions.contains(TrustlineUnfreeze)) + return tecNO_PERMISSION; + + // updating LimitAmount is not allowed only with granular permissions, + // unless there's a new granular permission for this in the future. + auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer() + ? sleRippleState->getFieldAmount(sfHighLimit) + : sleRippleState->getFieldAmount(sfLowLimit); + + STAmount saLimitAllow = saLimitAmount; + saLimitAllow.setIssuer(tx[sfAccount]); + + if (curLimit != saLimitAllow) + return tecNO_PERMISSION; + + return tesSUCCESS; +} + TER SetTrust::preclaim(PreclaimContext const& ctx) { diff --git a/src/xrpld/app/tx/detail/SetTrust.h b/src/xrpld/app/tx/detail/SetTrust.h index 7a5394c6841..a0476918ac4 100644 --- a/src/xrpld/app/tx/detail/SetTrust.h +++ b/src/xrpld/app/tx/detail/SetTrust.h @@ -38,6 +38,9 @@ class SetTrust : public Transactor static NotTEC preflight(PreflightContext const& ctx); + static TER + checkPermission(ReadView const& view, STTx const& tx); + static TER preclaim(PreclaimContext const& ctx); diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 2e49794c2d5..390f32e02b0 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -89,6 +90,15 @@ preflight1(PreflightContext const& ctx) return temMALFORMED; } + if (ctx.tx.isFieldPresent(sfDelegate)) + { + if (!ctx.rules.enabled(featurePermissionDelegation)) + return temDISABLED; + + if (ctx.tx[sfDelegate] == ctx.tx[sfAccount]) + return temBAD_SIGNER; + } + auto const ret = preflight0(ctx); if (!isTesSuccess(ret)) return ret; @@ -190,6 +200,22 @@ Transactor::Transactor(ApplyContext& ctx) { } +TER +Transactor::checkPermission(ReadView const& view, STTx const& tx) +{ + auto const delegate = tx[~sfDelegate]; + if (!delegate) + return tesSUCCESS; + + auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); + auto const sle = view.read(delegateKey); + + if (!sle) + return tecNO_PERMISSION; + + return checkTxPermission(sle, tx); +} + XRPAmount Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -246,7 +272,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) if (feePaid == beast::zero) return tesSUCCESS; - auto const id = ctx.tx.getAccountID(sfAccount); + auto const id = ctx.tx.isFieldPresent(sfDelegate) + ? ctx.tx.getAccountID(sfDelegate) + : ctx.tx.getAccountID(sfAccount); auto const sle = ctx.view.read(keylet::account(id)); if (!sle) return terNO_ACCOUNT; @@ -276,17 +304,32 @@ Transactor::payFee() { auto const feePaid = ctx_.tx[sfFee].xrp(); - auto const sle = view().peek(keylet::account(account_)); - if (!sle) - return tefINTERNAL; + if (ctx_.tx.isFieldPresent(sfDelegate)) + { + // Delegated transactions are paid by the delegated account. + auto const delegate = ctx_.tx.getAccountID(sfDelegate); + auto const delegatedSle = view().peek(keylet::account(delegate)); + if (!delegatedSle) + return tefINTERNAL; // LCOV_EXCL_LINE + + delegatedSle->setFieldAmount( + sfBalance, delegatedSle->getFieldAmount(sfBalance) - feePaid); + view().update(delegatedSle); + } + else + { + auto const sle = view().peek(keylet::account(account_)); + if (!sle) + return tefINTERNAL; // LCOV_EXCL_LINE - // Deduct the fee, so it's not available during the transaction. - // Will only write the account back if the transaction succeeds. + // Deduct the fee, so it's not available during the transaction. + // Will only write the account back if the transaction succeeds. - mSourceBalance -= feePaid; - sle->setFieldAmount(sfBalance, mSourceBalance); + mSourceBalance -= feePaid; + sle->setFieldAmount(sfBalance, mSourceBalance); - // VFALCO Should we call view().rawDestroyXRP() here as well? + // VFALCO Should we call view().rawDestroyXRP() here as well? + } return tesSUCCESS; } @@ -542,7 +585,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } // Look up the account. - auto const idAccount = ctx.tx.getAccountID(sfAccount); + auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) + ? ctx.tx.getAccountID(sfDelegate) + : ctx.tx.getAccountID(sfAccount); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; @@ -612,7 +657,9 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) NotTEC Transactor::checkMultiSign(PreclaimContext const& ctx) { - auto const id = ctx.tx.getAccountID(sfAccount); + auto const id = ctx.tx.isFieldPresent(sfDelegate) + ? ctx.tx.getAccountID(sfDelegate) + : ctx.tx.getAccountID(sfAccount); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = ctx.view.read(keylet::signers(id)); @@ -870,15 +917,22 @@ Transactor::reset(XRPAmount fee) // is missing then we can't very well charge it a fee, can we? return {tefINTERNAL, beast::zero}; - auto const balance = txnAcct->getFieldAmount(sfBalance).xrp(); + auto const payerSle = ctx_.tx.isFieldPresent(sfDelegate) + ? view().peek(keylet::account(ctx_.tx.getAccountID(sfDelegate))) + : txnAcct; + if (!payerSle) + return {tefINTERNAL, beast::zero}; // LCOV_EXCL_LINE + + auto const balance = payerSle->getFieldAmount(sfBalance).xrp(); // balance should have already been checked in checkFee / preFlight. XRPL_ASSERT( balance != beast::zero && (!view().open() || balance >= fee), "ripple::Transactor::reset : valid balance"); - // We retry/reject the transaction if the account balance is zero or we're - // applying against an open ledger and the balance is less than the fee + // We retry/reject the transaction if the account balance is zero or + // we're applying against an open ledger and the balance is less than + // the fee if (fee > balance) fee = balance; @@ -888,13 +942,17 @@ Transactor::reset(XRPAmount fee) // If for some reason we are unable to consume the ticket or sequence // then the ledger is corrupted. Rather than make things worse we // reject the transaction. - txnAcct->setFieldAmount(sfBalance, balance - fee); + payerSle->setFieldAmount(sfBalance, balance - fee); TER const ter{consumeSeqProxy(txnAcct)}; XRPL_ASSERT( isTesSuccess(ter), "ripple::Transactor::reset : result is tesSUCCESS"); if (isTesSuccess(ter)) + { view().update(txnAcct); + if (payerSle != txnAcct) + view().update(payerSle); + } return {ter, fee}; } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index e98269c38a0..4956f021df0 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -24,6 +24,7 @@ #include #include +#include #include namespace ripple { @@ -149,6 +150,9 @@ class Transactor // after checkSeq/Fee/Sign. return tesSUCCESS; } + + static TER + checkPermission(ReadView const& view, STTx const& tx); ///////////////////////////////////////////////////// // Interface used by DeleteAccount diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 4cb505db50c..b20b5a29f62 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -89,8 +90,8 @@ with_txn_type(TxType txnType, F&& f) #pragma push_macro("TRANSACTION") #undef TRANSACTION -#define TRANSACTION(tag, value, name, fields) \ - case tag: \ +#define TRANSACTION(tag, value, name, delegatable, fields) \ + case tag: \ return f.template operator()(); #include @@ -193,6 +194,11 @@ invoke_preclaim(PreclaimContext const& ctx) result = T::checkFee(ctx, calculateBaseFee(ctx.view, ctx.tx)); + if (result != tesSUCCESS) + return result; + + result = T::checkPermission(ctx.view, ctx.tx); + if (result != tesSUCCESS) return result; diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 2a7807f8cae..3f388e636fe 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -531,10 +531,40 @@ transactionPreProcessImpl( if (!signingArgs.isMultiSigning()) { // Make sure the account and secret belong together. - auto const err = acctMatchesPubKey(sle, srcAddressID, pk); + if (tx_json.isMember(sfDelegate.jsonName)) + { + // Delegated transaction + auto const delegateJson = tx_json[sfDelegate.jsonName]; + auto const ptrDelegatedAddressID = delegateJson.isString() + ? parseBase58(delegateJson.asString()) + : std::nullopt; + + if (!ptrDelegatedAddressID) + { + return RPC::make_error( + rpcSRC_ACT_MALFORMED, + RPC::invalid_field_message("tx_json.Delegate")); + } + + auto delegatedAddressID = *ptrDelegatedAddressID; + auto delegatedSle = app.openLedger().current()->read( + keylet::account(delegatedAddressID)); + if (!delegatedSle) + return rpcError(rpcDELEGATE_ACT_NOT_FOUND); - if (err != rpcSUCCESS) - return rpcError(err); + auto const err = + acctMatchesPubKey(delegatedSle, delegatedAddressID, pk); + + if (err != rpcSUCCESS) + return rpcError(err); + } + else + { + auto const err = acctMatchesPubKey(sle, srcAddressID, pk); + + if (err != rpcSUCCESS) + return rpcError(err); + } } } diff --git a/src/xrpld/rpc/handlers/LedgerEntry.cpp b/src/xrpld/rpc/handlers/LedgerEntry.cpp index 1d158257862..ade9b9578bd 100644 --- a/src/xrpld/rpc/handlers/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/LedgerEntry.cpp @@ -230,6 +230,46 @@ parseAuthorizeCredentials(Json::Value const& jv) return arr; } +static std::optional +parseDelegate(Json::Value const& params, Json::Value& jvResult) +{ + if (!params.isObject()) + { + uint256 uNodeIndex; + if (!params.isString() || !uNodeIndex.parseHex(params.asString())) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + return uNodeIndex; + } + if (!params.isMember(jss::account) || !params.isMember(jss::authorize)) + { + jvResult[jss::error] = "malformedRequest"; + return std::nullopt; + } + if (!params[jss::account].isString() || !params[jss::authorize].isString()) + { + jvResult[jss::error] = "malformedAddress"; + return std::nullopt; + } + auto const account = + parseBase58(params[jss::account].asString()); + if (!account) + { + jvResult[jss::error] = "malformedAddress"; + return std::nullopt; + } + auto const authorize = + parseBase58(params[jss::authorize].asString()); + if (!authorize) + { + jvResult[jss::error] = "malformedAddress"; + return std::nullopt; + } + return keylet::delegate(*account, *authorize).key; +} + static std::optional parseDepositPreauth(Json::Value const& dp, Json::Value& jvResult) { @@ -884,6 +924,7 @@ doLedgerEntry(RPC::JsonContext& context) {jss::bridge, parseBridge, ltBRIDGE}, {jss::check, parseCheck, ltCHECK}, {jss::credential, parseCredential, ltCREDENTIAL}, + {jss::delegate, parseDelegate, ltDELEGATE}, {jss::deposit_preauth, parseDepositPreauth, ltDEPOSIT_PREAUTH}, {jss::did, parseDID, ltDID}, {jss::directory, parseDirectory, ltDIR_NODE},