Compare commits

..

8 Commits

Author SHA1 Message Date
Bart
801c4b95a7 Merge branch 'develop' into bthomee/jq 2026-06-17 10:17:41 -04:00
Bart
8f67a2a1cc Merge branch 'develop' into bthomee/jq 2026-06-10 17:22:32 -04:00
Bart
77aa8715f0 Improve log message 2026-06-06 19:06:31 -04:00
Bart
e67a980849 Improve comment 2026-06-06 18:56:19 -04:00
Bart
5771c38406 Limit number of requested nodes to handle 2026-06-06 18:52:14 -04:00
Bart
ae1b5b6bac Post charge to peer strand 2026-06-06 17:21:19 -04:00
Bart
ecd0136844 Improve log message 2026-06-06 09:29:26 -04:00
Bart
02f20331d5 refactor: Deserialize received nodes once and only in job queue 2026-06-06 09:17:26 -04:00
38 changed files with 409 additions and 941 deletions

View File

@@ -20,6 +20,8 @@ _SANITIZER_SUFFIX: dict[str, str] = {
def get_cmake_args(build_type: str, extra_args: str) -> str:
"""Get the full list of CMake arguments for a config."""
args = _BASE_CMAKE_ARGS.copy()
if build_type == "Release":
args.append("-Dassert=ON")
if extra_args:
args.extend(extra_args.split())
return " ".join(args)

View File

@@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Write PR body to file
env:

View File

@@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Determine changed files
# This step checks whether any files have changed that should
# cause the next jobs to run. We do it this way rather than

View File

@@ -44,7 +44,7 @@ jobs:
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8

View File

@@ -110,7 +110,7 @@ jobs:
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Check levelization
run: python .github/scripts/levelization/generate.py
- name: Check for differences

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Check definitions
run: .github/scripts/rename/definitions.sh .
- name: Check copyright notices

View File

@@ -45,7 +45,7 @@ jobs:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8

View File

@@ -27,7 +27,7 @@ jobs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -45,7 +45,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
sparse-checkout: |
.github/actions/generate-version
@@ -69,7 +69,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Download pre-built binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

View File

@@ -23,7 +23,7 @@ jobs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0

View File

@@ -43,7 +43,7 @@ jobs:
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-fe4c8ae
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Generate build version number
id: version

View File

@@ -65,7 +65,7 @@ jobs:
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Prepare runner
uses: XRPLF/actions/prepare-runner@c47daebb2f9db64ffbac71b47d68a661498d5ce8

View File

@@ -4,7 +4,7 @@
/*
ASAN flags some false positives with sudden jumps in control flow, like
exceptions, or when encountering coroutine stack switches. This macro can be used to disable ASAN
instrumentation for specific functions.
intrumentation for specific functions.
*/
#if defined(__GNUC__) || defined(__clang__)
#define XRPL_NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address", "hwaddress")))

View File

@@ -36,13 +36,13 @@ checkFields(STTx const& tx, beast::Journal j);
TER
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j);
// Check if subject has any credential matching the given domain. If you call it
// Check if subject has any credential maching the given domain. If you call it
// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in
// doApply. This will ensure that expired credentials are deleted.
TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject);
// This function is only called when we are about to return tecNO_PERMISSION
// This function is only called when we about to return tecNO_PERMISSION
// because all the checks for the DepositPreauth authorization failed.
TER
authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst);
@@ -58,7 +58,7 @@ checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
} // namespace credentials
// Check expired credentials and for credentials matching DomainID of the ledger
// Check expired credentials and for credentials maching DomainID of the ledger
// object
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);

View File

@@ -23,9 +23,13 @@ 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.
* @return the granted granular permissions tied to the transaction type.
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
*/
std::unordered_set<GranularPermissionType>
getGranularPermission(SLE::const_ref delegate, TxType const& type);
void
loadGranularPermission(
SLE::const_ref delegate,
TxType const& type,
std::unordered_set<GranularPermissionType>& granularPermissions);
} // namespace xrpl

View File

@@ -7,13 +7,8 @@
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace xrpl {
class STTx;
/**
* We have both transaction type permissions and granular type permissions.
* Since we will reuse the TransactionFormats to parse the Transaction
@@ -24,15 +19,15 @@ class STTx;
// Macro-generated, complex
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum GranularPermissionType : std::uint32_t {
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define GRANULAR_PERMISSION(name, txType, value, ...) name = (value),
#define PERMISSION(type, txType, value) type = (value),
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor
@@ -45,30 +40,15 @@ class Permission
private:
Permission();
struct GranularPermissionEntry
{
std::string name;
TxType txType;
std::uint32_t permittedFlags;
SOTemplate permittedFields;
std::unordered_map<std::uint16_t, uint256> txFeatureMap_;
GranularPermissionEntry(
std::string name,
TxType txType,
std::uint32_t permittedFlags,
std::vector<SOElement> fields);
};
std::unordered_map<std::uint16_t, Delegation> delegableTx_;
struct TxDelegationEntry
{
uint256 amendment;
Delegation delegable{NotDelegable};
};
std::unordered_map<std::string, GranularPermissionType> granularPermissionMap_;
std::unordered_set<TxType> granularTxTypes_;
std::unordered_map<TxType, TxDelegationEntry> txDelegationMap_;
std::unordered_map<std::string, GranularPermissionType> granularPermissionsByName_;
std::unordered_map<GranularPermissionType, GranularPermissionEntry> granularPermissions_;
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
public:
static Permission const&
@@ -79,52 +59,30 @@ public:
operator=(Permission const&) = delete;
[[nodiscard]] std::optional<std::string>
getPermissionName(std::uint32_t value) const;
getPermissionName(std::uint32_t const value) const;
[[nodiscard]] std::optional<std::uint32_t>
getGranularValue(std::string const& name) const;
[[nodiscard]] std::optional<std::string>
getGranularName(GranularPermissionType value) const;
getGranularName(GranularPermissionType const& value) const;
[[nodiscard]] std::optional<TxType>
getGranularTxType(GranularPermissionType gpType) const;
getGranularTxType(GranularPermissionType const& 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<std::reference_wrapper<uint256 const>>
getTxFeature(TxType txType) const;
[[nodiscard]] bool
isDelegable(std::uint32_t permissionValue, Rules const& rules) const;
[[nodiscard]] bool
hasGranularPermissions(TxType txType) const;
isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const;
// for tx level permission, permission value is equal to tx type plus one
[[nodiscard]] static uint32_t
txToPermissionType(TxType type);
static uint32_t
txToPermissionType(TxType const& type);
// tx type value is permission value minus one
[[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<GranularPermissionType> const& heldPermissions) const;
static TxType
permissionToTxType(uint32_t const& value);
};
} // namespace xrpl

View File

@@ -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::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (IncludeKeyletFields, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicMPT, Supported::No, VoteBehavior::DefaultNo)

View File

@@ -1,74 +1,49 @@
#if !defined(GRANULAR_PERMISSION)
#error "undefined macro: GRANULAR_PERMISSION"
#if !defined(PERMISSION)
#error "undefined macro: PERMISSION"
#endif
/**
* GRANULAR_PERMISSION(name, txType, value, allowedFlags, allowedFields)
* PERMISSION(name, type, txType, value)
*
* 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 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.
*/
/** 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 authorize a trustline. */
PERMISSION(TrustlineAuthorize, ttTRUST_SET, 65537)
/** 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 freeze a trustline. */
PERMISSION(TrustlineFreeze, ttTRUST_SET, 65538)
/** 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 unfreeze a trustline. */
PERMISSION(TrustlineUnfreeze, ttTRUST_SET, 65539)
/** 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 Domain. */
PERMISSION(AccountDomainSet, ttACCOUNT_SET, 65540)
/** 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 EmailHashSet. */
PERMISSION(AccountEmailHashSet, ttACCOUNT_SET, 65541)
/** 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 MessageKey. */
PERMISSION(AccountMessageKeySet, ttACCOUNT_SET, 65542)
/** 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 TransferRate. */
PERMISSION(AccountTransferRateSet, ttACCOUNT_SET, 65543)
/** Grants the ability to set TickSize. */
GRANULAR_PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544, tfUniversal,
({{sfTickSize, SoeOptional}}))
/** This permission grants the delegated account the ability to set TickSize. */
PERMISSION(AccountTickSizeSet, ttACCOUNT_SET, 65544)
/** 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 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 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 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 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 lock MPToken. */
PERMISSION(MPTokenIssuanceLock, ttMPTOKEN_ISSUANCE_SET, 65547)
/** Grants the ability to unlock an MPToken. */
GRANULAR_PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548, tfUniversal | tfMPTUnlock,
({{sfMPTokenIssuanceID, SoeRequired},
{sfHolder, SoeOptional}}))
/** This permission grants the delegated account the ability to unlock MPToken. */
PERMISSION(MPTokenIssuanceUnlock, ttMPTOKEN_ISSUANCE_SET, 65548)

View File

@@ -222,63 +222,8 @@ 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
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> 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 <class T>
static NotTEC
invokeCheckPermission(ReadView const& view, STTx const& tx)
{
// heldGranularPermissions is passed by reference into checkPermission.
// It is populated with the senders 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<GranularPermissionType> heldGranularPermissions;
if (NotTEC const result = checkPermission(view, tx, heldGranularPermissions);
!isTesSuccess(result) || heldGranularPermissions.empty())
{
return result;
}
return T::checkGranularSemantics(view, tx, heldGranularPermissions);
}
checkPermission(ReadView const& view, STTx const& tx);
/////////////////////////////////////////////////////
// Interface used by AccountDelete
@@ -408,12 +353,6 @@ protected:
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private:
static NotTEC
checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions);
std::pair<TER, XRPAmount>
reset(XRPAmount fee);

View File

@@ -23,6 +23,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -32,10 +32,7 @@ public:
preflight(PreflightContext const& ctx);
static NotTEC
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions);
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -22,6 +22,9 @@ public:
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -21,10 +21,7 @@ public:
preflight(PreflightContext const& ctx);
static NotTEC
checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions);
checkPermission(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);

View File

@@ -22,7 +22,6 @@ in
git-lfs
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
llvmPackages_22.clang-tools
less # needed for git diff
mold

View File

@@ -17,10 +17,6 @@ ProtectHome=true
PrivateTmp=true
User=xrpld
Group=xrpld
StateDirectory=xrpld
StateDirectoryMode=0750
LogsDirectory=xrpld
LogsDirectoryMode=0750
LimitNOFILE=65536
SystemCallArchitectures=native

View File

@@ -1,136 +1,91 @@
#include <xrpl/protocol/Permissions.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/Feature.h> // IWYU pragma: keep
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TxFlags.h> // IWYU pragma: keep
#include <xrpl/protocol/TxFormats.h>
#include <algorithm>
#include <cstdint>
#include <functional>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
namespace xrpl {
Permission::GranularPermissionEntry::GranularPermissionEntry(
std::string name,
TxType txType,
std::uint32_t permittedFlags,
std::vector<SOElement> 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, ...) \
txDelegationMap_[static_cast<TxType>(value)] = {amendment, delegable};
#define TRANSACTION(tag, value, name, delegable, amendment, ...) {value, amendment},
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
}
granularPermissionsByName_ = {
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
#define GRANULAR_PERMISSION(type, ...) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
};
{
#pragma push_macro("GRANULAR_PERMISSION")
#undef GRANULAR_PERMISSION
delegableTx_ = {
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
// 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<std::uint32_t>(flags), std::vector<SOElement> fields));
// NOLINTEND(bugprone-macro-parentheses)
#define TRANSACTION(tag, value, name, delegable, ...) {value, delegable},
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
};
granularPermissionMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {#type, type},
#include <xrpl/protocol/detail/permissions.macro>
#undef GRANULAR_PERMISSION
#pragma pop_macro("GRANULAR_PERMISSION")
}
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
if (granularPermissionsByName_.size() != granularPermissions_.size())
granularNameMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, #type},
#include <xrpl/protocol/detail/permissions.macro>
#undef PERMISSION
#pragma pop_macro("PERMISSION")
};
granularTxTypeMap_ = {
#pragma push_macro("PERMISSION")
#undef PERMISSION
#define PERMISSION(type, txType, value) {type, txType},
#include <xrpl/protocol/detail/permissions.macro>
#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_)
{
// LCOV_EXCL_START
Throw<std::logic_error>(
"granularPermissionsByName_ and granularPermissions_ must have same size");
// LCOV_EXCL_STOP
}
for (auto const& [name, type] : granularPermissionsByName_)
{
if (type <= UINT16_MAX)
{
// LCOV_EXCL_START
Throw<std::logic_error>(
"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<std::logic_error>("Invalid granular permission txType in txDelegationMap_");
// LCOV_EXCL_STOP
}
auto const* fmt = TxFormats::getInstance().findByType(entry.txType);
if (fmt == nullptr)
{
// LCOV_EXCL_START
Throw<std::logic_error>("Invalid granular permission txType");
// LCOV_EXCL_STOP
}
for (auto const& field : entry.permittedFields)
{
if (fmt->getSOTemplate().getIndex(field.sField()) == -1)
{
// LCOV_EXCL_START
Throw<std::logic_error>("Invalid granular permission field");
// LCOV_EXCL_STOP
}
}
XRPL_ASSERT(
permission.second > UINT16_MAX,
"xrpl::Permission::granularPermissionMap_ : granular permission "
"value must not exceed the maximum uint16_t value.");
}
}
@@ -142,11 +97,8 @@ Permission::getInstance()
}
std::optional<std::string>
Permission::getPermissionName(std::uint32_t value) const
Permission::getPermissionName(std::uint32_t const value) const
{
if (value == 0)
return std::nullopt;
auto const permissionValue = static_cast<GranularPermissionType>(value);
if (auto const granular = getGranularName(permissionValue))
return granular;
@@ -162,131 +114,90 @@ Permission::getPermissionName(std::uint32_t value) const
std::optional<std::uint32_t>
Permission::getGranularValue(std::string const& name) const
{
auto const it = granularPermissionsByName_.find(name);
if (it != granularPermissionsByName_.end())
auto const it = granularPermissionMap_.find(name);
if (it != granularPermissionMap_.end())
return static_cast<uint32_t>(it->second);
return std::nullopt;
}
std::optional<std::string>
Permission::getGranularName(GranularPermissionType value) const
Permission::getGranularName(GranularPermissionType const& value) const
{
auto const it = granularPermissions_.find(value);
if (it != granularPermissions_.end())
return it->second.name;
auto const it = granularNameMap_.find(value);
if (it != granularNameMap_.end())
return it->second;
return std::nullopt;
}
std::optional<TxType>
Permission::getGranularTxType(GranularPermissionType gpType) const
Permission::getGranularTxType(GranularPermissionType const& gpType) const
{
auto const it = granularPermissions_.find(gpType);
if (it != granularPermissions_.end())
return it->second.txType;
auto const it = granularTxTypeMap_.find(gpType);
if (it != granularTxTypeMap_.end())
return it->second;
return std::nullopt;
}
bool
Permission::hasGranularPermissions(TxType txType) const
{
return granularTxTypes_.contains(txType);
}
std::optional<std::reference_wrapper<uint256 const>>
Permission::getTxFeature(TxType txType) const
{
auto const it = txDelegationMap_.find(txType);
auto const txFeaturesIt = txFeatureMap_.find(txType);
XRPL_ASSERT(
it != txDelegationMap_.end(),
"xrpl::Permission::getTxFeature : tx exists in txDelegationMap_");
txFeaturesIt != txFeatureMap_.end(),
"xrpl::Permissions::getTxFeature : tx exists in txFeatureMap_");
if (it->second.amendment == uint256{})
if (txFeaturesIt->second == uint256{})
return std::nullopt;
return std::optional{std::cref(it->second.amendment)};
return txFeaturesIt->second;
}
bool
Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const
Permission::isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const
{
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<GranularPermissionType>(permissionValue));
granularIt != granularPermissions_.end())
auto const granularPermission =
getGranularName(static_cast<GranularPermissionType>(permissionValue));
if (granularPermission)
{
auto const txIt = txDelegationMap_.find(granularIt->second.txType);
return txIt != txDelegationMap_.end() && amendmentEnabled(txIt->second);
// granular permissions are always allowed to be delegated
return true;
}
auto const txType = permissionToTxType(permissionValue);
auto const txIt = txDelegationMap_.find(txType);
auto const it = delegableTx_.find(txType);
// 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);
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;
}
uint32_t
Permission::txToPermissionType(TxType const type)
Permission::txToPermissionType(TxType const& type)
{
return static_cast<uint32_t>(type) + 1;
}
TxType
Permission::permissionToTxType(uint32_t value)
Permission::permissionToTxType(uint32_t const& value)
{
XRPL_ASSERT(value > 0, "xrpl::Permission::permissionToTxType : value is greater than 0");
return static_cast<TxType>(value - 1);
}
bool
Permission::checkGranularSandbox(
STTx const& tx,
std::unordered_set<GranularPermissionType> 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

View File

@@ -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::invokeCheckPermission`
// enforced in `Transactor::checkPermission`
// cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`)
if (isFieldPresent(sfDelegate))
return getAccountID(sfDelegate);

View File

@@ -22,7 +22,6 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rules.h>
@@ -47,7 +46,6 @@
#include <exception>
#include <optional>
#include <stdexcept>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -177,16 +175,6 @@ 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))
@@ -307,33 +295,19 @@ Transactor::preflightSigValidated(PreflightContext const& ctx)
}
NotTEC
Transactor::checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions)
Transactor::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const sle = view.read(keylet::delegate(tx[sfAccount], *delegate));
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;
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;
return checkTxPermission(sle, tx);
}
XRPAmount

View File

@@ -181,8 +181,7 @@ invokePreclaim(PreclaimContext const& ctx)
if (NotTEC const result = T::checkPriorTxAndLastLedger(ctx))
return result;
if (NotTEC const result =
Transactor::invokeCheckPermission<T>(ctx.view, ctx.tx))
if (NotTEC const result = T::checkPermission(ctx.view, ctx.tx))
return result;
if (NotTEC const result = T::checkSign(ctx))

View File

@@ -6,6 +6,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -19,11 +20,13 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/applySteps.h>
#include <cstdint>
#include <unordered_set>
namespace xrpl {
@@ -165,6 +168,54 @@ 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<GranularPermissionType> 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)
{

View File

@@ -29,12 +29,14 @@ checkTxPermission(SLE::const_ref delegate, STTx const& tx)
return terNO_DELEGATE_PERMISSION;
}
std::unordered_set<GranularPermissionType>
getGranularPermission(SLE::const_ref delegate, TxType const& txType)
void
loadGranularPermission(
SLE::const_ref delegate,
TxType const& txType,
std::unordered_set<GranularPermissionType>& granularPermissions)
{
std::unordered_set<GranularPermissionType> granularPermissions;
if (!delegate)
return granularPermissions;
return;
auto const permissionArray = delegate->getFieldArray(sfPermissions);
for (auto const& permission : permissionArray)
@@ -45,8 +47,6 @@ getGranularPermission(SLE::const_ref delegate, TxType const& txType)
if (type && *type == txType)
granularPermissions.insert(granularValue);
}
return granularPermissions;
}
} // namespace xrpl

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/PermissionedDEXHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
@@ -28,6 +29,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
@@ -271,24 +273,38 @@ Payment::preflight(PreflightContext const& ctx)
}
NotTEC
Payment::checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
Payment::checkPermission(ReadView const& view, STTx const& tx)
{
auto const delegate = tx[~sfDelegate];
if (!delegate)
return tesSUCCESS;
auto const delegateKey = keylet::delegate(tx[sfAccount], *delegate);
auto const sle = view.read(delegateKey);
if (!sle)
return terNO_DELEGATE_PERMISSION;
if (isTesSuccess(checkTxPermission(sle, tx)))
return tesSUCCESS;
std::unordered_set<GranularPermissionType> 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)
if ((tx.isFieldPresent(sfSendMax) && tx[sfSendMax].asset() != amountAsset) ||
tx.isFieldPresent(sfPaths))
return terNO_DELEGATE_PERMISSION;
// PaymentMint and PaymentBurn apply to both IOU and MPT direct payments.
if (heldGranularPermissions.contains(PaymentMint) && !isXRP(amountAsset) &&
if (granularPermissions.contains(PaymentMint) && !isXRP(amountAsset) &&
amountAsset.getIssuer() == tx[sfAccount])
return tesSUCCESS;
if (heldGranularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) &&
if (granularPermissions.contains(PaymentBurn) && !isXRP(amountAsset) &&
amountAsset.getIssuer() == tx[sfDestination])
return tesSUCCESS;

View File

@@ -5,6 +5,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -15,12 +16,14 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <unordered_set>
namespace xrpl {
@@ -135,6 +138,39 @@ 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<GranularPermissionType> 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)
{

View File

@@ -6,6 +6,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AccountID.h>
@@ -20,6 +21,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
@@ -122,21 +124,51 @@ TrustSet::preflight(PreflightContext const& ctx)
}
NotTEC
TrustSet::checkGranularSemantics(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType> const& heldGranularPermissions)
TrustSet::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;
// 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<Issue>().currency));
// granular permissions are not allowed to create a trustline
// if the trustline does not exist, granular permissions are
// not allowed to create trustline
if (!sleRippleState)
return terNO_DELEGATE_PERMISSION;
// updating LimitAmount is not allowed with granular permissions,
std::unordered_set<GranularPermissionType> 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,
// unless there's a new granular permission for this in the future.
auto const curLimit = tx[sfAccount] > saLimitAmount.getIssuer()
? sleRippleState->getFieldAmount(sfHighLimit)

View File

@@ -5,11 +5,8 @@
#include <test/jtx/acctdelete.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/batch.h>
#include <test/jtx/delegate.h>
#include <test/jtx/delivermin.h>
#include <test/jtx/did.h>
#include <test/jtx/domain.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/mpt.h>
@@ -25,9 +22,7 @@
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
@@ -38,7 +33,6 @@
#include <xrpl/ledger/helpers/DelegateHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Permissions.h>
#include <xrpl/protocol/SField.h>
@@ -47,7 +41,6 @@
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
@@ -1070,93 +1063,6 @@ 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;
@@ -1213,40 +1119,6 @@ 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
@@ -1429,34 +1301,6 @@ 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
@@ -1612,9 +1456,7 @@ class Delegate_test : public beast::unit_test::Suite
env(jv2, Ter(terNO_DELEGATE_PERMISSION));
}
// 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.
// can not set AccountSet flags on behalf of other account
{
Env env(*this);
auto const alice = Account{"alice"};
@@ -1710,71 +1552,6 @@ 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
@@ -1895,37 +1672,6 @@ 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
@@ -2395,62 +2141,6 @@ 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
@@ -2503,94 +2193,6 @@ 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()
{
@@ -2600,8 +2202,9 @@ class Delegate_test : public beast::unit_test::Suite
STTx const tx{ttPAYMENT, [](STObject&) {}};
BEAST_EXPECT(checkTxPermission(nullptr, tx) == terNO_DELEGATE_PERMISSION);
// getGranularPermission nullptr check
auto const granularPermissions = getGranularPermission(nullptr, ttPAYMENT);
// loadGranularPermission nullptr check
std::unordered_set<GranularPermissionType> granularPermissions;
loadGranularPermission(nullptr, ttPAYMENT, granularPermissions);
BEAST_EXPECT(granularPermissions.empty());
}
@@ -2631,9 +2234,7 @@ class Delegate_test : public beast::unit_test::Suite
testSignForDelegated();
testPermissionValue(all);
testTxRequireFeatures(all);
testGranularSandboxCheckOrder();
testTxDelegableCount();
testNonDelegableTxWithDelegate(all);
testDelegateUtilsNullptrCheck();
}
};

View File

@@ -8,21 +8,18 @@
#include <test/jtx/pay.h>
#include <test/jtx/permissioned_domains.h>
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/txflags.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <map>
#include <optional>
@@ -529,47 +526,6 @@ class PermissionedDomains_test : public beast::unit_test::Suite
BEAST_EXPECT(env.ownerCount(alice) == 1);
}
void
testTicket(FeatureBitset features)
{
testcase("Tickets");
using namespace test::jtx;
Env env(*this, features);
Account const alice("alice");
env.fund(XRP(1000), alice);
pdomain::Credentials const credentials{
{alice, "credential1"},
};
std::uint32_t seq{env.seq(alice)};
env(ticket::create(alice, 2));
{
env(pdomain::setTx(alice, credentials), ticket::Use(++seq));
auto domain = pdomain::getNewDomain(env.meta());
if (features[fixCleanup3_1_3])
{
BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), seq).key);
}
else
{
BEAST_EXPECT(domain == keylet::permissionedDomain(alice.id(), 0).key);
}
}
if (features[fixCleanup3_1_3])
{
env(pdomain::setTx(alice, credentials), ticket::Use(++seq));
}
else
{
env(pdomain::setTx(alice, credentials), ticket::Use(++seq), Ter(tefEXCEPTION));
}
}
public:
void
run() override
@@ -584,8 +540,6 @@ public:
testDelete(withFix_);
testAccountReserve(withFeature_);
testAccountReserve(withFix_);
testTicket(withFeature_);
testTicket(withFix_);
}
};

View File

@@ -61,6 +61,7 @@
#include <xrpl/resource/Gossip.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/server/NetworkOPs.h>
#include <xrpl/shamap/SHAMap.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <xrpl/tx/apply.h>
@@ -1507,23 +1508,12 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
}
}
// Verify ledger node IDs
if (itype != protocol::liBASE)
// Verify ledger node counts. Full parsing of the node IDs is deferred to the job, so the I/O
// thread is not burdened with SHAMapNodeID deserialization for every TMGetLedger message.
if (itype != protocol::liBASE && m->nodeids_size() <= 0)
{
if (m->nodeids_size() <= 0)
{
badData("Invalid ledger node IDs");
return;
}
for (auto const& nodeId : m->nodeids())
{
if (deserializeSHAMapNodeID(nodeId) == std::nullopt)
{
badData("Invalid SHAMap node ID");
return;
}
}
badData("Invalid ledger node IDs");
return;
}
// Verify query type
@@ -1543,11 +1533,40 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetLedger> const& m)
}
}
// Queue a job to process the request
// Queue a job to process the request.
std::weak_ptr<PeerImp> const weak = shared_from_this();
app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m]() {
if (auto peer = weak.lock())
peer->processLedgerRequest(m);
app_.getJobQueue().addJob(JtLedgerReq, "RcvGetLedger", [weak, m, itype]() {
auto peer = weak.lock();
if (!peer)
return;
std::vector<SHAMapNodeID> nodeIDs;
if (itype != protocol::liBASE)
{
nodeIDs.reserve(std::min(m->nodeids_size(), Tuning::kSoftMaxReplyNodes));
for (auto const& nodeId : m->nodeids())
{
if (nodeIDs.size() >= Tuning::kSoftMaxReplyNodes)
{
// Charge the peer for sending too many node IDs, but continue processing the
// received node IDs up to the limit. If the request is legitimate then at least
// they will get a response and won't have to resend these nodes in their next
// request.
peer->charge(
Resource::kFeeModerateBurdenPeer, "TMGetLedger: too many node IDs");
break;
}
auto parsed = deserializeSHAMapNodeID(nodeId);
if (!parsed)
{
peer->charge(Resource::kFeeInvalidData, "TMGetLedger: Invalid node ID");
return;
}
nodeIDs.push_back(std::move(*parsed));
}
}
peer->processLedgerRequest(m, std::move(nodeIDs));
});
}
@@ -3319,7 +3338,9 @@ PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const
}
void
PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
PeerImp::processLedgerRequest(
std::shared_ptr<protocol::TMGetLedger> const& m,
std::vector<SHAMapNodeID> nodeIDs)
{
// Do not resource charge a peer responding to a relay
if (!m->has_requestcookie())
@@ -3404,26 +3425,23 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
}
// Add requested node data to reply
if (m->nodeids_size() > 0)
if (!nodeIDs.empty())
{
std::uint32_t const defaultDepth = isHighLatency() ? 2 : 1;
auto const queryDepth{m->has_querydepth() ? m->querydepth() : defaultDepth};
std::vector<std::pair<SHAMapNodeID, Blob>> data;
for (int i = 0;
i < m->nodeids_size() && ledgerData.nodes_size() < Tuning::kSoftMaxReplyNodes;
++i)
data.reserve(Tuning::kSoftMaxReplyNodes);
for (auto const& nodeID : nodeIDs)
{
auto const shaMapNodeId{deserializeSHAMapNodeID(m->nodeids(i))};
if (ledgerData.nodes_size() >= Tuning::kSoftMaxReplyNodes)
break;
data.clear();
data.reserve(Tuning::kSoftMaxReplyNodes);
try
{
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) nodeids checked in onGetLedger
if (map->getNodeFat(*shaMapNodeId, data, fatLeaves, queryDepth))
if (map->getNodeFat(nodeID, data, fatLeaves, queryDepth))
{
JLOG(pJournal_.trace())
<< "processLedgerRequest: getNodeFat got " << data.size() << " nodes";
@@ -3432,9 +3450,9 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
{
if (ledgerData.nodes_size() >= Tuning::kHardMaxReplyNodes)
break;
protocol::TMLedgerNode* node{ledgerData.add_nodes()};
node->set_nodeid(d.first.getRawString());
node->set_nodedata(d.second.data(), d.second.size());
protocol::TMLedgerNode* ledgerNode{ledgerData.add_nodes()};
ledgerNode->set_nodeid(d.first.getRawString());
ledgerNode->set_nodedata(d.second.data(), d.second.size());
}
}
else
@@ -3473,14 +3491,14 @@ PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
info += ", no hash specified";
JLOG(pJournal_.warn())
<< "processLedgerRequest: getNodeFat with nodeId " << *shaMapNodeId
<< "processLedgerRequest: getNodeFat with nodeId " << nodeID
<< " and ledger info type " << info << " throws exception: " << e.what();
}
}
JLOG(pJournal_.info()) << "processLedgerRequest: Got request for " << m->nodeids_size()
<< " nodes at depth " << queryDepth << ", return "
<< ledgerData.nodes_size() << " nodes";
<< " node IDs (processed " << nodeIDs.size() << ") at depth "
<< queryDepth << ", return " << ledgerData.nodes_size() << " nodes";
}
if (ledgerData.nodes_size() == 0)

View File

@@ -14,6 +14,7 @@
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STValidation.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/shamap/SHAMapNodeID.h>
#include <boost/circular_buffer.hpp>
#include <boost/endian/conversion.hpp>
@@ -629,7 +630,9 @@ private:
getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const;
void
processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m);
processLedgerRequest(
std::shared_ptr<protocol::TMGetLedger> const& m,
std::vector<SHAMapNodeID> nodeIDs);
protected:
// Kept `protected` so test subclasses (see