rippled
Loading...
Searching...
No Matches
Pathfinder.cpp
1#include <xrpld/app/main/Application.h>
2#include <xrpld/rpc/detail/Pathfinder.h>
3#include <xrpld/rpc/detail/PathfinderUtils.h>
4#include <xrpld/rpc/detail/RippleLineCache.h>
5
6#include <xrpl/basics/join.h>
7#include <xrpl/core/JobQueue.h>
8#include <xrpl/json/to_string.h>
9#include <xrpl/ledger/OrderBookDB.h>
10#include <xrpl/ledger/PaymentSandbox.h>
11#include <xrpl/tx/paths/RippleCalc.h>
12
13#include <tuple>
14
15/*
16
17Core Pathfinding Engine
18
19The pathfinding request is identified by category, XRP to XRP, XRP to
20non-XRP, non-XRP to XRP, same currency non-XRP to non-XRP, cross-currency
21non-XRP to non-XRP. For each category, there is a table of paths that the
22pathfinder searches for. Complete paths are collected.
23
24Each complete path is then rated and sorted. Paths with no or trivial
25liquidity are dropped. Otherwise, paths are sorted based on quality,
26liquidity, and path length.
27
28Path slots are filled in quality (ratio of out to in) order, with the
29exception that the last path must have enough liquidity to complete the
30payment (assuming no liquidity overlap). In addition, if no selected path
31is capable of providing enough liquidity to complete the payment by itself,
32an extra "covering" path is returned.
33
34The selected paths are then tested to determine if they can complete the
35payment and, if so, at what cost. If they fail and a covering path was
36found, the test is repeated with the covering path. If this succeeds, the
37final paths and the estimated cost are returned.
38
39The engine permits the search depth to be selected and the paths table
40includes the depth at which each path type is found. A search depth of zero
41causes no searching to be done. Extra paths can also be injected, and this
42should be used to preserve previously-found paths across invocations for the
43same path request (particularly if the search depth may change).
44
45*/
46
47namespace xrpl {
48
49namespace {
50
51// This is an arbitrary cutoff, and it might cause us to miss other
52// good paths with this arbitrary cut off.
53constexpr std::size_t PATHFINDER_MAX_COMPLETE_PATHS = 1000;
54
55struct AccountCandidate
56{
57 int priority;
58 AccountID account;
59
60 static int const highPriority = 10000;
61};
62
63bool
64compareAccountCandidate(
65 std::uint32_t seq,
66 AccountCandidate const& first,
67 AccountCandidate const& second)
68{
69 if (first.priority < second.priority)
70 return false;
71
72 if (first.account > second.account)
73 return true;
74
75 return (first.priority ^ seq) < (second.priority ^ seq);
76}
77
78using AccountCandidates = std::vector<AccountCandidate>;
79
80struct CostedPath
81{
82 int searchLevel;
84};
85
86using CostedPathList = std::vector<CostedPath>;
87
89
90struct PathCost
91{
92 int cost;
93 char const* path;
94};
95using PathCostList = std::vector<PathCost>;
96
97PathTable mPathTable;
98
100pathTypeToString(Pathfinder::PathType const& type)
101{
102 std::string ret;
103
104 for (auto const& node : type)
105 {
106 switch (node)
107 {
109 ret.append("s");
110 break;
112 ret.append("a");
113 break;
115 ret.append("b");
116 break;
118 ret.append("x");
119 break;
121 ret.append("f");
122 break;
124 ret.append("d");
125 break;
126 }
127 }
128
129 return ret;
130}
131
132// Return the smallest amount of useful liquidity for a given amount, and the
133// total number of paths we have to evaluate.
134STAmount
135smallestUsefulAmount(STAmount const& amount, int maxPaths)
136{
137 return divide(amount, STAmount(maxPaths + 2), amount.issue());
138}
139} // namespace
140
143 AccountID const& uSrcAccount,
144 AccountID const& uDstAccount,
145 Currency const& uSrcCurrency,
146 std::optional<AccountID> const& uSrcIssuer,
147 STAmount const& saDstAmount,
148 std::optional<STAmount> const& srcAmount,
149 std::optional<uint256> const& domain,
150 Application& app)
151 : mSrcAccount(uSrcAccount)
152 , mDstAccount(uDstAccount)
153 , mEffectiveDst(isXRP(saDstAmount.getIssuer()) ? uDstAccount : saDstAmount.getIssuer())
154 , mDstAmount(saDstAmount)
155 , mSrcCurrency(uSrcCurrency)
156 , mSrcIssuer(uSrcIssuer)
157 , mSrcAmount(srcAmount.value_or(STAmount(
158 Issue{
159 uSrcCurrency,
160 uSrcIssuer.value_or(isXRP(uSrcCurrency) ? xrpAccount() : uSrcAccount)},
161 1u,
162 0,
163 true)))
164 , convert_all_(convertAllCheck(mDstAmount))
165 , mDomain(domain)
166 , mLedger(cache->getLedger())
167 , mRLCache(cache)
168 , app_(app)
169 , j_(app.getJournal("Pathfinder"))
170{
171 XRPL_ASSERT(
172 !uSrcIssuer || isXRP(uSrcCurrency) == isXRP(uSrcIssuer.value()),
173 "xrpl::Pathfinder::Pathfinder : valid inputs");
174}
175
176bool
177Pathfinder::findPaths(int searchLevel, std::function<bool(void)> const& continueCallback)
178{
179 JLOG(j_.trace()) << "findPaths start";
180 if (mDstAmount == beast::zero)
181 {
182 // No need to send zero money.
183 JLOG(j_.debug()) << "Destination amount was zero.";
184 mLedger.reset();
185 return false;
186
187 // TODO(tom): why do we reset the ledger just in this case and the one
188 // below - why don't we do it each time we return false?
189 }
190
193 {
194 // No need to send to same account with same currency.
195 JLOG(j_.debug()) << "Tried to send to same issuer";
196 mLedger.reset();
197 return false;
198 }
199
201 {
202 // Default path might work, but any path would loop
203 return true;
204 }
205
207 auto currencyIsXRP = isXRP(mSrcCurrency);
208
209 bool const useIssuerAccount = mSrcIssuer && !currencyIsXRP && !isXRP(*mSrcIssuer);
210 auto& account = useIssuerAccount ? *mSrcIssuer : mSrcAccount;
211 auto issuer = currencyIsXRP ? AccountID() : account;
212 mSource = STPathElement(account, mSrcCurrency, issuer);
213 auto issuerString = mSrcIssuer ? to_string(*mSrcIssuer) : std::string("none");
214 JLOG(j_.trace()) << "findPaths>"
215 << " mSrcAccount=" << mSrcAccount << " mDstAccount=" << mDstAccount
216 << " mDstAmount=" << mDstAmount.getFullText()
217 << " mSrcCurrency=" << mSrcCurrency << " mSrcIssuer=" << issuerString;
218
219 if (!mLedger)
220 {
221 JLOG(j_.debug()) << "findPaths< no ledger";
222 return false;
223 }
224
225 bool const bSrcXrp = isXRP(mSrcCurrency);
226 bool const bDstXrp = isXRP(mDstAmount.getCurrency());
227
228 if (!mLedger->exists(keylet::account(mSrcAccount)))
229 {
230 // We can't even start without a source account.
231 JLOG(j_.debug()) << "invalid source account";
232 return false;
233 }
234
236 {
237 JLOG(j_.debug()) << "Non-existent gateway";
238 return false;
239 }
240
241 if (!mLedger->exists(keylet::account(mDstAccount)))
242 {
243 // Can't find the destination account - we must be funding a new
244 // account.
245 if (!bDstXrp)
246 {
247 JLOG(j_.debug()) << "New account not being funded in XRP ";
248 return false;
249 }
250
251 auto const reserve = STAmount(mLedger->fees().reserve);
252 if (mDstAmount < reserve)
253 {
254 JLOG(j_.debug()) << "New account not getting enough funding: " << mDstAmount << " < "
255 << reserve;
256 return false;
257 }
258 }
259
260 // Now compute the payment type from the types of the source and destination
261 // currencies.
262 PaymentType paymentType = pt_XRP_to_XRP;
263 if (bSrcXrp && bDstXrp)
264 {
265 // XRP -> XRP
266 JLOG(j_.debug()) << "XRP to XRP payment";
267 paymentType = pt_XRP_to_XRP;
268 }
269 else if (bSrcXrp)
270 {
271 // XRP -> non-XRP
272 JLOG(j_.debug()) << "XRP to non-XRP payment";
273 paymentType = pt_XRP_to_nonXRP;
274 }
275 else if (bDstXrp)
276 {
277 // non-XRP -> XRP
278 JLOG(j_.debug()) << "non-XRP to XRP payment";
279 paymentType = pt_nonXRP_to_XRP;
280 }
281 else if (mSrcCurrency == mDstAmount.getCurrency())
282 {
283 // non-XRP -> non-XRP - Same currency
284 JLOG(j_.debug()) << "non-XRP to non-XRP - same currency";
285 paymentType = pt_nonXRP_to_same;
286 }
287 else
288 {
289 // non-XRP to non-XRP - Different currency
290 JLOG(j_.debug()) << "non-XRP to non-XRP - cross currency";
291 paymentType = pt_nonXRP_to_nonXRP;
292 }
293
294 // Now iterate over all paths for that paymentType.
295 for (auto const& costedPath : mPathTable[paymentType])
296 {
297 if (continueCallback && !continueCallback())
298 return false;
299 // Only use paths with at most the current search level.
300 if (costedPath.searchLevel <= searchLevel)
301 {
302 JLOG(j_.trace()) << "findPaths trying payment type " << paymentType;
303 addPathsForType(costedPath.type, continueCallback);
304
305 if (mCompletePaths.size() > PATHFINDER_MAX_COMPLETE_PATHS)
306 break;
307 }
308 }
309
310 JLOG(j_.debug()) << mCompletePaths.size() << " complete paths found";
311
312 // Even if we find no paths, default paths may work, and we don't check them
313 // currently.
314 return true;
315}
316
317TER
319 STPath const& path, // IN: The path to check.
320 STAmount const& minDstAmount, // IN: The minimum output this path must
321 // deliver to be worth keeping.
322 STAmount& amountOut, // OUT: The actual liquidity along the path.
323 uint64_t& qualityOut) const // OUT: The returned initial quality
324{
325 STPathSet pathSet;
326 pathSet.push_back(path);
327
329 rcInput.defaultPathsAllowed = false;
330
331 PaymentSandbox sandbox(&*mLedger, tapNONE);
332
333 try
334 {
335 // Compute a path that provides at least the minimum liquidity.
336 if (convert_all_)
337 rcInput.partialPaymentAllowed = true;
338
340 sandbox,
341 mSrcAmount,
342 minDstAmount,
343 mDstAccount,
344 mSrcAccount,
345 pathSet,
346 mDomain,
347 app_,
348 &rcInput);
349 // If we can't get even the minimum liquidity requested, we're done.
350 if (!isTesSuccess(rc.result()))
351 return rc.result();
352
353 qualityOut = getRate(rc.actualAmountOut, rc.actualAmountIn);
354 amountOut = rc.actualAmountOut;
355
356 if (!convert_all_)
357 {
358 // Now try to compute the remaining liquidity.
359 rcInput.partialPaymentAllowed = true;
361 sandbox,
362 mSrcAmount,
363 mDstAmount - amountOut,
364 mDstAccount,
365 mSrcAccount,
366 pathSet,
367 mDomain,
368 app_,
369 &rcInput);
370
371 // If we found further liquidity, add it into the result.
372 if (rc.result() == tesSUCCESS)
373 amountOut += rc.actualAmountOut;
374 }
375
376 return tesSUCCESS;
377 }
378 catch (std::exception const& e)
379 {
380 JLOG(j_.info()) << "checkpath: exception (" << e.what() << ") "
381 << path.getJson(JsonOptions::none);
382 return tefEXCEPTION;
383 }
384}
385
386void
387Pathfinder::computePathRanks(int maxPaths, std::function<bool(void)> const& continueCallback)
388{
390
391 // Must subtract liquidity in default path from remaining amount.
392 try
393 {
394 PaymentSandbox sandbox(&*mLedger, tapNONE);
395
397 rcInput.partialPaymentAllowed = true;
399 sandbox,
404 STPathSet(),
405 mDomain,
406 app_,
407 &rcInput);
408
409 if (rc.result() == tesSUCCESS)
410 {
411 JLOG(j_.debug()) << "Default path contributes: " << rc.actualAmountIn;
412 mRemainingAmount -= rc.actualAmountOut;
413 }
414 else
415 {
416 JLOG(j_.debug()) << "Default path fails: " << transToken(rc.result());
417 }
418 }
419 catch (std::exception const&)
420 {
421 JLOG(j_.debug()) << "Default path causes exception";
422 }
423
424 rankPaths(maxPaths, mCompletePaths, mPathRanks, continueCallback);
425}
426
427static bool
429{
430 // FIXME: default paths can consist of more than just an account:
431 //
432 // JoelKatz writes:
433 // So the test for whether a path is a default path is incorrect. I'm not
434 // sure it's worth the complexity of fixing though. If we are going to fix
435 // it, I'd suggest doing it this way:
436 //
437 // 1) Compute the default path, probably by using 'expandPath' to expand an
438 // empty path. 2) Chop off the source and destination nodes.
439 //
440 // 3) In the pathfinding loop, if the source issuer is not the sender,
441 // reject all paths that don't begin with the issuer's account node or match
442 // the path we built at step 2.
443 return path.size() == 1;
444}
445
446static STPath
448{
449 // This path starts with the issuer, which is already implied
450 // so remove the head node
451 STPath ret;
452
453 for (auto it = path.begin() + 1; it != path.end(); ++it)
454 ret.push_back(*it);
455
456 return ret;
457}
458
459// For each useful path in the input path set,
460// create a ranking entry in the output vector of path ranks
461void
463 int maxPaths,
464 STPathSet const& paths,
465 std::vector<PathRank>& rankedPaths,
466 std::function<bool(void)> const& continueCallback)
467{
468 JLOG(j_.trace()) << "rankPaths with " << paths.size() << " candidates, and " << maxPaths
469 << " maximum";
470 rankedPaths.clear();
471 rankedPaths.reserve(paths.size());
472
473 auto const saMinDstAmount = [&]() -> STAmount {
474 if (!convert_all_)
475 {
476 // Ignore paths that move only very small amounts.
477 return smallestUsefulAmount(mDstAmount, maxPaths);
478 }
479
480 // On convert_all_ partialPaymentAllowed will be set to true
481 // and requiring a huge amount will find the highest liquidity.
483 }();
484
485 for (int i = 0; i < paths.size(); ++i)
486 {
487 if (continueCallback && !continueCallback())
488 return;
489 auto const& currentPath = paths[i];
490 if (!currentPath.empty())
491 {
492 STAmount liquidity;
493 uint64_t uQuality = 0;
494 auto const resultCode =
495 getPathLiquidity(currentPath, saMinDstAmount, liquidity, uQuality);
496 if (!isTesSuccess(resultCode))
497 {
498 JLOG(j_.debug()) << "findPaths: dropping : " << transToken(resultCode) << ": "
499 << currentPath.getJson(JsonOptions::none);
500 }
501 else
502 {
503 JLOG(j_.debug()) << "findPaths: quality: " << uQuality << ": "
504 << currentPath.getJson(JsonOptions::none);
505
506 rankedPaths.push_back({uQuality, currentPath.size(), liquidity, i});
507 }
508 }
509 }
510
511 // Sort paths by:
512 // cost of path (when considering quality)
513 // width of path
514 // length of path
515 // A better PathRank is lower, best are sorted to the beginning.
516 std::sort(
517 rankedPaths.begin(),
518 rankedPaths.end(),
519 [&](Pathfinder::PathRank const& a, Pathfinder::PathRank const& b) {
520 // 1) Higher quality (lower cost) is better
521 if (!convert_all_ && a.quality != b.quality)
522 return a.quality < b.quality;
523
524 // 2) More liquidity (higher volume) is better
525 if (a.liquidity != b.liquidity)
526 return a.liquidity > b.liquidity;
527
528 // 3) Shorter paths are better
529 if (a.length != b.length)
530 return a.length < b.length;
531
532 // 4) Tie breaker
533 return a.index > b.index;
534 });
535}
536
539 int maxPaths,
540 STPath& fullLiquidityPath,
541 STPathSet const& extraPaths,
542 AccountID const& srcIssuer,
543 std::function<bool(void)> const& continueCallback)
544{
545 JLOG(j_.debug()) << "findPaths: " << mCompletePaths.size() << " paths and " << extraPaths.size()
546 << " extras";
547
548 if (mCompletePaths.empty() && extraPaths.empty())
549 return mCompletePaths;
550
551 XRPL_ASSERT(
552 fullLiquidityPath.empty(), "xrpl::Pathfinder::getBestPaths : first empty path result");
553 bool const issuerIsSender = isXRP(mSrcCurrency) || (srcIssuer == mSrcAccount);
554
555 std::vector<PathRank> extraPathRanks;
556 rankPaths(maxPaths, extraPaths, extraPathRanks, continueCallback);
557
558 STPathSet bestPaths;
559
560 // The best PathRanks are now at the start. Pull off enough of them to
561 // fill bestPaths, then look through the rest for the best individual
562 // path that can satisfy the entire liquidity - if one exists.
563 STAmount remaining = mRemainingAmount;
564
565 auto pathsIterator = mPathRanks.begin();
566 auto extraPathsIterator = extraPathRanks.begin();
567
568 while (pathsIterator != mPathRanks.end() || extraPathsIterator != extraPathRanks.end())
569 {
570 if (continueCallback && !continueCallback())
571 break;
572 bool usePath = false;
573 bool useExtraPath = false;
574
575 if (pathsIterator == mPathRanks.end())
576 {
577 useExtraPath = true;
578 }
579 else if (extraPathsIterator == extraPathRanks.end())
580 {
581 usePath = true;
582 }
583 else if (extraPathsIterator->quality < pathsIterator->quality)
584 {
585 useExtraPath = true;
586 }
587 else if (extraPathsIterator->quality > pathsIterator->quality)
588 {
589 usePath = true;
590 }
591 else if (extraPathsIterator->liquidity > pathsIterator->liquidity)
592 {
593 useExtraPath = true;
594 }
595 else if (extraPathsIterator->liquidity < pathsIterator->liquidity)
596 {
597 usePath = true;
598 }
599 else
600 {
601 // Risk is high they have identical liquidity
602 useExtraPath = true;
603 usePath = true;
604 }
605
606 auto& pathRank = usePath ? *pathsIterator : *extraPathsIterator;
607
608 auto const& path = usePath ? mCompletePaths[pathRank.index] : extraPaths[pathRank.index];
609
610 if (useExtraPath)
611 ++extraPathsIterator;
612
613 if (usePath)
614 ++pathsIterator;
615
616 auto iPathsLeft = maxPaths - bestPaths.size();
617 if (iPathsLeft <= 0 && !fullLiquidityPath.empty())
618 break;
619
620 if (path.empty())
621 {
622 // LCOV_EXCL_START
623 UNREACHABLE("xrpl::Pathfinder::getBestPaths : path not found");
624 continue;
625 // LCOV_EXCL_STOP
626 }
627
628 bool startsWithIssuer = false;
629
630 if (!issuerIsSender && usePath)
631 {
632 // Need to make sure path matches issuer constraints
633 if (isDefaultPath(path) || path.front().getAccountID() != srcIssuer)
634 {
635 continue;
636 }
637
638 startsWithIssuer = true;
639 }
640
641 if (iPathsLeft > 1 || (iPathsLeft > 0 && pathRank.liquidity >= remaining))
642 // last path must fill
643 {
644 --iPathsLeft;
645 remaining -= pathRank.liquidity;
646 bestPaths.push_back(startsWithIssuer ? removeIssuer(path) : path);
647 }
648 else if (iPathsLeft == 0 && pathRank.liquidity >= mDstAmount && fullLiquidityPath.empty())
649 {
650 // We found an extra path that can move the whole amount.
651 fullLiquidityPath = (startsWithIssuer ? removeIssuer(path) : path);
652 JLOG(j_.debug()) << "Found extra full path: "
653 << fullLiquidityPath.getJson(JsonOptions::none);
654 }
655 else
656 {
657 JLOG(j_.debug()) << "Skipping a non-filling path: " << path.getJson(JsonOptions::none);
658 }
659 }
660
661 if (remaining > beast::zero)
662 {
663 XRPL_ASSERT(
664 fullLiquidityPath.empty(), "xrpl::Pathfinder::getBestPaths : second empty path result");
665 JLOG(j_.info()) << "Paths could not send " << remaining << " of " << mDstAmount;
666 }
667 else
668 {
669 JLOG(j_.debug()) << "findPaths: RESULTS: " << bestPaths.getJson(JsonOptions::none);
670 }
671 return bestPaths;
672}
673
674bool
676{
677 bool const matchingCurrency = (issue.currency == mSrcCurrency);
678 bool const matchingAccount = isXRP(issue.currency) ||
679 (mSrcIssuer && issue.account == mSrcIssuer) || issue.account == mSrcAccount;
680
681 return matchingCurrency && matchingAccount;
682}
683
684int
686 Currency const& currency,
687 AccountID const& account,
688 LineDirection direction,
689 bool isDstCurrency,
690 AccountID const& dstAccount,
691 std::function<bool(void)> const& continueCallback)
692{
693 Issue const issue(currency, account);
694
695 auto [it, inserted] = mPathsOutCountMap.emplace(issue, 0);
696
697 // If it was already present, return the stored number of paths
698 if (!inserted)
699 return it->second;
700
701 auto sleAccount = mLedger->read(keylet::account(account));
702
703 if (!sleAccount)
704 return 0;
705
706 int const aFlags = sleAccount->getFieldU32(sfFlags);
707 bool const bAuthRequired = (aFlags & lsfRequireAuth) != 0;
708 bool const bFrozen = ((aFlags & lsfGlobalFreeze) != 0);
709
710 int count = 0;
711
712 if (!bFrozen)
713 {
714 count = app_.getOrderBookDB().getBookSize(issue, mDomain);
715
716 if (auto const lines = mRLCache->getRippleLines(account, direction))
717 {
718 for (auto const& rspEntry : *lines)
719 {
720 if (currency != rspEntry.getLimit().getCurrency())
721 {
722 }
723 else if (
724 rspEntry.getBalance() <= beast::zero &&
725 (!rspEntry.getLimitPeer() ||
726 -rspEntry.getBalance() >= rspEntry.getLimitPeer() ||
727 (bAuthRequired && !rspEntry.getAuth())))
728 {
729 }
730 else if (isDstCurrency && dstAccount == rspEntry.getAccountIDPeer())
731 {
732 count += 10000; // count a path to the destination extra
733 }
734 else if (rspEntry.getNoRipplePeer())
735 {
736 // This probably isn't a useful path out
737 }
738 else if (rspEntry.getFreezePeer())
739 {
740 // Not a useful path out
741 }
742 else
743 {
744 ++count;
745 }
746 }
747 }
748 }
749 it->second = count;
750 return count;
751}
752
753void
755 STPathSet const& currentPaths, // The paths to build from
756 STPathSet& incompletePaths, // The set of partial paths we add to
757 int addFlags,
758 std::function<bool(void)> const& continueCallback)
759{
760 JLOG(j_.debug()) << "addLink< on " << currentPaths.size() << " source(s), flags=" << addFlags;
761 for (auto const& path : currentPaths)
762 {
763 if (continueCallback && !continueCallback())
764 return;
765 addLink(path, incompletePaths, addFlags, continueCallback);
766 }
767}
768
771 PathType const& pathType,
772 std::function<bool(void)> const& continueCallback)
773{
774 JLOG(j_.debug()) << "addPathsForType " << CollectionAndDelimiter(pathType, ", ");
775 // See if the set of paths for this type already exists.
776 auto it = mPaths.find(pathType);
777 if (it != mPaths.end())
778 return it->second;
779
780 // Otherwise, if the type has no nodes, return the empty path.
781 if (pathType.empty())
782 return mPaths[pathType];
783 if (continueCallback && !continueCallback())
784 return mPaths[{}];
785
786 // Otherwise, get the paths for the parent PathType by calling
787 // addPathsForType recursively.
788 PathType parentPathType = pathType;
789 parentPathType.pop_back();
790
791 STPathSet const& parentPaths = addPathsForType(parentPathType, continueCallback);
792 STPathSet& pathsOut = mPaths[pathType];
793
794 JLOG(j_.debug()) << "getPaths< adding onto '" << pathTypeToString(parentPathType)
795 << "' to get '" << pathTypeToString(pathType) << "'";
796
797 int const initialSize = mCompletePaths.size();
798
799 // Add the last NodeType to the lists.
800 auto nodeType = pathType.back();
801 switch (nodeType)
802 {
803 case nt_SOURCE:
804 // Source must always be at the start, so pathsOut has to be empty.
805 XRPL_ASSERT(pathsOut.empty(), "xrpl::Pathfinder::addPathsForType : empty paths");
806 pathsOut.push_back(STPath());
807 break;
808
809 case nt_ACCOUNTS:
810 addLinks(parentPaths, pathsOut, afADD_ACCOUNTS, continueCallback);
811 break;
812
813 case nt_BOOKS:
814 addLinks(parentPaths, pathsOut, afADD_BOOKS, continueCallback);
815 break;
816
817 case nt_XRP_BOOK:
818 addLinks(parentPaths, pathsOut, afADD_BOOKS | afOB_XRP, continueCallback);
819 break;
820
821 case nt_DEST_BOOK:
822 addLinks(parentPaths, pathsOut, afADD_BOOKS | afOB_LAST, continueCallback);
823 break;
824
825 case nt_DESTINATION:
826 // FIXME: What if a different issuer was specified on the
827 // destination amount?
828 // TODO(tom): what does this even mean? Should it be a JIRA?
829 addLinks(parentPaths, pathsOut, afADD_ACCOUNTS | afAC_LAST, continueCallback);
830 break;
831 }
832
833 if (mCompletePaths.size() != initialSize)
834 {
835 JLOG(j_.debug()) << (mCompletePaths.size() - initialSize) << " complete paths added";
836 }
837
838 JLOG(j_.debug()) << "getPaths> " << pathsOut.size() << " partial paths found";
839 return pathsOut;
840}
841
842bool
844 AccountID const& fromAccount,
845 AccountID const& toAccount,
846 Currency const& currency)
847{
848 auto sleRipple = mLedger->read(keylet::line(toAccount, fromAccount, currency));
849
850 auto const flag((toAccount > fromAccount) ? lsfHighNoRipple : lsfLowNoRipple);
851
852 return sleRipple && ((sleRipple->getFieldU32(sfFlags) & flag) != 0u);
853}
854
855// Does this path end on an account-to-account link whose last account has
856// set "no ripple" on the link?
857bool
859{
860 // Must have at least one link.
861 if (currentPath.empty())
862 return false;
863
864 // Last link must be an account.
865 STPathElement const& endElement = currentPath.back();
866 if ((endElement.getNodeType() & STPathElement::typeAccount) == 0u)
867 return false;
868
869 // If there's only one item in the path, return true if that item specifies
870 // no ripple on the output. A path with no ripple on its output can't be
871 // followed by a link with no ripple on its input.
872 auto const& fromAccount =
873 (currentPath.size() == 1) ? mSrcAccount : (currentPath.end() - 2)->getAccountID();
874 auto const& toAccount = endElement.getAccountID();
875 return isNoRipple(fromAccount, toAccount, endElement.getCurrency());
876}
877
878void
879addUniquePath(STPathSet& pathSet, STPath const& path)
880{
881 // TODO(tom): building an STPathSet this way is quadratic in the size
882 // of the STPathSet!
883 for (auto const& p : pathSet)
884 {
885 if (p == path)
886 return;
887 }
888 pathSet.push_back(path);
889}
890
891void
893 STPath const& currentPath, // The path to build from
894 STPathSet& incompletePaths, // The set of partial paths we add to
895 int addFlags,
896 std::function<bool(void)> const& continueCallback)
897{
898 auto const& pathEnd = currentPath.empty() ? mSource : currentPath.back();
899 auto const& uEndCurrency = pathEnd.getCurrency();
900 auto const& uEndIssuer = pathEnd.getIssuerID();
901 auto const& uEndAccount = pathEnd.getAccountID();
902 bool const bOnXRP = uEndCurrency.isZero();
903
904 // Does pathfinding really need to get this to
905 // a gateway (the issuer of the destination amount)
906 // rather than the ultimate destination?
907 bool const hasEffectiveDestination = mEffectiveDst != mDstAccount;
908
909 JLOG(j_.trace()) << "addLink< flags=" << addFlags << " onXRP=" << bOnXRP
910 << " completePaths size=" << mCompletePaths.size();
911 JLOG(j_.trace()) << currentPath.getJson(JsonOptions::none);
912
913 if ((addFlags & afADD_ACCOUNTS) != 0u)
914 {
915 // add accounts
916 if (bOnXRP)
917 {
918 if (mDstAmount.native() && !currentPath.empty())
919 { // non-default path to XRP destination
920 JLOG(j_.trace()) << "complete path found ax: "
921 << currentPath.getJson(JsonOptions::none);
922 addUniquePath(mCompletePaths, currentPath);
923 }
924 }
925 else
926 {
927 // search for accounts to add
928 auto const sleEnd = mLedger->read(keylet::account(uEndAccount));
929
930 if (sleEnd)
931 {
932 bool const bRequireAuth((sleEnd->getFieldU32(sfFlags) & lsfRequireAuth) != 0u);
933 bool const bIsEndCurrency(uEndCurrency == mDstAmount.getCurrency());
934 bool const bIsNoRippleOut(isNoRippleOut(currentPath));
935 bool const bDestOnly((addFlags & afAC_LAST) != 0u);
936
937 if (auto const lines = mRLCache->getRippleLines(
938 uEndAccount,
940 {
941 auto& rippleLines = *lines;
942
943 AccountCandidates candidates;
944 candidates.reserve(rippleLines.size());
945
946 for (auto const& rs : rippleLines)
947 {
948 if (continueCallback && !continueCallback())
949 return;
950 auto const& acct = rs.getAccountIDPeer();
951 LineDirection const direction = rs.getDirectionPeer();
952
953 if (hasEffectiveDestination && (acct == mDstAccount))
954 {
955 // We skipped the gateway
956 continue;
957 }
958
959 bool const bToDestination = acct == mEffectiveDst;
960
961 if (bDestOnly && !bToDestination)
962 {
963 continue;
964 }
965
966 if ((uEndCurrency == rs.getLimit().getCurrency()) &&
967 !currentPath.hasSeen(acct, uEndCurrency, acct))
968 {
969 // path is for correct currency and has not been
970 // seen
971 if (rs.getBalance() <= beast::zero &&
972 (!rs.getLimitPeer() || -rs.getBalance() >= rs.getLimitPeer() ||
973 (bRequireAuth && !rs.getAuth())))
974 {
975 // path has no credit
976 }
977 else if (bIsNoRippleOut && rs.getNoRipple())
978 {
979 // Can't leave on this path
980 }
981 else if (bToDestination)
982 {
983 // destination is always worth trying
984 if (uEndCurrency == mDstAmount.getCurrency())
985 {
986 // this is a complete path
987 if (!currentPath.empty())
988 {
989 JLOG(j_.trace()) << "complete path found ae: "
990 << currentPath.getJson(JsonOptions::none);
991 addUniquePath(mCompletePaths, currentPath);
992 }
993 }
994 else if (!bDestOnly)
995 {
996 // this is a high-priority candidate
997 candidates.push_back({AccountCandidate::highPriority, acct});
998 }
999 }
1000 else if (acct == mSrcAccount)
1001 {
1002 // going back to the source is bad
1003 }
1004 else
1005 {
1006 // save this candidate
1007 int const out = getPathsOut(
1008 uEndCurrency,
1009 acct,
1010 direction,
1011 bIsEndCurrency,
1013 continueCallback);
1014 if (out != 0)
1015 candidates.push_back({out, acct});
1016 }
1017 }
1018 }
1019
1020 if (!candidates.empty())
1021 {
1022 std::sort(
1023 candidates.begin(),
1024 candidates.end(),
1025 std::bind(
1026 compareAccountCandidate,
1027 mLedger->seq(),
1028 std::placeholders::_1,
1029 std::placeholders::_2));
1030
1031 int count = candidates.size();
1032 // allow more paths from source
1033 if ((count > 10) && (uEndAccount != mSrcAccount))
1034 {
1035 count = 10;
1036 }
1037 else if (count > 50)
1038 {
1039 count = 50;
1040 }
1041
1042 auto it = candidates.begin();
1043 while (count-- != 0)
1044 {
1045 if (continueCallback && !continueCallback())
1046 return;
1047 // Add accounts to incompletePaths
1048 STPathElement const pathElement(
1049 STPathElement::typeAccount, it->account, uEndCurrency, it->account);
1050 incompletePaths.assembleAdd(currentPath, pathElement);
1051 ++it;
1052 }
1053 }
1054 }
1055 }
1056 else
1057 {
1058 JLOG(j_.warn()) << "Path ends on non-existent issuer";
1059 }
1060 }
1061 }
1062 if ((addFlags & afADD_BOOKS) != 0u)
1063 {
1064 // add order books
1065 if ((addFlags & afOB_XRP) != 0u)
1066 {
1067 // to XRP only
1068 if (!bOnXRP && app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer}, mDomain))
1069 {
1070 STPathElement const pathElement(
1072 incompletePaths.assembleAdd(currentPath, pathElement);
1073 }
1074 }
1075 else
1076 {
1077 bool const bDestOnly = (addFlags & afOB_LAST) != 0;
1078 auto books =
1079 app_.getOrderBookDB().getBooksByTakerPays({uEndCurrency, uEndIssuer}, mDomain);
1080 JLOG(j_.trace()) << books.size() << " books found from this currency/issuer";
1081
1082 for (auto const& book : books)
1083 {
1084 if (continueCallback && !continueCallback())
1085 return;
1086 if (!currentPath.hasSeen(xrpAccount(), book.out.currency, book.out.account) &&
1087 !issueMatchesOrigin(book.out) &&
1088 (!bDestOnly || (book.out.currency == mDstAmount.getCurrency())))
1089 {
1090 STPath newPath(currentPath);
1091
1092 if (book.out.currency.isZero())
1093 { // to XRP
1094
1095 // add the order book itself
1096 newPath.emplace_back(
1098
1100 {
1101 // destination is XRP, add account and path is
1102 // complete
1103 JLOG(j_.trace()) << "complete path found bx: "
1104 << currentPath.getJson(JsonOptions::none);
1105 addUniquePath(mCompletePaths, newPath);
1106 }
1107 else
1108 {
1109 incompletePaths.push_back(newPath);
1110 }
1111 }
1112 else if (!currentPath.hasSeen(
1113 book.out.account, book.out.currency, book.out.account))
1114 {
1115 // Don't want the book if we've already seen the issuer
1116 // book -> account -> book
1117 if ((newPath.size() >= 2) && (newPath.back().isAccount()) &&
1118 (newPath[newPath.size() - 2].isOffer()))
1119 {
1120 // replace the redundant account with the order book
1121 newPath[newPath.size() - 1] = STPathElement(
1123 xrpAccount(),
1124 book.out.currency,
1125 book.out.account);
1126 }
1127 else
1128 {
1129 // add the order book
1130 newPath.emplace_back(
1132 xrpAccount(),
1133 book.out.currency,
1134 book.out.account);
1135 }
1136
1137 if (hasEffectiveDestination && book.out.account == mDstAccount &&
1138 book.out.currency == mDstAmount.getCurrency())
1139 {
1140 // We skipped a required issuer
1141 }
1142 else if (
1143 book.out.account == mEffectiveDst &&
1144 book.out.currency == mDstAmount.getCurrency())
1145 { // with the destination account, this path is
1146 // complete
1147 JLOG(j_.trace()) << "complete path found ba: "
1148 << currentPath.getJson(JsonOptions::none);
1149 addUniquePath(mCompletePaths, newPath);
1150 }
1151 else
1152 {
1153 // add issuer's account, path still incomplete
1154 incompletePaths.assembleAdd(
1155 newPath,
1158 book.out.account,
1159 book.out.currency,
1160 book.out.account));
1161 }
1162 }
1163 }
1164 }
1165 }
1166 }
1167}
1168
1169namespace {
1170
1172makePath(char const* string)
1173{
1175
1176 while (true)
1177 {
1178 // NOLINTNEXTLINE(bugprone-switch-missing-default-case)
1179 switch (*string++)
1180 {
1181 case 's': // source
1183 break;
1184
1185 case 'a': // accounts
1187 break;
1188
1189 case 'b': // books
1191 break;
1192
1193 case 'x': // xrp book
1195 break;
1196
1197 case 'f': // book to final currency
1199 break;
1200
1201 case 'd':
1202 // Destination (with account, if required and not already
1203 // present).
1205 break;
1206
1207 case 0:
1208 return ret;
1209 }
1210 }
1211}
1212
1213void
1214fillPaths(Pathfinder::PaymentType type, PathCostList const& costs)
1215{
1216 auto& list = mPathTable[type];
1217 XRPL_ASSERT(list.empty(), "xrpl::fillPaths : empty paths");
1218 for (auto& cost : costs)
1219 list.push_back({cost.cost, makePath(cost.path)});
1220}
1221
1222} // namespace
1223
1224// Costs:
1225// 0 = minimum to make some payments possible
1226// 1 = include trivial paths to make common cases work
1227// 4 = normal fast search level
1228// 7 = normal slow search level
1229// 10 = most aggressive
1230
1231void
1233{
1234 // CAUTION: Do not include rules that build default paths
1235
1236 mPathTable.clear();
1237 fillPaths(pt_XRP_to_XRP, {});
1238 /* cspell: disable */
1239
1240 fillPaths(
1242 {{1, "sfd"}, // source -> book -> gateway
1243 {3, "sfad"}, // source -> book -> account -> destination
1244 {5, "sfaad"}, // source -> book -> account -> account -> destination
1245 {6, "sbfd"}, // source -> book -> book -> destination
1246 {8, "sbafd"}, // source -> book -> account -> book -> destination
1247 {9, "sbfad"}, // source -> book -> book -> account -> destination
1248 {10, "sbafad"}});
1249
1250 fillPaths(
1252 {{1, "sxd"}, // gateway buys XRP
1253 {2, "saxd"}, // source -> gateway -> book(XRP) -> dest
1254 {6, "saaxd"},
1255 {7, "sbxd"},
1256 {8, "sabxd"},
1257 {9, "sabaxd"}});
1258
1259 // non-XRP to non-XRP (same currency)
1260 fillPaths(
1262 {
1263 {1, "sad"}, // source -> gateway -> destination
1264 {1, "sfd"}, // source -> book -> destination
1265 {4, "safd"}, // source -> gateway -> book -> destination
1266 {4, "sfad"},
1267 {5, "saad"},
1268 {5, "sbfd"},
1269 {6, "sxfad"},
1270 {6, "safad"},
1271 {6, "saxfd"}, // source -> gateway -> book to XRP -> book ->
1272 // destination
1273 {6, "saxfad"},
1274 {6, "sabfd"}, // source -> gateway -> book -> book -> destination
1275 {7, "saaad"},
1276 });
1277
1278 // non-XRP to non-XRP (different currency)
1279 fillPaths(
1281 {
1282 {1, "sfad"},
1283 {1, "safd"},
1284 {3, "safad"},
1285 {4, "sxfd"},
1286 {5, "saxfd"},
1287 {5, "sxfad"},
1288 {5, "sbfd"},
1289 {6, "saxfad"},
1290 {6, "sabfd"},
1291 {7, "saafd"},
1292 {8, "saafad"},
1293 {9, "safaad"},
1294 });
1295 /* cspell: enable */
1296}
1297
1298} // namespace xrpl
T append(T... args)
T back(T... args)
T begin(T... args)
T bind(T... args)
Stream debug() const
Definition Journal.h:301
Stream info() const
Definition Journal.h:307
Stream trace() const
Severity stream access functions.
Definition Journal.h:295
Stream warn() const
Definition Journal.h:313
A currency issued by an account.
Definition Issue.h:13
Currency currency
Definition Issue.h:15
AccountID account
Definition Issue.h:16
std::unique_ptr< LoadEvent > makeLoadEvent(JobType t, std::string const &name)
Return a scoped LoadEvent.
Definition JobQueue.cpp:142
virtual std::vector< Book > getBooksByTakerPays(Issue const &issue, std::optional< Domain > const &domain=std::nullopt)=0
Get all order books that want a specific issue.
virtual bool isBookToXRP(Issue const &issue, std::optional< Domain > const &domain=std::nullopt)=0
Check if an order book to XRP exists for the given issue.
virtual int getBookSize(Issue const &issue, std::optional< Domain > const &domain=std::nullopt)=0
Get the count of order books that want a specific issue.
Currency mSrcCurrency
Definition Pathfinder.h:173
void addLinks(STPathSet const &currentPaths, STPathSet &incompletePaths, int addFlags, std::function< bool(void)> const &continueCallback)
void rankPaths(int maxPaths, STPathSet const &paths, std::vector< PathRank > &rankedPaths, std::function< bool(void)> const &continueCallback)
TER getPathLiquidity(STPath const &path, STAmount const &minDstAmount, STAmount &amountOut, uint64_t &qualityOut) const
AccountID mSrcAccount
Definition Pathfinder.h:169
std::shared_ptr< RippleLineCache > mRLCache
Definition Pathfinder.h:184
static std::uint32_t const afADD_ACCOUNTS
Definition Pathfinder.h:197
std::unique_ptr< LoadEvent > m_loadEvent
Definition Pathfinder.h:183
STAmount mDstAmount
Definition Pathfinder.h:172
Pathfinder(std::shared_ptr< RippleLineCache > const &cache, AccountID const &srcAccount, AccountID const &dstAccount, Currency const &uSrcCurrency, std::optional< AccountID > const &uSrcIssuer, STAmount const &dstAmount, std::optional< STAmount > const &srcAmount, std::optional< uint256 > const &domain, Application &app)
Construct a pathfinder without an issuer.
bool issueMatchesOrigin(Issue const &)
STPathSet getBestPaths(int maxPaths, STPath &fullLiquidityPath, STPathSet const &extraPaths, AccountID const &srcIssuer, std::function< bool(void)> const &continueCallback={})
void addLink(STPath const &currentPath, STPathSet &incompletePaths, int addFlags, std::function< bool(void)> const &continueCallback)
static std::uint32_t const afAC_LAST
Definition Pathfinder.h:209
std::optional< AccountID > mSrcIssuer
Definition Pathfinder.h:174
STPathElement mSource
Definition Pathfinder.h:186
std::vector< NodeType > PathType
Definition Pathfinder.h:71
static std::uint32_t const afOB_XRP
Definition Pathfinder.h:203
std::map< PathType, STPathSet > mPaths
Definition Pathfinder.h:189
hash_map< Issue, int > mPathsOutCountMap
Definition Pathfinder.h:191
STPathSet & addPathsForType(PathType const &type, std::function< bool(void)> const &continueCallback)
std::optional< uint256 > mDomain
Definition Pathfinder.h:180
bool isNoRipple(AccountID const &fromAccount, AccountID const &toAccount, Currency const &currency)
AccountID mEffectiveDst
Definition Pathfinder.h:171
bool findPaths(int searchLevel, std::function< bool(void)> const &continueCallback={})
Application & app_
Definition Pathfinder.h:193
int getPathsOut(Currency const &currency, AccountID const &account, LineDirection direction, bool isDestCurrency, AccountID const &dest, std::function< bool(void)> const &continueCallback)
static std::uint32_t const afOB_LAST
Definition Pathfinder.h:206
bool isNoRippleOut(STPath const &currentPath)
beast::Journal const j_
Definition Pathfinder.h:194
STAmount mSrcAmount
Definition Pathfinder.h:175
static void initPathTable()
STAmount mRemainingAmount
The amount remaining from mSrcAccount after the default liquidity has been removed.
Definition Pathfinder.h:178
std::shared_ptr< ReadView const > mLedger
Definition Pathfinder.h:182
static std::uint32_t const afADD_BOOKS
Definition Pathfinder.h:200
std::vector< PathRank > mPathRanks
Definition Pathfinder.h:188
STPathSet mCompletePaths
Definition Pathfinder.h:187
void computePathRanks(int maxPaths, std::function< bool(void)> const &continueCallback={})
Compute the rankings of the paths.
AccountID mDstAccount
Definition Pathfinder.h:170
A wrapper which makes credits unavailable to balances.
std::string getFullText() const override
Definition STAmount.cpp:646
Currency const & getCurrency() const
Definition STAmount.h:476
bool native() const noexcept
Definition STAmount.h:432
auto getNodeType() const
Definition STPathSet.h:295
AccountID const & getAccountID() const
Definition STPathSet.h:333
Currency const & getCurrency() const
Definition STPathSet.h:339
bool assembleAdd(STPath const &base, STPathElement const &tail)
void push_back(STPath const &e)
Definition STPathSet.h:486
std::vector< STPath >::size_type size() const
Definition STPathSet.h:474
bool empty() const
Definition STPathSet.h:480
Json::Value getJson(JsonOptions) const override
std::vector< STPathElement >::size_type size() const
Definition STPathSet.h:370
Json::Value getJson(JsonOptions) const
bool empty() const
Definition STPathSet.h:376
void push_back(STPathElement const &e)
Definition STPathSet.h:382
std::vector< STPathElement >::const_iterator end() const
Definition STPathSet.h:401
bool hasSeen(AccountID const &account, Currency const &currency, AccountID const &issuer) const
void emplace_back(Args &&... args)
Definition STPathSet.h:389
std::vector< STPathElement >::const_reference back() const
Definition STPathSet.h:413
std::vector< STPathElement >::const_iterator begin() const
Definition STPathSet.h:395
virtual JobQueue & getJobQueue()=0
virtual OrderBookDB & getOrderBookDB()=0
bool isZero() const
Definition base_uint.h:513
static Output rippleCalculate(PaymentSandbox &view, STAmount const &saMaxAmountReq, STAmount const &saDstAmountReq, AccountID const &uDstAccountID, AccountID const &uSrcAccountID, STPathSet const &spsPaths, std::optional< uint256 > const &domainID, ServiceRegistry &registry, Input const *const pInputs=nullptr)
T clear(T... args)
T empty(T... args)
T end(T... args)
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:220
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:165
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
STAmount divide(STAmount const &amount, Rate const &rate)
Definition Rate2.cpp:69
STAmount convertAmount(STAmount const &amt, bool all)
bool isXRP(AccountID const &c)
Definition AccountID.h:70
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:602
STAmount largestAmount(STAmount const &amt)
@ tefEXCEPTION
Definition TER.h:152
base_uint< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
static STPath removeIssuer(STPath const &path)
std::string transToken(TER code)
Definition TER.cpp:243
Currency const & xrpCurrency()
XRP currency.
Definition UintTypes.cpp:98
static bool isDefaultPath(STPath const &path)
LineDirection
Describes how an account was found in a path, and how to find the next set of paths.
Definition TrustLine.h:21
@ jtPATH_FIND
Definition Job.h:63
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:450
bool convertAllCheck(STAmount const &a)
void addUniquePath(STPathSet &pathSet, STPath const &path)
@ tapNONE
Definition ApplyView.h:11
bool isTesSuccess(TER x) noexcept
Definition TER.h:651
AccountID const & xrpAccount()
Compute AccountID from public key.
@ tesSUCCESS
Definition TER.h:225
T pop_back(T... args)
T push_back(T... args)
T reserve(T... args)
T sort(T... args)
T value(T... args)
T what(T... args)