diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h b/include/xrpl/ledger/helpers/DelegateHelpers.h index 78ccc46d0b..82e55f8ee2 100644 --- a/include/xrpl/ledger/helpers/DelegateHelpers.h +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h @@ -26,10 +26,7 @@ checkTxPermission(std::shared_ptr const& delegate, STTx const& tx); * @param granularPermissions Granted granular permissions tied to the * transaction type. */ -void -loadGranularPermission( - std::shared_ptr const& delegate, - TxType const& type, - std::unordered_set& granularPermissions); +std::unordered_set +getGranularPermission(std::shared_ptr const& delegate, TxType const& type); } // namespace xrpl diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 0ec4f04f1a..764a3c6b1a 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -7,8 +7,12 @@ #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 @@ -17,15 +21,15 @@ namespace xrpl { * greater than the maximum value of uint16. */ 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, allowedFlags, allowedFields) name = value, #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; enum Delegation { delegable, notDelegable }; @@ -36,15 +40,16 @@ private: Permission(); std::unordered_map txFeatureMap_; - std::unordered_map delegableTx_; - std::unordered_map granularPermissionMap_; std::unordered_map granularNameMap_; - std::unordered_map granularTxTypeMap_; + std::unordered_map granularPermittedFlags_; + std::unordered_map granularTemplates_; + std::unordered_set granularTxTypes_; + public: static Permission const& getInstance(); @@ -71,6 +76,9 @@ public: bool isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const; + 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); @@ -78,6 +86,23 @@ public: // tx type value is permission value minus one static TxType permissionToTxType(uint32_t const& 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/permissions.macro b/include/xrpl/protocol/detail/permissions.macro index 729861a013..d85d5fe55c 100644 --- a/include/xrpl/protocol/detail/permissions.macro +++ b/include/xrpl/protocol/detail/permissions.macro @@ -1,49 +1,80 @@ -#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 | tfPartialPayment | tfLimitQuality | tfNoRippleDirect, + ({{sfDestination, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfSendMax, soeOPTIONAL}, + {sfInvoiceID, soeOPTIONAL}, + {sfDestinationTag, soeOPTIONAL}, + {sfDeliverMin, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, + {sfDomainID, 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 | tfPartialPayment | tfLimitQuality | tfNoRippleDirect, + ({{sfDestination, soeREQUIRED}, + {sfAmount, soeREQUIRED}, + {sfSendMax, soeOPTIONAL}, + {sfInvoiceID, soeOPTIONAL}, + {sfDestinationTag, soeOPTIONAL}, + {sfDeliverMin, soeOPTIONAL}, + {sfCredentialIDs, soeOPTIONAL}, + {sfDomainID, 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 287f785cd7..7acd7affc9 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -206,8 +206,47 @@ 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 + * checkPermission 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 + checkPermission(ReadView const& view, STTx const& tx) + { + std::unordered_set heldGranularPermissions; + if (NotTEC const result = checkPermissionImpl(view, tx, heldGranularPermissions); + !isTesSuccess(result) || heldGranularPermissions.empty()) + { + return result; + } + + return T::checkGranularSemantics(view, tx, heldGranularPermissions); + } ///////////////////////////////////////////////////// // Interface used by AccountDelete @@ -294,6 +333,12 @@ protected: unit::ValueUnit min = unit::ValueUnit{}); private: + static NotTEC + checkPermissionImpl( + 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 0940ab0739..708bfa55b8 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 bc5bc4fee3..dc33de54cb 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 dccd4e4cee..dbb76285fa 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 2e67aaeded..4f1f55d3b4 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 686b6376c1..ebb7f6fed9 100644 --- a/src/libxrpl/protocol/Permissions.cpp +++ b/src/libxrpl/protocol/Permissions.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include namespace xrpl { @@ -32,41 +34,70 @@ Permission::Permission() }; granularPermissionMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION -#define PERMISSION(type, txType, value) {#type, type}, +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) {#type, type}, #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; granularNameMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION -#define PERMISSION(type, txType, value) {type, #type}, +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) {type, #type}, #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; granularTxTypeMap_ = { -#pragma push_macro("PERMISSION") -#undef PERMISSION +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION -#define PERMISSION(type, txType, value) {type, txType}, +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) {type, txType}, #include -#undef PERMISSION -#pragma pop_macro("PERMISSION") +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") }; + { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \ + granularPermittedFlags_[type] = (flags); + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + } + + { +#pragma push_macro("GRANULAR_PERMISSION") +#undef GRANULAR_PERMISSION + +#define GRANULAR_PERMISSION(type, txType, value, flags, fields) \ + granularTemplates_.emplace( \ + std::piecewise_construct, \ + std::forward_as_tuple(type), \ + std::forward_as_tuple(std::vector fields, TxFormats::getCommonFields())); + +#include + +#undef GRANULAR_PERMISSION +#pragma pop_macro("GRANULAR_PERMISSION") + } + XRPL_ASSERT( txFeatureMap_.size() == delegableTx_.size(), "xrpl::Permission : txFeatureMap_ and delegableTx_ must have same " @@ -79,6 +110,9 @@ Permission::Permission() "xrpl::Permission::granularPermissionMap_ : granular permission " "value must not exceed the maximum uint16_t value."); } + + for (auto const& [gp, txType] : granularTxTypeMap_) + granularTxTypes_.insert(txType); } Permission const& @@ -133,6 +167,12 @@ Permission::getGranularTxType(GranularPermissionType const& gpType) const return std::nullopt; } +bool +Permission::hasGranularPermissions(TxType txType) const +{ + return granularTxTypes_.contains(txType); +} + std::optional> Permission::getTxFeature(TxType txType) const { @@ -192,4 +232,48 @@ Permission::permissionToTxType(uint32_t const& value) return static_cast(value - 1); } +bool +Permission::checkGranularSandbox( + STTx const& tx, + std::unordered_set const& heldPermissions) const +{ + auto const txFlags = tx.getFlags(); + + // Build union of flags and templates across all held permissions. + std::uint32_t unionFlags = 0; + for (auto const& gp : heldPermissions) + { + auto const it = granularPermittedFlags_.find(gp); + if (it != granularPermittedFlags_.end()) + unionFlags |= it->second; + } + + // Check if flags are permitted + if ((txFlags & ~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; + + bool fieldAllowed = false; + for (auto const& gp : heldPermissions) + { + auto const it = granularTemplates_.find(gp); + if (it != granularTemplates_.end() && it->second.getIndex(field.getFName()) != -1) + { + fieldAllowed = true; + break; + } + } + if (!fieldAllowed) + return false; + } + + return true; +} + } // namespace xrpl diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 5010763afd..0af3cf9fb4 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -256,19 +256,33 @@ Transactor::preflightSigValidated(PreflightContext const& ctx) } NotTEC -Transactor::checkPermission(ReadView const& view, STTx const& tx) +Transactor::checkPermissionImpl( + 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 b688377541..e329c3fc39 100644 --- a/src/libxrpl/tx/applySteps.cpp +++ b/src/libxrpl/tx/applySteps.cpp @@ -174,7 +174,7 @@ invoke_preclaim(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::checkPermission(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 093019766c..856fe638f2 100644 --- a/src/libxrpl/tx/transactors/account/AccountSet.cpp +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp @@ -155,55 +155,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); - 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 & 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 f5e92bf06d..987d54dedf 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp @@ -21,14 +21,12 @@ checkTxPermission(std::shared_ptr const& delegate, STTx const& tx) return terNO_DELEGATE_PERMISSION; } -void -loadGranularPermission( - std::shared_ptr const& delegate, - TxType const& txType, - std::unordered_set& granularPermissions) +std::unordered_set +getGranularPermission(std::shared_ptr const& delegate, TxType const& txType) { + std::unordered_set granularPermissions; if (!delegate) - return; + return granularPermissions; // LCOV_EXCL_LINE auto const permissionArray = delegate->getFieldArray(sfPermissions); for (auto const& permission : permissionArray) @@ -39,6 +37,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 3a64b5c739..b5f83b5c39 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -239,38 +239,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 9b4d288de8..1cb0b18886 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -151,41 +151,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; - - auto const txFlags = tx.getFlags(); - - // this is added in case more flags will be added for MPTokenIssuanceSet - // in the future. Currently unreachable. - if ((txFlags & tfMPTokenIssuanceSetMask) != 0u) - return terNO_DELEGATE_PERMISSION; // LCOV_EXCL_LINE - - std::unordered_set granularPermissions; - loadGranularPermission(sle, ttMPTOKEN_ISSUANCE_SET, granularPermissions); - - if (((txFlags & tfMPTLock) != 0u) && !granularPermissions.contains(MPTokenIssuanceLock)) - return terNO_DELEGATE_PERMISSION; - - if (((txFlags & tfMPTUnlock) != 0u) && !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 13c444af8b..761f249785 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -107,53 +107,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; - - 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) != 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 (((txFlags & tfSetfAuth) != 0u) && !granularPermissions.contains(TrustlineAuthorize)) - return terNO_DELEGATE_PERMISSION; - if (((txFlags & tfSetFreeze) != 0u) && !granularPermissions.contains(TrustlineFreeze)) - return terNO_DELEGATE_PERMISSION; - if (((txFlags & tfClearFreeze) != 0u) && !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 0710ddf2d1..c94ec8d697 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -870,6 +870,54 @@ 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))); + } + + // 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; @@ -926,6 +974,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 @@ -1108,6 +1190,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 @@ -1263,7 +1373,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"}; @@ -1359,6 +1471,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 @@ -1479,6 +1656,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 @@ -1807,6 +2015,46 @@ class Delegate_test : public beast::unit_test::suite } } + 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 testTxDelegableCount() { @@ -1866,9 +2114,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()); } @@ -1896,6 +2143,7 @@ class Delegate_test : public beast::unit_test::suite testMultiSignQuorumNotMet(); testPermissionValue(all); testTxRequireFeatures(all); + testGranularSandboxCheckOrder(); testTxDelegableCount(); testDelegateUtilsNullptrCheck(); }