From 772ea80a25227b5e9fb75ea0cdb62723b099cd33 Mon Sep 17 00:00:00 2001 From: yinyiqian1 Date: Wed, 17 Jun 2026 19:20:54 -0400 Subject: [PATCH] fix: Use template for granular delegation permissions (#6613) Co-authored-by: Bart --- include/xrpl/ledger/helpers/DelegateHelpers.h | 10 +- include/xrpl/protocol/Permissions.h | 80 +++- include/xrpl/protocol/detail/features.macro | 2 +- .../xrpl/protocol/detail/permissions.macro | 89 ++-- include/xrpl/tx/Transactor.h | 63 ++- .../xrpl/tx/transactors/account/AccountSet.h | 3 - include/xrpl/tx/transactors/payment/Payment.h | 5 +- .../tx/transactors/token/MPTokenIssuanceSet.h | 3 - include/xrpl/tx/transactors/token/TrustSet.h | 5 +- src/libxrpl/protocol/Permissions.cpp | 317 +++++++++----- src/libxrpl/protocol/STTx.cpp | 2 +- src/libxrpl/tx/Transactor.cpp | 36 +- src/libxrpl/tx/applySteps.cpp | 3 +- .../tx/transactors/account/AccountSet.cpp | 51 --- .../tx/transactors/delegate/DelegateUtils.cpp | 12 +- .../tx/transactors/payment/Payment.cpp | 30 +- .../transactors/token/MPTokenIssuanceSet.cpp | 36 -- src/libxrpl/tx/transactors/token/TrustSet.cpp | 44 +- src/test/app/Delegate_test.cpp | 407 +++++++++++++++++- 19 files changed, 851 insertions(+), 347 deletions(-) diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h index 9cdad7173d..a517eefdaa 100644 --- a/include/xrpl/ledger/helpers/DelegateHelpers.h +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h @@ -23,13 +23,9 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx); * @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. + * @return the granted granular permissions tied to the transaction type. */ -void -loadGranularPermission( - SLE::const_ref delegate, - TxType const& type, - std::unordered_set& granularPermissions); +std::unordered_set +getGranularPermission(SLE::const_ref delegate, TxType const& type); } // namespace xrpl diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 5d56fa4461..eb161ef7ad 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -7,8 +7,13 @@ #include #include #include +#include +#include namespace xrpl { + +class STTx; + /** * We have both transaction type permissions and granular type permissions. * Since we will reuse the TransactionFormats to parse the Transaction @@ -19,15 +24,15 @@ namespace xrpl { // Macro-generated, complex // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum GranularPermissionType : std::uint32_t { -#pragma push_macro("PERMISSION") -#undef PERMISSION +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION -#define PERMISSION(type, txType, value) type = (value), +#define GRANULAR_PERMISSION(name, txType, value, ...) name = (value), #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; // Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor @@ -40,15 +45,30 @@ class Permission private: Permission(); - std::unordered_map txFeatureMap_; + struct GranularPermissionEntry + { + std::string name; + TxType txType; + std::uint32_t permittedFlags; + SOTemplate permittedFields; - std::unordered_map delegableTx_; + GranularPermissionEntry( + std::string name, + TxType txType, + std::uint32_t permittedFlags, + std::vector fields); + }; - std::unordered_map granularPermissionMap_; + struct TxDelegationEntry + { + uint256 amendment; + Delegation delegable{NotDelegable}; + }; - std::unordered_map granularNameMap_; - - std::unordered_map granularTxTypeMap_; + std::unordered_set granularTxTypes_; + std::unordered_map txDelegationMap_; + std::unordered_map granularPermissionsByName_; + std::unordered_map granularPermissions_; public: static Permission const& @@ -59,30 +79,52 @@ public: operator=(Permission const&) = delete; [[nodiscard]] std::optional - getPermissionName(std::uint32_t const value) const; + getPermissionName(std::uint32_t value) const; [[nodiscard]] std::optional getGranularValue(std::string const& name) const; [[nodiscard]] std::optional - getGranularName(GranularPermissionType const& value) const; + getGranularName(GranularPermissionType value) const; [[nodiscard]] std::optional - getGranularTxType(GranularPermissionType const& gpType) const; + getGranularTxType(GranularPermissionType gpType) const; + // Returns a reference to avoid copying uint256 - 32 bytes. std::optional + // cannot hold references directly, so std::reference_wrapper is used. [[nodiscard]] std::optional> getTxFeature(TxType txType) const; [[nodiscard]] bool - isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const; + isDelegable(std::uint32_t permissionValue, Rules const& rules) const; + + [[nodiscard]] bool + hasGranularPermissions(TxType txType) const; // for tx level permission, permission value is equal to tx type plus one - static uint32_t - txToPermissionType(TxType const& type); + [[nodiscard]] static uint32_t + txToPermissionType(TxType type); // tx type value is permission value minus one - static TxType - permissionToTxType(uint32_t const& value); + [[nodiscard]] static TxType + permissionToTxType(std::uint32_t value); + + /** + * @brief Verifies a delegated transaction against its granular permission template. + * + * @note WARNING: Do not move this check before standard transaction-level + * format checks, which is in preclaim. This function assumes the transaction's + * base structural integrity (fees, sequence, signatures) has already been + * validated. + * + * @param tx The transaction to verify. + * @param heldPermissions The granular permissions that the sender hold. + * @return true if the transaction fields and flags comply with the granular template. + */ + [[nodiscard]] bool + checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const; }; } // namespace xrpl diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index d3500ab144..d25b0b1f2c 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -21,7 +21,7 @@ XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultN XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes) XRPL_FIX (BatchInnerSigs, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocol, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(PermissionDelegationV1_1, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (IncludeKeyletFields, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicMPT, Supported::No, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/permissions.macro b/include/xrpl/protocol/detail/permissions.macro index 729861a013..35532a03ca 100644 --- a/include/xrpl/protocol/detail/permissions.macro +++ b/include/xrpl/protocol/detail/permissions.macro @@ -1,49 +1,74 @@ -#if !defined(PERMISSION) -#error "undefined macro: PERMISSION" +#if !defined(GRANULAR_PERMISSION) +#error "undefined macro: GRANULAR_PERMISSION" #endif /** - * PERMISSION(name, type, txType, value) + * GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields) * - * 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. + * Defines a granular permission: + * name: the granular permission name. + * txType: the corresponding TxType for this permission. + * value: the uint32 numeric value for the enum type. + * allowedFlags: transaction flags permitted under this permission. + * allowedFields: transaction fields permitted under this permission. */ -/** This permission grants the delegated account the ability to authorize a trustline. */ -PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537) +/** Grants the ability to authorize a trustline. */ +GRANULAR_PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537, tfUniversal | tfSetfAuth, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to freeze a trustline. */ -PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538) +/** Grants the ability to freeze a trustline. */ +GRANULAR_PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538, tfUniversal | tfSetFreeze, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to unfreeze a trustline. */ -PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539) +/** Grants the ability to unfreeze a trustline. */ +GRANULAR_PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539, tfUniversal | tfClearFreeze, + ({{sfLimitAmount, SoeRequired}})) -/** This permission grants the delegated account the ability to set Domain. */ -PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540) +/** Grants the ability to set Domain. */ +GRANULAR_PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540, tfUniversal, + ({{sfDomain, SoeOptional}})) -/** This permission grants the delegated account the ability to set EmailHashSet. */ -PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541) +/** Grants the ability to set EmailHash. */ +GRANULAR_PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541, tfUniversal, + ({{sfEmailHash, SoeOptional}})) -/** This permission grants the delegated account the ability to set MessageKey. */ -PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542) +/** Grants the ability to set MessageKey. */ +GRANULAR_PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542, tfUniversal, + ({{sfMessageKey, SoeOptional}})) -/** This permission grants the delegated account the ability to set TransferRate. */ -PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543) +/** Grants the ability to set TransferRate. */ +GRANULAR_PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543, tfUniversal, + ({{sfTransferRate, SoeOptional}})) -/** This permission grants the delegated account the ability to set TickSize. */ -PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544) +/** Grants the ability to set TickSize. */ +GRANULAR_PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544, tfUniversal, + ({{sfTickSize, SoeOptional}})) -/** 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) +/** Grants the ability to mint payment (sending account is the issuer). Cross-currency payments are disallowed. */ +GRANULAR_PERMISSION(PaymentMint, ttPAYMENT, 65545, tfUniversal, + ({{sfDestination, SoeRequired}, + {sfAmount, SoeRequired}, + {sfSendMax, SoeOptional}, + {sfInvoiceID, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}})) -/** 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) +/** Grants the ability to burn payment (destination account is the issuer). Cross-currency payments are disallowed. */ +GRANULAR_PERMISSION(PaymentBurn, ttPAYMENT, 65546, tfUniversal, + ({{sfDestination, SoeRequired}, + {sfAmount, SoeRequired}, + {sfSendMax, SoeOptional}, + {sfInvoiceID, SoeOptional}, + {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}})) -/** This permission grants the delegated account the ability to lock MPToken. */ -PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547) +/** Grants the ability to lock an MPToken. */ +GRANULAR_PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547, tfUniversal | tfMPTLock, + ({{sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}})) -/** This permission grants the delegated account the ability to unlock MPToken. */ -PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548) +/** Grants the ability to unlock an MPToken. */ +GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock, + ({{sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}})) diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index 86b1e856b3..a27d638107 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -222,8 +222,63 @@ public: return tesSUCCESS; } + /** + * This function can be overridden to introduce additional semantic constraints beyond the + * granular template validation for granular permissions. It is called by the base + * invokeCheckPermission method only after the transaction has successfully passed + * checkGranularSandbox. + */ static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) + { + return tesSUCCESS; + } + + /** + * Checks whether the transaction is authorized to be executed by the delegated account. + * This function enforces the strict permission check hierarchy. It is explicitly + * designed NOT to be overridden. Derived transactors must instead implement + * checkGranularSemantics to add custom validation logic for granular permissions. + * + * The evaluation proceeds as follows: + * - If transaction-level permission is granted, the function immediately returns tesSUCCESS. + * - If transaction-level permission is not granted, the function checks whether the transaction + * matches the granular permission template defined in permissions.macro. If it does, it then + * calls checkGranularSemantics to perform any additional, fine-grained validation. + * + */ + template + static NotTEC + invokeCheckPermission(ReadView const& view, STTx const& tx) + { + // heldGranularPermissions is passed by reference into checkPermission. + // It is populated with the sender’s granular permissions only when the sender + // lacks tx-level permission but has granular permissions that satisfy the + // granular permission template. + // + // - result is terNO_DELEGATE_PERMISSION: return immediately. + // - result is tesSUCCESS and heldGranularPermissions is empty: tx-level permission was + // granted, so we returned success before populating it. + // - result is tesSUCCESS and heldGranularPermissions is not empty: tx-level permission was + // not granted, but the held granular permissions passed checkGranularSandbox, so we proceed + // to checkGranularSemantics. + // + // WARNING: Do not simplify checkPermission to return only + // heldGranularPermissions or the ter code. Both the result and the + // populated set are required to enforce the strict permission hierarchy + // described above. + std::unordered_set heldGranularPermissions; + if (NotTEC const result = checkPermission(view, tx, heldGranularPermissions); + !isTesSuccess(result) || heldGranularPermissions.empty()) + { + return result; + } + + return T::checkGranularSemantics(view, tx, heldGranularPermissions); + } ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -353,6 +408,12 @@ protected: unit::ValueUnit min = unit::ValueUnit{}); private: + static NotTEC + checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions); + std::pair reset(XRPAmount fee); diff --git a/include/xrpl/tx/transactors/account/AccountSet.h b/include/xrpl/tx/transactors/account/AccountSet.h index a40a9ec963..91b38e7968 100644 --- a/include/xrpl/tx/transactors/account/AccountSet.h +++ b/include/xrpl/tx/transactors/account/AccountSet.h @@ -23,9 +23,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/payment/Payment.h b/include/xrpl/tx/transactors/payment/Payment.h index 14897b4efe..dd792aa1c2 100644 --- a/include/xrpl/tx/transactors/payment/Payment.h +++ b/include/xrpl/tx/transactors/payment/Payment.h @@ -32,7 +32,10 @@ public: preflight(PreflightContext const& ctx); static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions); static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h index 6a6d1fc445..428c573e2f 100644 --- a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h @@ -22,9 +22,6 @@ public: static NotTEC preflight(PreflightContext const& ctx); - static NotTEC - checkPermission(ReadView const& view, STTx const& tx); - static TER preclaim(PreclaimContext const& ctx); diff --git a/include/xrpl/tx/transactors/token/TrustSet.h b/include/xrpl/tx/transactors/token/TrustSet.h index dcf454bea1..d719f06326 100644 --- a/include/xrpl/tx/transactors/token/TrustSet.h +++ b/include/xrpl/tx/transactors/token/TrustSet.h @@ -21,7 +21,10 @@ public: preflight(PreflightContext const& ctx); static NotTEC - checkPermission(ReadView const& view, STTx const& tx); + checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions); static TER preclaim(PreclaimContext const& ctx); diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp index ce3baeb35e..3aa9705b03 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -1,91 +1,136 @@ #include #include +#include #include -#include // IWYU pragma: keep #include +#include +#include +#include +#include // IWYU pragma: keep #include +#include #include #include #include +#include #include +#include +#include +#include +#include namespace xrpl { +Permission::GranularPermissionEntry::GranularPermissionEntry( + std::string name, + TxType txType, + std::uint32_t permittedFlags, + std::vector permittedFields) + : name(std::move(name)) + , txType(txType) + , permittedFlags(permittedFlags) + , permittedFields(std::move(permittedFields), TxFormats::getCommonFields()) +{ +} + Permission::Permission() { - txFeatureMap_ = { -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, amendment, ...) {value, amendment}, - -#include - -#undef TRANSACTION -#pragma pop_macro("TRANSACTION") - }; - - delegableTx_ = { -#pragma push_macro("TRANSACTION") -#undef TRANSACTION - -#define TRANSACTION(tag, value, name, delegable, ...) {value, delegable}, - -#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") - }; - - XRPL_ASSERT( - txFeatureMap_.size() == delegableTx_.size(), - "xrpl::Permission : txFeatureMap_ and delegableTx_ must have same " - "size"); - - for ([[maybe_unused]] auto const& permission : granularPermissionMap_) { - XRPL_ASSERT( - permission.second > UINT16_MAX, - "xrpl::Permission::granularPermissionMap_ : granular permission " - "value must not exceed the maximum uint16_t value."); +#pragma push_macro("TRANSACTION") +#undef TRANSACTION + +#define TRANSACTION(tag, value, name, delegable, amendment, ...) \ + txDelegationMap_[static_cast(value)] = {amendment, delegable}; + +#include + +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") + } + + granularPermissionsByName_ = { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +#define GRANULAR_PERMISSION(type, ...) {#type, type}, + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + }; + + { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +// NOLINTBEGIN(bugprone-macro-parentheses) +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \ + granularPermissions_.emplace( \ + std::piecewise_construct, \ + std::forward_as_tuple(GranularPermissionType::type), \ + std::forward_as_tuple( \ + #type, txType, static_cast(flags), std::vector fields)); + // NOLINTEND(bugprone-macro-parentheses) + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + } + + if (granularPermissionsByName_.size() != granularPermissions_.size()) + { + // LCOV_EXCL_START + Throw( + "granularPermissionsByName_ and granularPermissions_ must have same size"); + // LCOV_EXCL_STOP + } + + for (auto const& [name, type] : granularPermissionsByName_) + { + if (type <= UINT16_MAX) + { + // LCOV_EXCL_START + Throw( + "Granular permission value must exceed the maximum uint16_t value: " + name); + // LCOV_EXCL_STOP + } + } + + for (auto const& [type, entry] : granularPermissions_) + granularTxTypes_.insert(entry.txType); + + // Validate that all fields listed in permissions.macro exist in the + // corresponding transaction type's format, catching typos at startup. + for (auto const& [type, entry] : granularPermissions_) + { + if (!txDelegationMap_.contains(entry.txType)) + { + // LCOV_EXCL_START + Throw("Invalid granular permission txType in txDelegationMap_"); + // LCOV_EXCL_STOP + } + + auto const* fmt = TxFormats::getInstance().findByType(entry.txType); + if (fmt == nullptr) + { + // LCOV_EXCL_START + Throw("Invalid granular permission txType"); + // LCOV_EXCL_STOP + } + + for (auto const& field : entry.permittedFields) + { + if (fmt->getSOTemplate().getIndex(field.sField()) == -1) + { + // LCOV_EXCL_START + Throw("Invalid granular permission field"); + // LCOV_EXCL_STOP + } + } } } @@ -97,8 +142,11 @@ Permission::getInstance() } std::optional -Permission::getPermissionName(std::uint32_t const value) const +Permission::getPermissionName(std::uint32_t value) const { + if (value == 0) + return std::nullopt; + auto const permissionValue = static_cast(value); if (auto const granular = getGranularName(permissionValue)) return granular; @@ -114,90 +162,131 @@ Permission::getPermissionName(std::uint32_t const value) const std::optional Permission::getGranularValue(std::string const& name) const { - auto const it = granularPermissionMap_.find(name); - if (it != granularPermissionMap_.end()) + auto const it = granularPermissionsByName_.find(name); + if (it != granularPermissionsByName_.end()) return static_cast(it->second); return std::nullopt; } std::optional -Permission::getGranularName(GranularPermissionType const& value) const +Permission::getGranularName(GranularPermissionType value) const { - auto const it = granularNameMap_.find(value); - if (it != granularNameMap_.end()) - return it->second; + auto const it = granularPermissions_.find(value); + if (it != granularPermissions_.end()) + return it->second.name; return std::nullopt; } std::optional -Permission::getGranularTxType(GranularPermissionType const& gpType) const +Permission::getGranularTxType(GranularPermissionType gpType) const { - auto const it = granularTxTypeMap_.find(gpType); - if (it != granularTxTypeMap_.end()) - return it->second; + auto const it = granularPermissions_.find(gpType); + if (it != granularPermissions_.end()) + return it->second.txType; return std::nullopt; } +bool +Permission::hasGranularPermissions(TxType txType) const +{ + return granularTxTypes_.contains(txType); +} + std::optional> Permission::getTxFeature(TxType txType) const { - auto const txFeaturesIt = txFeatureMap_.find(txType); + auto const it = txDelegationMap_.find(txType); XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "xrpl::Permissions::getTxFeature : tx exists in txFeatureMap_"); + it != txDelegationMap_.end(), + "xrpl::Permission::getTxFeature : tx exists in txDelegationMap_"); - if (txFeaturesIt->second == uint256{}) + if (it->second.amendment == uint256{}) return std::nullopt; - return txFeaturesIt->second; + + return std::optional{std::cref(it->second.amendment)}; } bool -Permission::isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const +Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const { - auto const granularPermission = - getGranularName(static_cast(permissionValue)); - if (granularPermission) + if (permissionValue == 0) + return false; // LCOV_EXCL_LINE + + auto const amendmentEnabled = [&rules](TxDelegationEntry const& entry) { + return entry.amendment == uint256{} || rules.enabled(entry.amendment); + }; + + // Granular permissions may authorize a limited subset of a tx type even + // when the full tx type is not delegable. They still require the + // underlying transaction amendment to be enabled. + if (auto const granularIt = + granularPermissions_.find(static_cast(permissionValue)); + granularIt != granularPermissions_.end()) { - // granular permissions are always allowed to be delegated - return true; + auto const txIt = txDelegationMap_.find(granularIt->second.txType); + return txIt != txDelegationMap_.end() && amendmentEnabled(txIt->second); } auto const txType = permissionToTxType(permissionValue); - auto const it = delegableTx_.find(txType); + auto const txIt = txDelegationMap_.find(txType); - if (it == delegableTx_.end()) - return false; - - auto const txFeaturesIt = txFeatureMap_.find(txType); - XRPL_ASSERT( - txFeaturesIt != txFeatureMap_.end(), - "xrpl::Permissions::isDelegable : tx exists in txFeatureMap_"); - - // Delegation is only allowed if the required amendment for the transaction - // is enabled. For transactions that do not require an amendment, delegation - // is always allowed. - if (txFeaturesIt->second != uint256{} && !rules.enabled(txFeaturesIt->second)) - return false; - - if (it->second == Delegation::NotDelegable) - return false; - - return true; + // Tx-level permissions require the transaction type itself to be delegable, and + // the corresponding amendment enabled. + return txIt != txDelegationMap_.end() && txIt->second.delegable != NotDelegable && + amendmentEnabled(txIt->second); } uint32_t -Permission::txToPermissionType(TxType const& type) +Permission::txToPermissionType(TxType const type) { return static_cast(type) + 1; } TxType -Permission::permissionToTxType(uint32_t const& value) +Permission::permissionToTxType(uint32_t value) { + XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0"); return static_cast(value - 1); } +bool +Permission::checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const +{ + // Build union of flags upfront to enable an early exit. Fields are not stored and + // grouped in advance to avoid heap allocation. + std::uint32_t unionFlags = 0; + for (auto const& gp : heldPermissions) + { + auto const it = granularPermissions_.find(gp); + if (it != granularPermissions_.end()) + unionFlags |= it->second.permittedFlags; + } + + // Check if flags are permitted + if ((tx.getFlags() & ~unionFlags) != 0) + return false; + + // Check if fields are permitted. Every present field must appear in at least one held + // permission's template. The common fields are included in the constructor. + for (auto const& field : tx) + { + if (field.getSType() == STI_NOTPRESENT) + continue; + + if (!std::ranges::any_of(heldPermissions, [&](auto const& gp) { + auto const it = granularPermissions_.find(gp); + return it != granularPermissions_.end() && + it->second.permittedFields.getIndex(field.getFName()) != -1; + })) + return false; + } + + return true; +} + } // namespace xrpl diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 55f0ea1289..be3b1a082f 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -217,7 +217,7 @@ STTx::getFeePayer() const { // If sfDelegate is present, the delegate account is the payer // note: if a delegate is specified, its authorization to act on behalf of the account is - // enforced in `Transactor::checkPermission` + // enforced in `Transactor::invokeCheckPermission` // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) if (isFieldPresent(sfDelegate)) return getAccountID(sfDelegate); diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index aa7b81c015..b57d30d2b3 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include @@ -175,6 +177,16 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask) if (ctx.tx[sfDelegate] == ctx.tx[sfAccount]) return temBAD_SIGNER; + + auto const& perm = Permission::getInstance(); + auto const txType = ctx.tx.getTxnType(); + + // If the transaction is not delegable and does not have granular permissions, fail earlier + // with temINVALID. This is to prevent transactions that are not delegable at all from + // being processed further in the invokeCheckPermission function. + if (!perm.isDelegable(Permission::txToPermissionType(txType), ctx.rules) && + !perm.hasGranularPermissions(txType)) + return temINVALID; } if (auto const ret = preflight0(ctx, flagMask)) @@ -295,19 +307,33 @@ Transactor::preflightSigValidated(PreflightContext const& ctx) } NotTEC -Transactor::checkPermission(ReadView const& view, STTx const& tx) +Transactor::checkPermission( + ReadView const& view, + STTx const& tx, + std::unordered_set& heldGranularPermissions) { auto const delegate = tx[~sfDelegate]; if (!delegate) return tesSUCCESS; - auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate); - auto const sle = view.read(delegateKey); - + auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate)); if (!sle) return terNO_DELEGATE_PERMISSION; - return checkTxPermission(sle, tx); + if (isTesSuccess(checkTxPermission(sle, tx))) + return tesSUCCESS; + + if (!Permission::getInstance().hasGranularPermissions(tx.getTxnType())) + return terNO_DELEGATE_PERMISSION; + + heldGranularPermissions = getGranularPermission(sle, tx.getTxnType()); + if (heldGranularPermissions.empty()) + return terNO_DELEGATE_PERMISSION; + + if (!Permission::getInstance().checkGranularSandbox(tx, heldGranularPermissions)) + return terNO_DELEGATE_PERMISSION; + + return tesSUCCESS; } XRPAmount diff --git a/src/libxrpl/tx/applySteps.cpp b/src/libxrpl/tx/applySteps.cpp index 336bb2004b..caaacfd010 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -181,7 +181,8 @@ invokePreclaim(PreclaimContext const& ctx) if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx)) return result; - if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx)) + if (NotTEC const result = + Transactor::invokeCheckPermission(ctx.view, ctx.tx)) return result; if (NotTEC const result = T::checkSign(ctx)) diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp b/src/libxrpl/tx/transactors/account/AccountSet.cpp index bc207b39dc..36a7e7419f 100644 --- a/src/libxrpl/tx/transactors/account/AccountSet.cpp +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -20,13 +19,11 @@ #include #include #include -#include #include #include #include #include -#include namespace xrpl { @@ -168,54 +165,6 @@ AccountSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -AccountSet::checkPermission(ReadView const& view, STTx const& tx) -{ - // AccountSet 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 terNO_DELEGATE_PERMISSION; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttACCOUNT_SET, granularPermissions); - - auto const uSetFlag = tx.getFieldU32(sfSetFlag); - auto const uClearFlag = tx.getFieldU32(sfClearFlag); - // 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 || ((tx.getFlags() & tfUniversalMask) != 0u)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfEmailHash) && !granularPermissions.contains(AccountEmailHashSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfWalletLocator) || tx.isFieldPresent(sfNFTokenMinter)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfMessageKey) && !granularPermissions.contains(AccountMessageKeySet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfDomain) && !granularPermissions.contains(AccountDomainSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfTransferRate) && !granularPermissions.contains(AccountTransferRateSet)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfTickSize) && !granularPermissions.contains(AccountTickSizeSet)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER AccountSet::preclaim(PreclaimContext const& ctx) { diff --git a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp index dc6c98f95e..6def542c7d 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp @@ -29,14 +29,12 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx) return terNO_DELEGATE_PERMISSION; } -void -loadGranularPermission( - SLE::const_ref delegate, - TxType const& txType, - std::unordered_set& granularPermissions) +std::unordered_set +getGranularPermission(SLE::const_ref delegate, TxType const& txType) { + std::unordered_set granularPermissions; if (!delegate) - return; + return granularPermissions; auto const permissionArray = delegate->getFieldArray(sfPermissions); for (auto const& permission : permissionArray) @@ -47,6 +45,8 @@ loadGranularPermission( if (type && *type == txType) granularPermissions.insert(granularValue); } + + return granularPermissions; } } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 805ebe3684..9a9a01ec19 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include @@ -29,7 +28,6 @@ #include #include #include -#include #include #include #include @@ -273,38 +271,24 @@ Payment::preflight(PreflightContext const& ctx) } NotTEC -Payment::checkPermission(ReadView const& view, STTx const& tx) +Payment::checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) { - 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 terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttPAYMENT, granularPermissions); - auto const& dstAmount = tx.getFieldAmount(sfAmount); auto const& amountAsset = dstAmount.asset(); // Granular permissions are only valid for direct payments. - if ((tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) || - tx.isFieldPresent(sfPaths)) + if (tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) return terNO_DELEGATE_PERMISSION; // PaymentMint and PaymentBurn apply to both IOU and MPT direct payments. - if (granularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && + if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) && amountAsset.getIssuer() == tx[sfAccount]) return tesSUCCESS; - if (granularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && + if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) && amountAsset.getIssuer() == tx[sfDestination]) return tesSUCCESS; diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index 9b25531161..683d65b6f9 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -16,14 +15,12 @@ #include #include #include -#include #include #include #include #include #include -#include namespace xrpl { @@ -138,39 +135,6 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx) return tesSUCCESS; } -NotTEC -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 terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - // this is added in case more flags will be added for MPTokenIssuanceSet - // in the future. Currently unreachable. - if ((tx.getFlags() & tfMPTokenIssuanceSetMask) != 0u) - return terNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); - - if (tx.isFlag(tfMPTLock) && !granularPermissions.contains(MPTokenIssuanceLock)) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFlag(tfMPTUnlock) && !granularPermissions.contains(MPTokenIssuanceUnlock)) - return terNO_DELEGATE_PERMISSION; - - return tesSUCCESS; -} - TER MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx) { diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 1d2bc96693..7838b212b2 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -21,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -124,51 +122,21 @@ TrustSet::preflight(PreflightContext const& ctx) } NotTEC -TrustSet::checkPermission(ReadView const& view, STTx const& tx) +TrustSet::checkGranularSemantics( + ReadView const& view, + STTx const& tx, + std::unordered_set const& heldGranularPermissions) { - 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 terNO_DELEGATE_PERMISSION; - - if (isTesSuccess(checkTxPermission(sle, tx))) - return tesSUCCESS; - - // Currently we only support TrustlineAuthorize, TrustlineFreeze and - // TrustlineUnfreeze granular permission. Setting other flags returns - // error. - if ((tx.getFlags() & tfTrustSetPermissionMask) != 0u) - return terNO_DELEGATE_PERMISSION; - - if (tx.isFieldPresent(sfQualityIn) || tx.isFieldPresent(sfQualityOut)) - return terNO_DELEGATE_PERMISSION; - auto const saLimitAmount = tx.getFieldAmount(sfLimitAmount); auto const sleRippleState = view.read( keylet::line( tx[sfAccount], saLimitAmount.getIssuer(), saLimitAmount.get().currency)); - // if the trustline does not exist, granular permissions are - // not allowed to create trustline + // granular permissions are not allowed to create a trustline if (!sleRippleState) return terNO_DELEGATE_PERMISSION; - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttTRUST_SET, granularPermissions); - - if (tx.isFlag(tfSetfAuth) && !granularPermissions.contains(TrustlineAuthorize)) - return terNO_DELEGATE_PERMISSION; - if (tx.isFlag(tfSetFreeze) && !granularPermissions.contains(TrustlineFreeze)) - return terNO_DELEGATE_PERMISSION; - if (tx.isFlag(tfClearFreeze) && !granularPermissions.contains(TrustlineUnfreeze)) - return terNO_DELEGATE_PERMISSION; - - // updating LimitAmount is not allowed only with granular permissions, + // updating LimitAmount is not allowed 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) diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 70b091290c..20668a42bf 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -5,8 +5,11 @@ #include #include #include +#include #include +#include #include +#include #include #include #include @@ -22,7 +25,9 @@ #include #include #include +#include +#include #include #include #include @@ -33,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +47,7 @@ #include #include #include +#include #include #include @@ -1063,6 +1070,93 @@ class Delegate_test : public beast::unit_test::Suite } } + // PaymentMint/PaymentBurn with sfSendMax of the same asset is allowed, + // same-asset SendMax is still a direct payment, not cross-currency. + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + + // sfSendMax with same asset as sfAmount, still a direct payment + env(pay(gw, alice, usd(50)), Sendmax(usd(50)), delegate::As(bob)); + env.require(Balance(alice, usd(50))); + + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + env(pay(alice, gw, usd(30)), Sendmax(usd(30)), delegate::As(bob)); + env.require(Balance(alice, usd(20))); + } + + // Test invalid fields or flags not allowed in granular permission template + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + env(delegate::set(gw, bob, {"PaymentMint"})); + env(delegate::set(alice, bob, {"PaymentBurn"})); + env.close(); + + // sfDeliverMin (with tfPartialPayment) is not in the PaymentMint + // or PaymentBurn template. + env(pay(gw, alice, usd(100)), + DeliverMin(usd(50)), + Txflags(tfPartialPayment), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, usd(50)), + DeliverMin(usd(25)), + Txflags(tfPartialPayment), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + + // sfDomainID is not in the PaymentMint or PaymentBurn template. + env(pay(gw, alice, usd(100)), + Domain(uint256{1}), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + env(pay(alice, gw, usd(50)), + Domain(uint256{1}), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } + + // Delegate account holds no granular permissions for the tx type: + // getGranularPermission returns empty set. + { + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + // Bob holds only an AccountSet granular permission. + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + // Payment has granular permissions defined in permissions.macro, + // but bob only holds AccountSet's granular permission, + // getGranularPermission returns empty. + env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + // PaymentMint and PaymentBurn for MPT { std::string logs; @@ -1119,6 +1213,40 @@ class Delegate_test : public beast::unit_test::Suite BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100)); } } + + // Verify granular permissions of different tx types in the same SLE are scoped + // correctly. AccountSet permissions don't apply to Payment and vice versa + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + auto const usd = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.trust(usd(200), alice); + env.close(); + + // Alice granted bob with both AccountDomainSet and PaymentMint. + env(delegate::set(alice, bob, {"AccountDomainSet", "PaymentMint"})); + env.close(); + + // PaymentMint fails at granular semantic check because alice is not the issuer. + env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // AccountDomainSet applies correctly to AccountSet + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + env(jt); + BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); + + // gw gives bob PaymentMint and bob can mint on gw's behalf + env(delegate::set(gw, bob, {"PaymentMint"})); + env.close(); + env(pay(gw, alice, usd(50)), delegate::As(bob)); + env.require(Balance(alice, usd(50))); + } } void @@ -1301,6 +1429,34 @@ class Delegate_test : public beast::unit_test::Suite env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfFullyCanonicalSig), delegate::As(bob)); } + + { + Env env(*this); + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob)); + env.close(); + + // sfQualityOut is a valid TrustSet field, but not permitted in granular template + json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth); + txJson[sfQualityOut.jsonName] = 100; + env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // tfSetNoRipple is a valid flag for TrustSet, but not permitted in granular template + env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfSetNoRipple), + delegate::As(bob), + Ter(terNO_DELEGATE_PERMISSION)); + } } void @@ -1456,7 +1612,9 @@ class Delegate_test : public beast::unit_test::Suite env(jv2, Ter(terNO_DELEGATE_PERMISSION)); } - // can not set AccountSet flags on behalf of other account + // can not set AccountSet flags on behalf of other account, + // in permissions.macro, the template for AccountSet does + // not allow any flag set or clear. { Env env(*this); auto const alice = Account{"alice"}; @@ -1552,6 +1710,71 @@ class Delegate_test : public beast::unit_test::Suite env(jt); BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); } + + // setting invalid field not in permissions.macro template will be rejected. + { + Env env(*this); + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // Alice gives Bob permission to set her Domain + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + std::string const domain = "example.com"; + auto txJson = noop(alice); + txJson[sfDomain] = strHex(domain); + txJson[sfDelegate] = bob.human(); + + // sfNFTokenMinter is a valid field in AccountSet tx, but + // it is not permitted for granular template + txJson[sfNFTokenMinter] = bob.human(); + + env(txJson, Ter(terNO_DELEGATE_PERMISSION)); + } + + // Delegated AccountSet with no fields and no flags is allowed, + // because it is allowed in the non-delegated case as well. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + auto jt = noop(alice); + jt[sfDelegate] = bob.human(); + env(jt); + } + + // Revoking all permissions deletes the SLE and subsequent attempts are rejected. + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + std::string const domain = "example.com"; + auto jt = noop(alice); + jt[sfDomain] = strHex(domain); + jt[sfDelegate] = bob.human(); + env(jt); + + // empty DelegateSet deletes the SLE + env(delegate::set(alice, bob, {})); + env.close(); + + env(jt, Ter(terNO_DELEGATE_PERMISSION)); + } } void @@ -1672,6 +1895,37 @@ class Delegate_test : public beast::unit_test::Suite env.close(); mpt.set({.account = alice, .flags = tfMPTLock | tfFullyCanonicalSig, .delegate = bob}); } + + // field not permitted to exist in granular delegation + { + Env env(*this); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(100000), alice, bob); + + MPTTester mpt(env, alice, {.fund = false}); + mpt.create({.flags = tfMPTCanLock}); + env.close(); + + // alice gives granular permission to bob for MPTokenIssuanceLock + env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); + env.close(); + + // Field is not permitted, permitted fields for delegation is defined in + // permissions.macro. + mpt.set( + {.account = alice, + .mutableFlags = 2, + .delegate = bob, + .err = terNO_DELEGATE_PERMISSION}); + + // Notice: flags not defined in permissions.macro are not permitted for delegation. + // Since preflight will check invalid flag for the tx, it is not reachable. + // If any new flag is defined into the transaction in the future, + // but is not allowed for delegation, the transaction will be rejected with + // terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in + // permissions.macro. + } } void @@ -2141,6 +2395,62 @@ class Delegate_test : public beast::unit_test::Suite for (auto const& tx : txRequiredFeatures) txAmendmentEnabled(tx.first); } + + // Granular permissions also require the amendment for their underlying + // transaction type. + { + for (auto const permission : {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"}) + { + Env env(*this, features - featureMPTokensV1); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(100000), alice, bob); + env.close(); + + env(delegate::set(alice, bob, {permission}), Ter(temMALFORMED)); + } + } + } + + void + testGranularSandboxCheckOrder() + { + testcase("Make sure GranularSandbox is checked after transaction-level permission"); + + using namespace jtx; + + Env env(*this); + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), gw, alice, bob); + + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(alice, gw["USD"](50))); + env.close(); + env(delegate::set(gw, bob, {"TrustlineAuthorize"})); + env.close(); + + env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob)); + env.close(); + + // sfQualityOut is a valid TrustSet field, but not permitted in granular template + json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth); + txJson[sfQualityOut.jsonName] = 100; + env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + + // Now Alice grants Bob with transaction level permission + env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustSet"})); + env.close(); + + // NOTE: This case is to ensure that if a delegate possesses a + // transaction-level permission (e.g., TrustSet), the granular sandbox must not incorrectly + // block the transaction. The function checkGranularSandbox MUST be called after the + // transaction-level permission check. This test case is to avoid future refactor mistakes, + // modifying the order will fail here. + env(txJson, delegate::As(bob)); } void @@ -2193,6 +2503,94 @@ class Delegate_test : public beast::unit_test::Suite "\n Action: Verify security requirements to interact with Delegation feature"); } + void + testNonDelegableTxWithDelegate(FeatureBitset features) + { + testcase("non-delegable tx with sfDelegate is rejected at preflight"); + using namespace jtx; + + Env env(*this, features); + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + // Transactions that are notDelegable and have no granular permissions + // will be rejected with temINVALID at preflight. + // Note: pseudo-transactions (EnableAmendment, SetFee and UNLModify) are also + // notDelegable but are excluded here — passesLocalChecks() blocks them + // before preflight1 is ever reached. + { + // SetRegularKey, SignerListSet, AccountDelete, DelegateSet. + env(regkey(alice, bob), delegate::As(bob), Ter(temINVALID)); + env(signers(alice, 1, {{bob, 1}}), delegate::As(bob), Ter(temINVALID)); + env(acctdelete(alice, bob), delegate::As(bob), Ter(temINVALID)); + env(delegate::set(alice, bob, {"Payment"}), delegate::As(bob), Ter(temINVALID)); + + // SAV transactions. + { + Vault const vault{env}; + auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(createTx, delegate::As(bob), Ter(temINVALID)); + + env(vault.set({.owner = alice, .id = keylet.key}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.del({.owner = alice, .id = keylet.key}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(1)}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(1)}), + delegate::As(bob), + Ter(temINVALID)); + env(vault.clawback({.issuer = alice, .id = keylet.key, .holder = bob}), + delegate::As(bob), + Ter(temINVALID)); + } + + // Batch transaction: the outer Batch itself is non-delegable. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 1); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::Inner(pay(alice, bob, XRP(1)), seq + 1), + delegate::As(bob), + Ter(temINVALID)); + } + + // Lending protocol transactions + { + Vault const vault{env}; + auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()}); + env(createTx); + + env(loanBroker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loanBroker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loanBroker::coverDeposit(alice, keylet.key, XRP(1)), + delegate::As(bob), + Ter(temINVALID)); + env(loanBroker::coverWithdraw(alice, keylet.key, XRP(1)), + delegate::As(bob), + Ter(temINVALID)); + env(loanBroker::coverClawback(alice), delegate::As(bob), Ter(temINVALID)); + + env(loan::set(alice, keylet.key, Number(100)), delegate::As(bob), Ter(temINVALID)); + env(loan::manage(alice, keylet.key, 0), delegate::As(bob), Ter(temINVALID)); + env(loan::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID)); + env(loan::pay(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID)); + } + } + + // AccountSet is notDelegable at tx level but has granular permissions, + // so sfDelegate passes preflight and is rejected at invokeCheckPermission with + // terNO_DELEGATE_PERMISSION. + { + env(fset(alice, asfDefaultRipple), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION)); + } + } + void testDelegateUtilsNullptrCheck() { @@ -2202,9 +2600,8 @@ class Delegate_test : public beast::unit_test::Suite STTx const tx{ttPAYMENT, [](STObject&) {}}; BEAST_EXPECT(checkTxPermission(nullptr, tx) == terNO_DELEGATE_PERMISSION); - // loadGranularPermission nullptr check - std::unordered_set granularPermissions; - loadGranularPermission(nullptr, ttPAYMENT, granularPermissions); + // getGranularPermission nullptr check + auto const granularPermissions = getGranularPermission(nullptr, ttPAYMENT); BEAST_EXPECT(granularPermissions.empty()); } @@ -2234,7 +2631,9 @@ class Delegate_test : public beast::unit_test::Suite testSignForDelegated(); testPermissionValue(all); testTxRequireFeatures(all); + testGranularSandboxCheckOrder(); testTxDelegableCount(); + testNonDelegableTxWithDelegate(all); testDelegateUtilsNullptrCheck(); } };