Files
rippled/src/xrpld/rpc/detail/PathRequest.cpp
Pratik Mankawde 56cadaff6d fix(telemetry): stop path-find tracing from altering request handling
Telemetry must read state, never change it. Two defects here did change it,
plus three smaller correctness and privacy fixes.

doPathFind and doRipplePathFind read source_account / destination_account off
context.params to hash them into span attributes. context.params is non-const,
so those reads selected json::Value's non-const operator[], which inserts a
null for a missing key. The same object is later validated by
PathRequest::parseJson, whose first checks are isMember(source_account) and
isMember(destination_account) — so a request that omitted either field looked
present and the client received Malformed instead of Missing. Reads now go
through std::as_const, whose overload returns kNull without inserting.

PathRequest::doUpdate emitted pathfind_dest_currency as
to_string(saDstAmount_.asset()). For a non-XRP asset that renders as
"<issuer>/<currency>" with the issuer as a plaintext Base58 address, so a
plain account address reached the span pipeline even though every other
account here is hashed first. The issuer is now redacted and the currency
kept; an MPT asset renders as its issuance ID and carries no address.

PathRequestManager::updateAll created pathfind.update_all with an unscoped
SpanGuard. An unscoped guard takes the ambient span as its own parent but does
not itself become the ambient parent, so the pathfind.compute spans that
doUpdate creates never nested under it, contradicting the documented hierarchy.
It is now a scoped guard, held in std::optional because ScopedSpanGuard is
deliberately non-movable and so cannot be produced by a ternary. The skip when
there are no active subscriptions is preserved. updateAll is dispatched via
addJob and doUpdate runs synchronously, so the guard is constructed and
destroyed under the same context store, as ScopedSpanGuard requires.

The WebSocket entry point emitted the client-supplied command string directly.
That value becomes a Prometheus label, so arbitrary request input could drive
unbounded label cardinality. It is now resolved against the handler registry,
collapsing anything unrecognized to "unknown", matching what the HTTP path
already does.

Also: the pathfind.discover comment claimed future child spans could be
parented off it, which its unscoped guard cannot do — corrected to say what
would be required instead. Config-reference and task-list docs named the
parser setupTelemetry(); the API is makeTelemetrySetup().
2026-07-29 17:50:34 +01:00

876 lines
28 KiB
C++

#include <xrpld/rpc/detail/PathRequest.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/detail/AccountAssets.h>
#include <xrpld/rpc/detail/PathFindSpanNames.h>
#include <xrpld/rpc/detail/PathRequestManager.h>
#include <xrpld/rpc/detail/Pathfinder.h>
#include <xrpld/rpc/detail/PathfinderUtils.h>
#include <xrpld/rpc/detail/Tuning.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PathAsset.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Consumer.h>
#include <xrpl/server/InfoSub.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/telemetry/Redaction.h>
#include <xrpl/telemetry/SpanGuard.h>
#include <xrpl/tx/paths/RippleCalc.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <variant>
namespace xrpl {
PathRequest::PathRequest(
Application& app,
std::shared_ptr<InfoSub> const& subscriber,
int id,
PathRequestManager& owner,
beast::Journal journal)
: app_(app)
, journal_(journal)
, owner_(owner)
, wpSubscriber_(subscriber)
, consumer_(subscriber->getConsumer())
, jvStatus_(json::ValueType::Object)
, lastIndex_(0)
, inProgress_(false)
, iLevel_(0)
, bLastSuccess_(false)
, iIdentifier_(id)
, created_(std::chrono::steady_clock::now())
{
JLOG(journal_.debug()) << iIdentifier_ << " created";
}
PathRequest::PathRequest(
Application& app,
std::function<void(void)> completion,
Resource::Consumer& consumer,
int id,
PathRequestManager& owner,
beast::Journal journal)
: app_(app)
, journal_(journal)
, owner_(owner)
, fCompletion_(std::move(completion))
, consumer_(consumer)
, jvStatus_(json::ValueType::Object)
, lastIndex_(0)
, inProgress_(false)
, iLevel_(0)
, bLastSuccess_(false)
, iIdentifier_(id)
, created_(std::chrono::steady_clock::now())
{
JLOG(journal_.debug()) << iIdentifier_ << " created";
}
PathRequest::~PathRequest()
{
using namespace std::chrono;
auto stream = journal_.info();
if (!stream)
return;
std::string fast, full;
if (quickReply_ != steady_clock::time_point{})
{
fast = " fast:";
fast += std::to_string(duration_cast<milliseconds>(quickReply_ - created_).count());
fast += "ms";
}
if (fullReply_ != steady_clock::time_point{})
{
full = " full:";
full += std::to_string(duration_cast<milliseconds>(fullReply_ - created_).count());
full += "ms";
}
stream << iIdentifier_ << " complete:" << fast << full
<< " total:" << duration_cast<milliseconds>(steady_clock::now() - created_).count()
<< "ms";
}
bool
PathRequest::isNew()
{
std::scoped_lock const sl(indexLock_);
// does this path request still need its first full path
return lastIndex_ == 0;
}
bool
PathRequest::needsUpdate(bool newOnly, LedgerIndex index)
{
std::scoped_lock const sl(indexLock_);
if (inProgress_)
{
// Another thread is handling this
return false;
}
if (newOnly && (lastIndex_ != 0))
{
// Only handling new requests, this isn't new
return false;
}
if (lastIndex_ >= index)
{
return false;
}
inProgress_ = true;
return true;
}
bool
PathRequest::hasCompletion()
{
return bool(fCompletion_);
}
void
PathRequest::updateComplete()
{
std::scoped_lock const sl(indexLock_);
XRPL_ASSERT(inProgress_, "xrpl::PathRequest::updateComplete : in progress");
inProgress_ = false;
if (fCompletion_)
{
fCompletion_();
fCompletion_ = std::function<void(void)>();
}
}
bool
PathRequest::isValid(std::shared_ptr<AssetCache> const& crCache)
{
if (!raSrcAccount_ || !raDstAccount_)
return false;
if (!convertAll_ && (saSendMax_ || saDstAmount_ <= beast::kZero))
{
// If send max specified, dst amt must be -1.
jvStatus_ = rpcError(RpcDstAmtMalformed);
return false;
}
auto const& lrLedger = crCache->getLedger();
if (!lrLedger->exists(keylet::account(*raSrcAccount_)))
{
// Source account does not exist.
jvStatus_ = rpcError(RpcSrcActNotFound);
return false;
}
auto const sleDest = lrLedger->read(keylet::account(*raDstAccount_));
json::Value& jvDestCur = (jvStatus_[jss::destination_currencies] = json::ValueType::Array);
if (!sleDest)
{
jvDestCur.append(json::Value(systemCurrencyCode()));
if (!saDstAmount_.native())
{
// Only XRP can be send to a non-existent account.
jvStatus_ = rpcError(RpcActNotFound);
return false;
}
if (!convertAll_ && saDstAmount_ < STAmount(lrLedger->fees().reserve))
{
// Payment must meet reserve.
jvStatus_ = rpcError(RpcDstAmtMalformed);
return false;
}
}
else
{
bool const disallowXRP(sleDest->isFlag(lsfDisallowXRP));
auto const destAssets = accountDestAssets(*raDstAccount_, crCache, !disallowXRP);
for (auto const& asset : destAssets)
jvDestCur.append(to_string(asset));
jvStatus_[jss::destination_tag] = (sleDest->getFlags() & lsfRequireDestTag);
}
jvStatus_[jss::ledger_hash] = to_string(lrLedger->header().hash);
jvStatus_[jss::ledger_index] = lrLedger->seq();
return true;
}
/* If this is a normal path request, we want to run it once "fast" now
to give preliminary results.
If this is a legacy path request, we are only going to run it once,
and we can't run it in full now, so we don't want to run it at all.
If there's an error, we need to be sure to return it to the caller
in all cases.
*/
std::pair<bool, json::Value>
PathRequest::doCreate(std::shared_ptr<AssetCache> const& cache, json::Value const& value)
{
bool valid = false;
if (parseJson(value) != PFR_PJ_INVALID)
{
valid = isValid(cache);
if (!hasCompletion() && valid)
doUpdate(cache, true);
}
if (auto stream = journal_.debug())
{
if (valid)
{
stream << iIdentifier_ << " valid: " << toBase58(*raSrcAccount_);
stream << iIdentifier_ << " deliver: " << saDstAmount_.getFullText();
}
else
{
stream << iIdentifier_ << " invalid";
}
}
return {valid, jvStatus_};
}
int
PathRequest::parseJson(json::Value const& jvParams)
{
if (!jvParams.isMember(jss::source_account))
{
jvStatus_ = rpcError(RpcSrcActMissing);
return PFR_PJ_INVALID;
}
if (!jvParams.isMember(jss::destination_account))
{
jvStatus_ = rpcError(RpcDstActMissing);
return PFR_PJ_INVALID;
}
if (!jvParams.isMember(jss::destination_amount))
{
jvStatus_ = rpcError(RpcDstAmtMissing);
return PFR_PJ_INVALID;
}
raSrcAccount_ = parseBase58<AccountID>(jvParams[jss::source_account].asString());
if (!raSrcAccount_)
{
jvStatus_ = rpcError(RpcSrcActMalformed);
return PFR_PJ_INVALID;
}
raDstAccount_ = parseBase58<AccountID>(jvParams[jss::destination_account].asString());
if (!raDstAccount_)
{
jvStatus_ = rpcError(RpcDstActMalformed);
return PFR_PJ_INVALID;
}
if (!amountFromJsonNoThrow(saDstAmount_, jvParams[jss::destination_amount]))
{
jvStatus_ = rpcError(RpcDstAmtMalformed);
return PFR_PJ_INVALID;
}
convertAll_ = saDstAmount_ == STAmount(saDstAmount_.asset(), 1u, 0, true);
if (!validAsset(saDstAmount_.asset()) || (!convertAll_ && saDstAmount_ <= beast::kZero))
{
jvStatus_ = rpcError(RpcDstAmtMalformed);
return PFR_PJ_INVALID;
}
if (jvParams.isMember(jss::send_max))
{
// Send_max requires destination amount to be -1.
if (!convertAll_)
{
jvStatus_ = rpcError(RpcDstAmtMalformed);
return PFR_PJ_INVALID;
}
saSendMax_.emplace();
if (!amountFromJsonNoThrow(*saSendMax_, jvParams[jss::send_max]) ||
!validAsset(saSendMax_->asset()) ||
(*saSendMax_ <= beast::kZero &&
*saSendMax_ != STAmount(saSendMax_->asset(), 1u, 0, true)))
{
jvStatus_ = rpcError(RpcSendmaxMalformed);
return PFR_PJ_INVALID;
}
}
if (jvParams.isMember(jss::source_currencies))
{
json::Value const& jvSrcCurrencies = jvParams[jss::source_currencies];
if (!jvSrcCurrencies.isArray() || jvSrcCurrencies.size() == 0 ||
jvSrcCurrencies.size() > RPC::Tuning::kMaxSrcCur)
{
jvStatus_ = rpcError(RpcSrcCurMalformed);
return PFR_PJ_INVALID;
}
sciSourceAssets_.clear();
for (auto const& c : jvSrcCurrencies)
{
// Mandatory currency or MPT
if (!validJSONAsset(c) || !c.isObject())
{
jvStatus_ = rpcError(RpcSrcCurMalformed);
return PFR_PJ_INVALID;
}
PathAsset srcPathAsset;
if (c.isMember(jss::currency))
{
Currency currency;
if (!c[jss::currency].isString() ||
!toCurrency(currency, c[jss::currency].asString()))
{
jvStatus_ = rpcError(RpcSrcCurMalformed);
return PFR_PJ_INVALID;
}
srcPathAsset = currency;
}
else
{
uint192 u;
if (!c[jss::mpt_issuance_id].isString() ||
!u.parseHex(c[jss::mpt_issuance_id].asString()))
{
jvStatus_ = rpcError(RpcSrcCurMalformed);
return PFR_PJ_INVALID;
}
srcPathAsset = u;
}
// Optional issuer
AccountID srcIssuerID;
if (c.isMember(jss::issuer) &&
(c.isMember(jss::mpt_issuance_id) || !c[jss::issuer].isString() ||
!toIssuer(srcIssuerID, c[jss::issuer].asString())))
{
jvStatus_ = rpcError(RpcSrcIsrMalformed);
return PFR_PJ_INVALID;
}
if (srcPathAsset.holds<Currency>())
{
if (srcPathAsset.get<Currency>().isZero())
{
if (srcIssuerID.isNonZero())
{
jvStatus_ = rpcError(RpcSrcCurMalformed);
return PFR_PJ_INVALID;
}
}
else if (srcIssuerID.isZero())
{
srcIssuerID = *raSrcAccount_;
}
}
if (saSendMax_)
{
// If the assets don't match, ignore the source asset.
if (srcPathAsset == saSendMax_->asset())
{
// If neither is the source and they are not equal, then the
// source issuer is illegal.
if (srcIssuerID != *raSrcAccount_ &&
saSendMax_->getIssuer() != *raSrcAccount_ &&
srcIssuerID != saSendMax_->getIssuer())
{
jvStatus_ = rpcError(RpcSrcIsrMalformed);
return PFR_PJ_INVALID;
}
// If both are the source, use the source.
// Otherwise, use the one that's not the source.
srcPathAsset.visit(
[&](Currency const& currency) {
if (srcIssuerID != *raSrcAccount_)
{
sciSourceAssets_.insert(Issue{currency, srcIssuerID});
}
else if (saSendMax_->getIssuer() != *raSrcAccount_)
{
sciSourceAssets_.insert(Issue{currency, saSendMax_->getIssuer()});
}
{
sciSourceAssets_.insert(Issue{currency, *raSrcAccount_});
}
},
[&](MPTID const& mpt) { sciSourceAssets_.insert(mpt); });
}
}
else
{
srcPathAsset.visit(
[&](Currency const& currency) {
sciSourceAssets_.insert(Issue{currency, srcIssuerID});
},
[&](MPTID const& mpt) { sciSourceAssets_.insert(MPTIssue{mpt}); });
}
}
}
if (jvParams.isMember(jss::id))
jvId_ = jvParams[jss::id];
if (jvParams.isMember(jss::domain))
{
uint256 num;
if (!jvParams[jss::domain].isString() || !num.parseHex(jvParams[jss::domain].asString()))
{
jvStatus_ = rpcError(RpcDomainMalformed);
return PFR_PJ_INVALID;
}
domain_ = num;
}
return PFR_PJ_NOCHANGE;
}
json::Value
PathRequest::doClose()
{
JLOG(journal_.debug()) << iIdentifier_ << " closed";
std::scoped_lock const sl(lock_);
jvStatus_[jss::closed] = true;
return jvStatus_;
}
json::Value
PathRequest::doStatus(json::Value const&)
{
std::scoped_lock const sl(lock_);
jvStatus_[jss::status] = jss::success;
return jvStatus_;
}
void
PathRequest::doAborting() const
{
JLOG(journal_.info()) << iIdentifier_ << " aborting early";
}
std::unique_ptr<Pathfinder> const&
PathRequest::getPathFinder(
std::shared_ptr<AssetCache> const& cache,
hash_map<PathAsset, std::unique_ptr<Pathfinder>>& currencyMap,
PathAsset const& currency,
STAmount const& dstAmount,
int const level,
std::function<bool(void)> const& continueCallback)
{
auto i = currencyMap.find(currency);
if (i != currencyMap.end())
return i->second;
// NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set
auto pathfinder = std::make_unique<Pathfinder>(
cache,
*raSrcAccount_,
*raDstAccount_,
currency,
std::nullopt,
dstAmount,
saSendMax_,
domain_,
app_);
// NOLINTEND(bugprone-unchecked-optional-access)
if (pathfinder->findPaths(level, continueCallback))
{
pathfinder->computePathRanks(kMaxPaths, continueCallback);
}
else
{
pathfinder.reset(); // It's a bad request - clear it.
}
return currencyMap[currency] = std::move(pathfinder);
}
bool
PathRequest::findPaths(
std::shared_ptr<AssetCache> const& cache,
int const level,
json::Value& jvArray,
std::function<bool(void)> const& continueCallback)
{
auto sourceAssets = sciSourceAssets_;
if (sourceAssets.empty() && saSendMax_)
{
sourceAssets.insert(saSendMax_->asset());
}
if (sourceAssets.empty())
{
// NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set
auto assets = accountSourceAssets(*raSrcAccount_, cache, true);
bool const sameAccount = *raSrcAccount_ == *raDstAccount_;
// NOLINTEND(bugprone-unchecked-optional-access)
for (auto const& asset : assets)
{
if (!std::visit(
[&]<typename TAsset>(TAsset const& a) {
if (!sameAccount || a != saDstAmount_.asset())
{
if (sourceAssets.size() >= RPC::Tuning::kMaxAutoSrcCur)
return false;
if constexpr (std::is_same_v<TAsset, Currency>)
{
sourceAssets.insert(
Issue{a, a.isZero() ? xrpAccount() : *raSrcAccount_});
}
else
{
sourceAssets.insert(MPTIssue{a});
}
}
return true;
},
asset.value()))
{
return false;
}
}
}
auto const dstAmount = convertAmount(saDstAmount_, convertAll_);
hash_map<PathAsset, std::unique_ptr<Pathfinder>> currencyMap;
// One `pathfind.discover` span wraps the entire per-source-asset loop so
// that a single RPC call produces one discover span instead of N (one per
// candidate source asset). Trade-off: per-asset discovery/ranking timing
// is no longer split into individual spans — span count and Tempo storage
// are bounded per RPC at the cost of per-asset visibility.
//
// This is an unscoped guard: it takes the ambient span as its own parent,
// but does not itself become the ambient parent. Adding per-asset child
// spans inside the loop body therefore requires either making this a
// ScopedSpanGuard or passing its context explicitly via childSpan(); a
// child created here today would parent to this span's parent, not to it.
using namespace telemetry;
auto span = SpanGuard::span(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::discover);
span.setAttribute(pathfind_span::attr::searchLevel, static_cast<int64_t>(level));
span.setAttribute(
pathfind_span::attr::numSourceAssets, static_cast<int64_t>(sourceAssets.size()));
std::int64_t totalPaths = 0;
for (auto const& asset : sourceAssets)
{
if (continueCallback && !continueCallback())
break;
JLOG(journal_.debug()) << iIdentifier_
<< " Trying to find paths: " << STAmount(asset, 1).getFullText();
auto& pathfinder =
getPathFinder(cache, currencyMap, PathAsset(asset), dstAmount, level, continueCallback);
if (!pathfinder)
{
JLOG(journal_.debug()) << iIdentifier_ << " No paths found";
continue;
}
STPath fullLiquidityPath;
auto ps = pathfinder->getBestPaths(
kMaxPaths, fullLiquidityPath, context_[asset], asset.getIssuer(), continueCallback);
context_[asset] = ps;
totalPaths += static_cast<std::int64_t>(ps.size());
auto const& sourceAccount = [&] {
if (!isXRP(asset.getIssuer()))
return asset.getIssuer();
if (isXRP(asset))
return xrpAccount();
return *raSrcAccount_;
}();
STAmount const saMaxAmount = [&]() {
if (saSendMax_)
return *saSendMax_;
return asset.visit(
[&](Issue const& issue) {
return STAmount(Issue{issue.currency, sourceAccount}, 1u, 0, true);
},
[](MPTIssue const& issue) { return STAmount(issue, 1u, 0, true); });
}();
JLOG(journal_.debug()) << iIdentifier_ << " Paths found, calling rippleCalc";
path::RippleCalc::Input rcInput;
if (convertAll_)
rcInput.partialPaymentAllowed = true;
auto sandbox = std::make_unique<PaymentSandbox>(&*cache->getLedger(), TapNone);
auto rc = path::RippleCalc::rippleCalculate(
*sandbox,
saMaxAmount, // --> Amount to send is unlimited
// to get an estimate.
dstAmount, // --> Amount to deliver.
// NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set
*raDstAccount_, // --> Account to deliver to.
*raSrcAccount_, // --> Account sending from.
// NOLINTEND(bugprone-unchecked-optional-access)
ps, // --> Path set.
domain_, // --> Domain.
app_,
&rcInput);
if (!convertAll_ && !fullLiquidityPath.empty() &&
(rc.result() == terNO_LINE || rc.result() == tecPATH_PARTIAL))
{
JLOG(journal_.debug()) << iIdentifier_ << " Trying with an extra path element";
ps.pushBack(fullLiquidityPath);
sandbox = std::make_unique<PaymentSandbox>(&*cache->getLedger(), TapNone);
rc = path::RippleCalc::rippleCalculate(
*sandbox,
saMaxAmount, // --> Amount to send is unlimited
// to get an estimate.
dstAmount, // --> Amount to deliver.
// NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set
*raDstAccount_, // --> Account to deliver to.
*raSrcAccount_, // --> Account sending from.
// NOLINTEND(bugprone-unchecked-optional-access)
ps, // --> Path set.
domain_, // --> Domain.
app_);
if (!isTesSuccess(rc.result()))
{
JLOG(journal_.warn())
<< iIdentifier_ << " Failed with covering path " << transHuman(rc.result());
}
else
{
JLOG(journal_.debug())
<< iIdentifier_ << " Extra path element gives " << transHuman(rc.result());
}
}
if (rc.result() == tesSUCCESS)
{
json::Value jvEntry(json::ValueType::Object);
if (rc.actualAmountIn.holds<Issue>())
rc.actualAmountIn.get<Issue>().account = sourceAccount;
jvEntry[jss::source_amount] = rc.actualAmountIn.getJson(JsonOptions::Values::None);
jvEntry[jss::paths_computed] = ps.getJson(JsonOptions::Values::None);
if (convertAll_)
{
jvEntry[jss::destination_amount] =
rc.actualAmountOut.getJson(JsonOptions::Values::None);
}
if (hasCompletion())
{
// Old ripple_path_find API requires this
jvEntry[jss::paths_canonical] = json::ValueType::Array;
}
jvArray.append(jvEntry);
}
else
{
JLOG(journal_.debug())
<< iIdentifier_ << " rippleCalc returns " << transHuman(rc.result());
}
}
span.setAttribute(pathfind_span::attr::numPaths, totalPaths);
/* The resource fee is based on the number of source currencies used.
The minimum cost is 50 and the maximum is 400. The cost increases
after four source currencies, 50 - (4 * 4) = 34.
*/
int const size = sourceAssets.size();
consumer_.charge({std::clamp((size * size) + 34, 50, 400), "path update"});
return true;
}
json::Value
PathRequest::doUpdate(
std::shared_ptr<AssetCache> const& cache,
bool fast,
std::function<bool(void)> const& continueCallback)
{
using namespace std::chrono;
using namespace telemetry;
// Scoped so pathfind.discover (created synchronously below via findPaths)
// nests under it. doUpdate does not yield, so scoping is safe.
auto span = ScopedSpanGuard(
TraceCategory::Rpc, pathfind_span::prefix::pathfind, pathfind_span::op::compute);
span.setAttribute(pathfind_span::attr::fast, fast);
// to_string(Issue) renders a non-XRP asset as "<issuer>/<currency>" with the
// issuer as a plaintext Base58 address, so it cannot be emitted as-is: every
// account reaching a span is hashed first. Redact just the issuer and keep
// the currency, which is what this attribute is for. An MPT asset renders as
// its issuance ID and carries no address, so it needs no redaction.
span.setAttribute(
pathfind_span::attr::destCurrency,
saDstAmount_.asset().visit(
[](Issue const& issue) {
return isXRP(issue.account)
? to_string(issue.currency)
: redactAccount(toBase58(issue.account)) + "/" + to_string(issue.currency);
},
[](MPTIssue const& mpt) { return to_string(mpt.getMptID()); }));
JLOG(journal_.debug()) << iIdentifier_ << " update " << (fast ? "fast" : "normal");
{
std::scoped_lock const sl(lock_);
if (!isValid(cache))
return jvStatus_;
}
json::Value newStatus = json::ValueType::Object;
if (hasCompletion())
{
// Old ripple_path_find API gives destination_currencies
auto& destAssets = (newStatus[jss::destination_currencies] = json::ValueType::Array);
// NOLINTNEXTLINE(bugprone-unchecked-optional-access) isValid() ensures both are set
auto const assets = accountDestAssets(*raDstAccount_, cache, true);
for (auto const& asset : assets)
destAssets.append(to_string(asset));
}
// NOLINTBEGIN(bugprone-unchecked-optional-access) isValid() ensures both are set
newStatus[jss::source_account] = toBase58(*raSrcAccount_);
newStatus[jss::destination_account] = toBase58(*raDstAccount_);
// NOLINTEND(bugprone-unchecked-optional-access)
newStatus[jss::destination_amount] = saDstAmount_.getJson(JsonOptions::Values::None);
newStatus[jss::full_reply] = !fast;
if (jvId_)
newStatus[jss::id] = jvId_;
bool const loaded = app_.getFeeTrack().isLoadedLocal();
if (iLevel_ == 0)
{
// first pass
if (loaded || fast)
{
iLevel_ = app_.config().pathSearchFast;
}
else
{
iLevel_ = app_.config().pathSearch;
}
}
else if ((iLevel_ == app_.config().pathSearchFast) && !fast)
{
// leaving fast pathfinding
iLevel_ = app_.config().pathSearch;
if (loaded && (iLevel_ > app_.config().pathSearchFast))
--iLevel_;
}
else if (bLastSuccess_)
{
// decrement, if possible
if (iLevel_ > app_.config().pathSearch ||
(loaded && (iLevel_ > app_.config().pathSearchFast)))
--iLevel_;
}
else
{
// adjust as needed
if (!loaded && (iLevel_ < app_.config().pathSearchMax))
++iLevel_;
if (loaded && (iLevel_ > app_.config().pathSearchFast))
--iLevel_;
}
JLOG(journal_.debug()) << iIdentifier_ << " processing at level " << iLevel_;
json::Value jvArray = json::ValueType::Array;
if (findPaths(cache, iLevel_, jvArray, continueCallback))
{
bLastSuccess_ = jvArray.size() != 0;
newStatus[jss::alternatives] = std::move(jvArray);
}
else
{
bLastSuccess_ = false;
newStatus = rpcError(RpcInternal);
}
if (fast && quickReply_ == steady_clock::time_point{})
{
quickReply_ = steady_clock::now();
owner_.reportFast(duration_cast<milliseconds>(quickReply_ - created_));
}
else if (!fast && fullReply_ == steady_clock::time_point{})
{
fullReply_ = steady_clock::now();
owner_.reportFull(duration_cast<milliseconds>(fullReply_ - created_));
}
{
std::scoped_lock const sl(lock_);
jvStatus_ = newStatus;
}
JLOG(journal_.debug()) << iIdentifier_ << " update finished " << (fast ? "fast" : "normal");
return newStatus;
}
InfoSub::pointer
PathRequest::getSubscriber() const
{
return wpSubscriber_.lock();
}
} // namespace xrpl