Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/data/BackendInterface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "data/LedgerCacheInterface.hpp"
#include "data/Types.hpp"
#include "etl/CorruptionDetector.hpp"
#include "rpc/common/Types.hpp"
#include "util/Spawn.hpp"
#include "util/log/Logger.hpp"

Expand Down Expand Up @@ -305,6 +306,7 @@ class BackendInterface {
* @param limit The maximum number of transactions per result page
* @param forward Whether to fetch the page forwards or backwards from the given cursor
* @param txnCursor The cursor to resume fetching from
* @param delegateFilter Delegate filter to restrict results to transactions involving permission delegation
* @param yield The coroutine context
* @return Results and a cursor to resume from
*/
Expand All @@ -314,6 +316,7 @@ class BackendInterface {
std::uint32_t limit,
bool forward,
std::optional<TransactionsCursor> const& txnCursor,
std::optional<rpc::DelegateFilter> const& delegateFilter,
boost::asio::yield_context yield
) const = 0;

Expand Down
1 change: 1 addition & 0 deletions src/data/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ struct TransactionAndMetadata {
Blob metadata;
std::uint32_t ledgerSequence = 0;
std::uint32_t date = 0;
std::optional<ripple::AccountID> delegatedAccount;

TransactionAndMetadata() = default;

Expand Down
98 changes: 97 additions & 1 deletion src/data/cassandra/CassandraBackendFamily.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "data/cassandra/Handle.hpp"
#include "data/cassandra/Types.hpp"
#include "data/cassandra/impl/ExecutionStrategy.hpp"
#include "rpc/common/Types.hpp"
#include "util/Assert.hpp"
#include "util/LedgerUtils.hpp"
#include "util/Profiler.hpp"
Expand All @@ -40,18 +41,25 @@
#include <cassandra.h>
#include <fmt/format.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/nft.h>
#include <xrpl/protocol/tokens.h>

#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <iterator>
#include <limits>
#include <optional>
Expand Down Expand Up @@ -152,6 +160,7 @@ class CassandraBackendFamily : public BackendInterface {
std::uint32_t const limit,
bool forward,
std::optional<TransactionsCursor> const& txnCursor,
std::optional<rpc::DelegateFilter> const& delegateFilter,
boost::asio::yield_context yield
) const override
{
Expand Down Expand Up @@ -203,9 +212,63 @@ class CassandraBackendFamily : public BackendInterface {
}
}

auto const txns = fetchTransactions(hashes, yield);
auto txns = fetchTransactions(hashes, yield);
LOG(log_.debug()) << "Txns = " << txns.size();

std::optional<ripple::AccountID> filterCounterpartyID;
if (delegateFilter && delegateFilter->counterParty) {
filterCounterpartyID = ripple::parseBase58<ripple::AccountID>(*delegateFilter->counterParty);

// If the counterparty string is invalid, we don't return anything. Error should have been caught by
// validators.
if (!filterCounterpartyID) {
LOG(log_.warn()) << "Invalid counterparty account in filter";
return {.txns = {}, .cursor = {}};
}
}

std::vector<TransactionAndMetadata> resultTxns;
if (delegateFilter.has_value()) {
resultTxns.reserve(txns.size());

for (auto& txn : txns) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good place to try and use ranges? 🙂

auto const delegationInfo = getDelegationInfo(txn.transaction);

if (delegationInfo) {
auto const& [delegatee, delegator] = *delegationInfo;
bool match = false;

// Filter by "Delegator" ie. User wants to find the Owner (Delegator).
// This implies the User (account) must be the Signer (Delegatee) that acted on someone's
// behalf.
if (delegateFilter->delegateType == rpc::DelegateFilter::Role::Delegator) {
// The user (account) must be delegatee
if (account == delegatee) {
if (!filterCounterpartyID || *filterCounterpartyID == delegator) {
txn.delegatedAccount = delegator;
match = true;
}
}
}
// Filter by "Delegatee" ie. User wants to find the Signer (Delegatee).
// This implies the User (account) must be the Owner (Delegator).
else if (delegateFilter->delegateType == rpc::DelegateFilter::Role::Delegatee) {
// The user (account) must be delegator
if (account == delegator) {
if (!filterCounterpartyID || *filterCounterpartyID == delegatee) {
txn.delegatedAccount = delegatee;
match = true;
}
}
}

if (match)
resultTxns.push_back(txn);
}
}
return {.txns = resultTxns, .cursor = cursor};
}

if (txns.size() == limit) {
LOG(log_.debug()) << "Returning cursor";
return {txns, cursor};
Expand Down Expand Up @@ -970,6 +1033,39 @@ class CassandraBackendFamily : public BackendInterface {

return true;
}

/**
* @brief Extracts delegation information from a transaction.
*
* Parses the transaction blob and checks whether the signer
* (derived from the SigningPubKey) differs from the delegator
* account. If so, returns {delegatee, delegator}. Otherwise returns null.
*
* @param txnBlob Serialized transaction blob.
* @return pair of {delegatee, delegator} if delegated, otherwise std::nullopt
*/
static std::optional<std::pair<ripple::AccountID, ripple::AccountID>>
getDelegationInfo(ripple::Blob const& txnBlob)
{
ripple::SerialIter it{txnBlob.data(), txnBlob.size()};
ripple::STTx const txn{it};

auto const delegator = txn.getAccountID(ripple::sfAccount);
if (txn.isFieldPresent(ripple::sfSigningPubKey)) {
auto const pubKeyBlob = txn.getFieldVL(ripple::sfSigningPubKey);
ripple::PublicKey const pubKey{ripple::Slice{pubKeyBlob.data(), pubKeyBlob.size()}};

auto const delegatee = ripple::calcAccountID(pubKey);

// Delegation Check
// If the signer (delegatee) is NOT the account owner (delegator), it's delegated.
if (delegatee != delegator) {
return std::make_pair(delegatee, delegator);
}
}

return std::nullopt;
}
};

} // namespace data::cassandra
44 changes: 44 additions & 0 deletions src/rpc/RPCHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1567,4 +1567,48 @@ toJsonWithBinaryTx(data::TransactionAndMetadata const& txnPlusMeta, std::uint32_
return obj;
}

std::optional<DelegateFilter::Role>
parseDelegateType(boost::json::value const& delegateType)
{
if (not delegateType.is_string())
return {};

auto const& type = delegateType.as_string();

if (type == "delegator")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic strings everywhere 😃 these should either be JS(...) if possible or some sort of constants 🔢

return DelegateFilter::Role::Delegator;
if (type == "delegatee")
return DelegateFilter::Role::Delegatee;

return {};
}

std::optional<DelegateFilter>
parseDelegateFilter(boost::json::object const& delegateObject)
{
DelegateFilter delegate{};
if (!delegateObject.contains("delegate_filter"))
return {};

auto const& filterVal = delegateObject.at("delegate_filter");
if (!filterVal.is_string())
return {};

auto const delegateTypeOpt = parseDelegateType(filterVal.as_string());
if (!delegateTypeOpt.has_value())
return {};

delegate.delegateType = *delegateTypeOpt;
if (delegateObject.contains("counterparty")) {
auto const& counterpartyVal = delegateObject.at("counterparty");

if (!counterpartyVal.is_string())
return {};

delegate.counterParty = counterpartyVal.as_string();
}

return delegate;
}

} // namespace rpc
18 changes: 18 additions & 0 deletions src/rpc/RPCHelpers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -861,4 +861,22 @@ getDeliveredAmount(
uint32_t date
);

/**
* @brief Parse the delegate type from a JSON value
*
* @param delegateType The JSON value containing the delegate type string
* @return The parsed delegate type or std::nullopt if the input is invalid or not a string
*/
std::optional<DelegateFilter::Role>
parseDelegateType(boost::json::value const& delegateType);

/**
* @brief Parse a delegate filter object from JSON
*
* @param delegateObject The JSON object containing the delegate filter input from user
* @return The constructed DelegateFilter or std::nullopt if parsing fails
*/
std::optional<DelegateFilter>
parseDelegateFilter(boost::json::object const& delegateObject);

} // namespace rpc
23 changes: 22 additions & 1 deletion src/rpc/common/Types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@

#include <cstdint>
#include <expected>
#include <optional>
#include <string>
#include <utility>
#include <variant>

namespace etl {
class LoadBalancer;
Expand Down Expand Up @@ -194,6 +194,27 @@ struct AccountCursor {
}
};

/**
* @brief A delegate object used filter account_tx by specific delegate accounts
*/
struct DelegateFilter {
/**
* @brief A delegate type used in delegate filter
*/
enum class Role {
// This account is the *active* sender, acting on behalf of another party.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be part of doxygen, perhaps with a trailing comment style

// e.g., Account A in "A sends payment to B on behalf of C."
Delegatee,

// This account is the *passive* party whose funds are being moved from.
// e.g., Account C in "A sends payment to B on behalf of C."
Delegator
};

Role delegateType;
std::optional<std::string> counterParty;
};

/**
* @brief Convert an empty output to a JSON object
*
Expand Down
22 changes: 22 additions & 0 deletions src/rpc/common/Validators.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -357,4 +357,26 @@ CustomValidator CustomValidators::authorizeCredentialValidator =
return MaybeError{};
}};

CustomValidator CustomValidators::delegateValidator =
CustomValidator{[](boost::json::value const& value, std::string_view key) -> MaybeError {
if (!value.is_object())
return Error{Status{RippledError::rpcINVALID_PARAMS, std::string(key) + " not object"}};

auto const& delegate = value.as_object();
if (!delegate.contains("delegate_filter"))
return Error{Status{RippledError::rpcINVALID_PARAMS, "Field 'delegate_filter' is required but missing."}};

if (!parseDelegateType(delegate.at("delegate_filter")).has_value())
return Error{Status{
RippledError::rpcINVALID_PARAMS, "Field 'delegate_filter' value must be 'delegator' or 'delegatee'."
}};

if (delegate.contains("counterparty") && !accountValidator.verify(delegate, "counterparty"))
return Error{
Status{RippledError::rpcINVALID_PARAMS, "Field 'counterparty' value must be a valid account."}
};

return MaybeError{};
}};

} // namespace rpc::validation
7 changes: 7 additions & 0 deletions src/rpc/common/Validators.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,13 @@ struct CustomValidators final {
* Used by AuthorizeCredentialValidator in deposit_preauth.
*/
static CustomValidator credentialTypeValidator;

/**
* @brief Provides a validator for validating filtering by delegation.
*
* Used by account_tx if user wants to filter by delegation.
*/
static CustomValidator delegateValidator;
};

/**
Expand Down
21 changes: 20 additions & 1 deletion src/rpc/handlers/AccountTx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c
auto const limit = input.limit.value_or(kLIMIT_DEFAULT);
auto const accountID = accountFromStringStrict(input.account);
auto const [txnsAndCursor, timeDiff] = util::timed([&]() {
return sharedPtrBackend_->fetchAccountTransactions(*accountID, limit, input.forward, cursor, ctx.yield);
return sharedPtrBackend_->fetchAccountTransactions(
*accountID, limit, input.forward, cursor, input.delegateFilter, ctx.yield
);
});

LOG(log_.info()) << "db fetch took " << timeDiff << " milliseconds - num blobs = " << txnsAndCursor.txns.size();
Expand Down Expand Up @@ -191,6 +193,19 @@ AccountTxHandler::process(AccountTxHandler::Input const& input, Context const& c
obj[JS(close_time_iso)] = ripple::to_string_iso(ledgerHeader->closeTime);
}
}

if (txnPlusMeta.delegatedAccount.has_value()) {
if (input.delegateFilter) {
if (input.delegateFilter->delegateType == rpc::DelegateFilter::Role::Delegator) {
// filtering by the txns where other accounts sent txns for this delegatedAccount
obj["delegator"] = to_string(*txnPlusMeta.delegatedAccount);
} else if (input.delegateFilter->delegateType == rpc::DelegateFilter::Role::Delegatee) {
// filtering by the txns where this delegatedAccount sent txn on behalf of other users
obj["delegatee"] = to_string(*txnPlusMeta.delegatedAccount);
}
}
}

obj[JS(validated)] = true;
response.transactions.push_back(std::move(obj));
continue;
Expand Down Expand Up @@ -286,6 +301,10 @@ tag_invoke(boost::json::value_to_tag<AccountTxHandler::Input>, boost::json::valu
if (jsonObject.contains("tx_type"))
input.transactionTypeInLowercase = boost::json::value_to<std::string>(jsonObject.at("tx_type"));

if (jsonObject.contains("delegate")) {
input.delegateFilter = parseDelegateFilter(jsonObject.at("delegate").as_object());
}

return input;
}

Expand Down
2 changes: 2 additions & 0 deletions src/rpc/handlers/AccountTx.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class AccountTxHandler {
std::optional<uint32_t> limit;
std::optional<Marker> marker;
std::optional<std::string> transactionTypeInLowercase;
std::optional<DelegateFilter> delegateFilter;
};

using Result = HandlerReturnType<Output>;
Expand Down Expand Up @@ -157,6 +158,7 @@ class AccountTxHandler {
modifiers::ToLower{},
validation::OneOf<std::string>(typesKeysInLowercase.cbegin(), typesKeysInLowercase.cend()),
},
{"delegate", validation::CustomValidators::delegateValidator}
};

static auto const kRPC_SPEC = RpcSpec{
Expand Down
Loading
Loading