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