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