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