mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Fix pathfinding with multiple issuers for one currency (RIPD-618).
* Allow pathfinding requests where the starting currency may have multiple issuers. * Cache paths over all issuers to avoid repeating work. * Clear the ledger checkpoint in one retry case. * Add an additional node at the front of paths when the starting issuer is not the source account.
This commit is contained in:
@@ -22,6 +22,89 @@
|
|||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
|
class FindPaths::Impl {
|
||||||
|
public:
|
||||||
|
Impl (
|
||||||
|
RippleLineCache::ref cache,
|
||||||
|
Account const& srcAccount,
|
||||||
|
Account const& dstAccount,
|
||||||
|
STAmount const& dstAmount,
|
||||||
|
int searchLevel,
|
||||||
|
unsigned int maxPaths)
|
||||||
|
: cache_ (cache),
|
||||||
|
srcAccount_ (srcAccount),
|
||||||
|
dstAccount_ (dstAccount),
|
||||||
|
dstAmount_ (dstAmount),
|
||||||
|
searchLevel_ (searchLevel),
|
||||||
|
maxPaths_ (maxPaths)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool findPathsForIssue (
|
||||||
|
Issue const& issue,
|
||||||
|
STPathSet& pathsOut,
|
||||||
|
STPath& fullLiquidityPath)
|
||||||
|
{
|
||||||
|
if (auto& pathfinder = getPathFinder (issue.currency))
|
||||||
|
{
|
||||||
|
pathsOut = pathfinder->getBestPaths (
|
||||||
|
maxPaths_, fullLiquidityPath, issue.account);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
hash_map<Currency, std::unique_ptr<Pathfinder>> currencyMap_;
|
||||||
|
|
||||||
|
RippleLineCache::ref cache_;
|
||||||
|
Account const srcAccount_;
|
||||||
|
Account const dstAccount_;
|
||||||
|
STAmount const dstAmount_;
|
||||||
|
int const searchLevel_;
|
||||||
|
unsigned int const maxPaths_;
|
||||||
|
|
||||||
|
std::unique_ptr<Pathfinder> const& getPathFinder (Currency const& currency)
|
||||||
|
{
|
||||||
|
auto i = currencyMap_.find (currency);
|
||||||
|
if (i != currencyMap_.end ())
|
||||||
|
return i->second;
|
||||||
|
auto pathfinder = std::make_unique<Pathfinder> (
|
||||||
|
cache_, srcAccount_, dstAccount_, currency, dstAmount_);
|
||||||
|
if (pathfinder->findPaths (searchLevel_))
|
||||||
|
pathfinder->computePathRanks (maxPaths_);
|
||||||
|
else
|
||||||
|
pathfinder.reset (); // It's a bad request - clear it.
|
||||||
|
return currencyMap_[currency] = std::move (pathfinder);
|
||||||
|
|
||||||
|
// TODO(tom): why doesn't this faster way compile?
|
||||||
|
// return currencyMap_.insert (i, std::move (pathfinder)).second;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FindPaths::FindPaths (
|
||||||
|
RippleLineCache::ref cache,
|
||||||
|
Account const& srcAccount,
|
||||||
|
Account const& dstAccount,
|
||||||
|
STAmount const& dstAmount,
|
||||||
|
int level,
|
||||||
|
unsigned int maxPaths)
|
||||||
|
: impl_ (std::make_unique<Impl> (
|
||||||
|
cache, srcAccount, dstAccount, dstAmount, level, maxPaths))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
FindPaths::~FindPaths() = default;
|
||||||
|
|
||||||
|
bool FindPaths::findPathsForIssue (
|
||||||
|
Issue const& issue,
|
||||||
|
STPathSet& pathsOut,
|
||||||
|
STPath& fullLiquidityPath)
|
||||||
|
{
|
||||||
|
return impl_->findPathsForIssue (issue, pathsOut, fullLiquidityPath);
|
||||||
|
}
|
||||||
|
|
||||||
bool findPathsForOneIssuer (
|
bool findPathsForOneIssuer (
|
||||||
RippleLineCache::ref cache,
|
RippleLineCache::ref cache,
|
||||||
Account const& srcAccount,
|
Account const& srcAccount,
|
||||||
@@ -44,9 +127,9 @@ bool findPathsForOneIssuer (
|
|||||||
if (!pf.findPaths (searchLevel))
|
if (!pf.findPaths (searchLevel))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Yes, ensurePathsAreComplete is called BEFORE we compute the paths...
|
pf.addPathsFromPreviousPathfinding (pathsOut);
|
||||||
pf.ensurePathsAreComplete (pathsOut);
|
pf.computePathRanks (maxPaths);
|
||||||
pathsOut = pf.getBestPaths(maxPaths, fullLiquidityPath);
|
pathsOut = pf.getBestPaths(maxPaths, fullLiquidityPath, srcIssue.account);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,43 @@
|
|||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
|
class FindPaths
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FindPaths (
|
||||||
|
RippleLineCache::ref cache,
|
||||||
|
Account const& srcAccount,
|
||||||
|
Account const& dstAccount,
|
||||||
|
STAmount const& dstAmount,
|
||||||
|
/** searchLevel is the maximum search level allowed in an output path.
|
||||||
|
*/
|
||||||
|
int searchLevel,
|
||||||
|
/** maxPaths is the maximum number of paths that can be returned in
|
||||||
|
pathsOut. */
|
||||||
|
unsigned int const maxPaths);
|
||||||
|
~FindPaths();
|
||||||
|
|
||||||
|
bool findPathsForIssue (
|
||||||
|
Issue const& issue,
|
||||||
|
|
||||||
|
/** On input, pathsOut contains any paths you want to ensure are
|
||||||
|
included if still good.
|
||||||
|
|
||||||
|
On output, pathsOut will have any additional paths found. Only
|
||||||
|
non-default paths without source or destination will be added. */
|
||||||
|
STPathSet& pathsOut,
|
||||||
|
|
||||||
|
/** On input, fullLiquidityPath must be an empty STPath.
|
||||||
|
|
||||||
|
On output, if fullLiquidityPath is non-empty, it contains one extra
|
||||||
|
path that can move the entire liquidity requested. */
|
||||||
|
STPath& fullLiquidityPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
bool findPathsForOneIssuer (
|
bool findPathsForOneIssuer (
|
||||||
RippleLineCache::ref cache,
|
RippleLineCache::ref cache,
|
||||||
Account const& srcAccount,
|
Account const& srcAccount,
|
||||||
|
|||||||
@@ -331,6 +331,11 @@ int PathRequest::parseJson (Json::Value const& jvParams, bool complete)
|
|||||||
return PFR_PJ_INVALID;
|
return PFR_PJ_INVALID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uCur.isNonZero() && uIss.isZero())
|
||||||
|
{
|
||||||
|
uIss = raSrcAccount.getAccountID();
|
||||||
|
}
|
||||||
|
|
||||||
sciSourceCurrencies.insert ({uCur, uIss});
|
sciSourceCurrencies.insert ({uCur, uIss});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,6 +440,13 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
|
|||||||
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
|
|
||||||
|
FindPaths fp (
|
||||||
|
cache,
|
||||||
|
raSrcAccount.getAccountID (),
|
||||||
|
raDstAccount.getAccountID (),
|
||||||
|
saDstAmount,
|
||||||
|
iLevel,
|
||||||
|
4); // iMaxPaths
|
||||||
for (auto const& currIssuer: sourceCurrencies)
|
for (auto const& currIssuer: sourceCurrencies)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -448,14 +460,8 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
|
|||||||
}
|
}
|
||||||
STPathSet& spsPaths = mContext[currIssuer];
|
STPathSet& spsPaths = mContext[currIssuer];
|
||||||
STPath fullLiquidityPath;
|
STPath fullLiquidityPath;
|
||||||
auto valid = findPathsForOneIssuer (
|
auto valid = fp.findPathsForIssue (
|
||||||
cache,
|
|
||||||
raSrcAccount.getAccountID(),
|
|
||||||
raDstAccount.getAccountID(),
|
|
||||||
currIssuer,
|
currIssuer,
|
||||||
saDstAmount,
|
|
||||||
iLevel,
|
|
||||||
4, // iMaxPaths
|
|
||||||
spsPaths,
|
spsPaths,
|
||||||
fullLiquidityPath);
|
fullLiquidityPath);
|
||||||
CondLog (!valid, lsDEBUG, PathRequest)
|
CondLog (!valid, lsDEBUG, PathRequest)
|
||||||
@@ -464,12 +470,12 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
|
|||||||
if (valid)
|
if (valid)
|
||||||
{
|
{
|
||||||
LedgerEntrySet lesSandbox (cache->getLedger (), tapNONE);
|
LedgerEntrySet lesSandbox (cache->getLedger (), tapNONE);
|
||||||
auto& account = !isXRP (currIssuer.account)
|
auto& sourceAccount = !isXRP (currIssuer.account)
|
||||||
? currIssuer.account
|
? currIssuer.account
|
||||||
: isXRP (currIssuer.currency)
|
: isXRP (currIssuer.currency)
|
||||||
? xrpAccount()
|
? xrpAccount()
|
||||||
: raSrcAccount.getAccountID ();
|
: raSrcAccount.getAccountID ();
|
||||||
STAmount saMaxAmount ({currIssuer.currency, account}, 1);
|
STAmount saMaxAmount ({currIssuer.currency, sourceAccount}, 1);
|
||||||
|
|
||||||
saMaxAmount.negate ();
|
saMaxAmount.negate ();
|
||||||
m_journal.debug << iIdentifier
|
m_journal.debug << iIdentifier
|
||||||
@@ -488,7 +494,9 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
|
|||||||
m_journal.debug
|
m_journal.debug
|
||||||
<< iIdentifier << " Trying with an extra path element";
|
<< iIdentifier << " Trying with an extra path element";
|
||||||
spsPaths.push_back (fullLiquidityPath);
|
spsPaths.push_back (fullLiquidityPath);
|
||||||
rc = path::RippleCalc::rippleCalculate (lesSandbox,
|
lesSandbox.clear();
|
||||||
|
rc = path::RippleCalc::rippleCalculate (
|
||||||
|
lesSandbox,
|
||||||
saMaxAmount,
|
saMaxAmount,
|
||||||
saDstAmount,
|
saDstAmount,
|
||||||
raDstAccount.getAccountID (),
|
raDstAccount.getAccountID (),
|
||||||
@@ -507,6 +515,8 @@ Json::Value PathRequest::doUpdate (RippleLineCache::ref cache, bool fast)
|
|||||||
if (rc.result () == tesSUCCESS)
|
if (rc.result () == tesSUCCESS)
|
||||||
{
|
{
|
||||||
Json::Value jvEntry (Json::objectValue);
|
Json::Value jvEntry (Json::objectValue);
|
||||||
|
rc.actualAmountIn.setIssuer (sourceAccount);
|
||||||
|
|
||||||
jvEntry["source_amount"] = rc.actualAmountIn.getJson (0);
|
jvEntry["source_amount"] = rc.actualAmountIn.getJson (0);
|
||||||
jvEntry["paths_computed"] = spsPaths.getJson (0);
|
jvEntry["paths_computed"] = spsPaths.getJson (0);
|
||||||
found = true;
|
found = true;
|
||||||
|
|||||||
@@ -64,17 +64,10 @@ namespace {
|
|||||||
// width of path
|
// width of path
|
||||||
// correct currency at the end.
|
// correct currency at the end.
|
||||||
|
|
||||||
struct PathRank
|
|
||||||
{
|
|
||||||
std::uint64_t quality;
|
|
||||||
std::uint64_t length;
|
|
||||||
STAmount liquidity;
|
|
||||||
int index;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Compare two PathRanks. A better PathRank is lower, so the best are sorted to
|
// Compare two PathRanks. A better PathRank is lower, so the best are sorted to
|
||||||
// the beginning.
|
// the beginning.
|
||||||
bool comparePathRank (PathRank const& a, PathRank const& b)
|
bool comparePathRank (
|
||||||
|
Pathfinder::PathRank const& a, Pathfinder::PathRank const& b)
|
||||||
{
|
{
|
||||||
// 1) Higher quality (lower cost) is better
|
// 1) Higher quality (lower cost) is better
|
||||||
if (a.quality != b.quality)
|
if (a.quality != b.quality)
|
||||||
@@ -179,12 +172,32 @@ Pathfinder::Pathfinder (
|
|||||||
mDstAmount (saDstAmount),
|
mDstAmount (saDstAmount),
|
||||||
mSrcCurrency (uSrcCurrency),
|
mSrcCurrency (uSrcCurrency),
|
||||||
mSrcIssuer (uSrcIssuer),
|
mSrcIssuer (uSrcIssuer),
|
||||||
mSrcAmount ({mSrcCurrency, mSrcIssuer}, 1u, 0, true),
|
mSrcAmount ({uSrcCurrency, uSrcIssuer}, 1u, 0, true),
|
||||||
mLedger (cache->getLedger ()),
|
mLedger (cache->getLedger ()),
|
||||||
mRLCache (cache)
|
mRLCache (cache)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pathfinder::Pathfinder (
|
||||||
|
RippleLineCache::ref cache,
|
||||||
|
Account const& uSrcAccount,
|
||||||
|
Account const& uDstAccount,
|
||||||
|
Currency const& uSrcCurrency,
|
||||||
|
STAmount const& saDstAmount)
|
||||||
|
: mSrcAccount (uSrcAccount),
|
||||||
|
mDstAccount (uDstAccount),
|
||||||
|
mDstAmount (saDstAmount),
|
||||||
|
mSrcCurrency (uSrcCurrency),
|
||||||
|
mSrcAmount ({uSrcCurrency, uSrcAccount}, 1u, 0, true),
|
||||||
|
mLedger (cache->getLedger ()),
|
||||||
|
mRLCache (cache)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Pathfinder::~Pathfinder()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
bool Pathfinder::findPaths (int searchLevel)
|
bool Pathfinder::findPaths (int searchLevel)
|
||||||
{
|
{
|
||||||
if (mDstAmount == zero)
|
if (mDstAmount == zero)
|
||||||
@@ -210,19 +223,21 @@ bool Pathfinder::findPaths (int searchLevel)
|
|||||||
m_loadEvent = getApp ().getJobQueue ().getLoadEvent (
|
m_loadEvent = getApp ().getJobQueue ().getLoadEvent (
|
||||||
jtPATH_FIND, "FindPath");
|
jtPATH_FIND, "FindPath");
|
||||||
auto currencyIsXRP = isXRP (mSrcCurrency);
|
auto currencyIsXRP = isXRP (mSrcCurrency);
|
||||||
auto issuerIsXRP = isXRP (mSrcIssuer);
|
|
||||||
|
|
||||||
bool useIssuerAccount = !currencyIsXRP && !issuerIsXRP;
|
bool useIssuerAccount
|
||||||
auto& account = useIssuerAccount ? mSrcIssuer : mSrcAccount;
|
= mSrcIssuer && !currencyIsXRP && !isXRP (*mSrcIssuer);
|
||||||
|
auto& account = useIssuerAccount ? *mSrcIssuer : mSrcAccount;
|
||||||
auto issuer = currencyIsXRP ? Account() : account;
|
auto issuer = currencyIsXRP ? Account() : account;
|
||||||
mSource = STPathElement (account, mSrcCurrency, issuer);
|
mSource = STPathElement (account, mSrcCurrency, issuer);
|
||||||
|
auto issuerString = mSrcIssuer
|
||||||
|
? to_string (*mSrcIssuer) : std::string ("none");
|
||||||
WriteLog (lsTRACE, Pathfinder)
|
WriteLog (lsTRACE, Pathfinder)
|
||||||
<< "findPaths>"
|
<< "findPaths>"
|
||||||
<< " mSrcAccount=" << mSrcAccount
|
<< " mSrcAccount=" << mSrcAccount
|
||||||
<< " mDstAccount=" << mDstAccount
|
<< " mDstAccount=" << mDstAccount
|
||||||
<< " mDstAmount=" << mDstAmount.getFullText ()
|
<< " mDstAmount=" << mDstAmount.getFullText ()
|
||||||
<< " mSrcCurrency=" << mSrcCurrency
|
<< " mSrcCurrency=" << mSrcCurrency
|
||||||
<< " mSrcIssuer=" << mSrcIssuer;
|
<< " mSrcIssuer=" << issuerString;
|
||||||
|
|
||||||
if (!mLedger)
|
if (!mLedger)
|
||||||
{
|
{
|
||||||
@@ -318,11 +333,10 @@ bool Pathfinder::findPaths (int searchLevel)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Pathfinder::ensurePathsAreComplete (STPathSet& pathsOut)
|
void Pathfinder::addPathsFromPreviousPathfinding (STPathSet& pathsOut)
|
||||||
{
|
{
|
||||||
// Add any result paths that aren't in mCompletePaths.
|
// Add any result paths that aren't in mCompletePaths.
|
||||||
// TODO(tom): this is also quadratic in the size of the paths.
|
// TODO(tom): this is also quadratic in the size of the paths.
|
||||||
// TODO(tom): how could a path possibly not be in mCompletePaths?
|
|
||||||
for (auto const& path : pathsOut)
|
for (auto const& path : pathsOut)
|
||||||
{
|
{
|
||||||
// make sure no paths were lost
|
// make sure no paths were lost
|
||||||
@@ -338,9 +352,7 @@ void Pathfinder::ensurePathsAreComplete (STPathSet& pathsOut)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(tom): this asssert never triggers. We should probably
|
// TODO(tom): write a test that exercises this code path.
|
||||||
// remove this whole loop, which might be expensive.
|
|
||||||
assert (found);
|
|
||||||
if (!found)
|
if (!found)
|
||||||
mCompletePaths.push_back (path);
|
mCompletePaths.push_back (path);
|
||||||
}
|
}
|
||||||
@@ -410,14 +422,23 @@ TER Pathfinder::getPathLiquidity (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
|
namespace {
|
||||||
|
|
||||||
|
// Return the smallest amount of useful liquidity for a given amount, and the
|
||||||
|
// total number of paths we have to evaluate.
|
||||||
|
STAmount smallestUsefulAmount (STAmount const& amount, int maxPaths)
|
||||||
{
|
{
|
||||||
assert (fullLiquidityPath.empty ());
|
return divide (amount, STAmount (maxPaths + 2), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Pathfinder::computePathRanks (int maxPaths)
|
||||||
|
{
|
||||||
if (mCompletePaths.size () <= maxPaths)
|
if (mCompletePaths.size () <= maxPaths)
|
||||||
return mCompletePaths;
|
return;
|
||||||
|
|
||||||
STAmount remaining = mDstAmount;
|
mRemainingAmount = mDstAmount;
|
||||||
|
|
||||||
// Must subtract liquidity in default path from remaining amount.
|
// Must subtract liquidity in default path from remaining amount.
|
||||||
try
|
try
|
||||||
@@ -439,7 +460,7 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
|
|||||||
{
|
{
|
||||||
WriteLog (lsDEBUG, Pathfinder)
|
WriteLog (lsDEBUG, Pathfinder)
|
||||||
<< "Default path contributes: " << rc.actualAmountIn;
|
<< "Default path contributes: " << rc.actualAmountIn;
|
||||||
remaining -= rc.actualAmountOut;
|
mRemainingAmount -= rc.actualAmountOut;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -453,12 +474,9 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ignore paths that move only very small amounts.
|
// Ignore paths that move only very small amounts.
|
||||||
// TODO(tom): the logic of "very small" is pretty arbitrary here.
|
auto saMinDstAmount = smallestUsefulAmount (mDstAmount, maxPaths);
|
||||||
auto saMinDstAmount = divide (
|
|
||||||
mDstAmount, STAmount (maxPaths + 2), mDstAmount);
|
|
||||||
|
|
||||||
// Get the PathRank for each path.
|
// Get the PathRank for each path.
|
||||||
std::vector<PathRank> pathRanks;
|
|
||||||
for (int i = 0; i < mCompletePaths.size (); ++i)
|
for (int i = 0; i < mCompletePaths.size (); ++i)
|
||||||
{
|
{
|
||||||
auto const& currentPath = mCompletePaths[i];
|
auto const& currentPath = mCompletePaths[i];
|
||||||
@@ -479,46 +497,104 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
|
|||||||
"findPaths: quality: " << uQuality <<
|
"findPaths: quality: " << uQuality <<
|
||||||
": " << currentPath.getJson (0);
|
": " << currentPath.getJson (0);
|
||||||
|
|
||||||
pathRanks.push_back ({uQuality, currentPath.size (), liquidity, i});
|
mPathRanks.push_back ({uQuality, currentPath.size (), liquidity, i});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
std::sort (mPathRanks.begin (), mPathRanks.end (), comparePathRank);
|
||||||
|
}
|
||||||
|
|
||||||
STPathSet bestPaths;
|
static bool isDefaultPath (STPath const& path)
|
||||||
if (pathRanks.size ())
|
|
||||||
{
|
{
|
||||||
std::sort (pathRanks.begin (), pathRanks.end (), comparePathRank);
|
// TODO(tom): default paths can consist of more than just an account:
|
||||||
|
// https://forum.ripple.com/viewtopic.php?f=2&t=8206&start=10#p57713
|
||||||
|
//
|
||||||
|
// JoelKatz writes:
|
||||||
|
// So the test for whether a path is a default path is incorrect. I'm not
|
||||||
|
// sure it's worth the complexity of fixing though. If we are going to fix
|
||||||
|
// it, I'd suggest doing it this way:
|
||||||
|
//
|
||||||
|
// 1) Compute the default path, probably by using 'expandPath' to expand an
|
||||||
|
// empty path. 2) Chop off the source and destination nodes.
|
||||||
|
//
|
||||||
|
// 3) In the pathfinding loop, if the source issuer is not the sender,
|
||||||
|
// reject all paths that don't begin with the issuer's account node or match
|
||||||
|
// the path we built at step 2.
|
||||||
|
return path.size() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static STPath removeIssuer (STPath const& path)
|
||||||
|
{
|
||||||
|
// This path starts with the issuer, which is already implied
|
||||||
|
// so remove the head node
|
||||||
|
STPath ret;
|
||||||
|
|
||||||
|
for (auto it = path.begin() + 1; it != path.end(); ++it)
|
||||||
|
ret.push_back (*it);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
STPathSet Pathfinder::getBestPaths (
|
||||||
|
int maxPaths,
|
||||||
|
STPath& fullLiquidityPath,
|
||||||
|
Account const& srcIssuer)
|
||||||
|
{
|
||||||
|
assert (fullLiquidityPath.empty ());
|
||||||
|
const bool issuerIsSender = isXRP (mSrcCurrency) || (srcIssuer == mSrcAccount);
|
||||||
|
|
||||||
|
if (issuerIsSender && (mCompletePaths.size () <= maxPaths))
|
||||||
|
return mCompletePaths;
|
||||||
|
|
||||||
|
STPathSet bestPaths;
|
||||||
|
|
||||||
// The best PathRanks are now at the start. Pull off enough of them to
|
// The best PathRanks are now at the start. Pull off enough of them to
|
||||||
// fill bestPaths, then look through the rest for the best individual
|
// fill bestPaths, then look through the rest for the best individual
|
||||||
// path that can satisfy the entire liquidity - if one exists.
|
// path that can satisfy the entire liquidity - if one exists.
|
||||||
for (auto& pathRank: pathRanks)
|
STAmount remaining = mRemainingAmount;
|
||||||
|
for (auto& pathRank: mPathRanks)
|
||||||
{
|
{
|
||||||
auto iPathsLeft = maxPaths - bestPaths.size ();
|
auto iPathsLeft = maxPaths - bestPaths.size ();
|
||||||
if (!(iPathsLeft > 0 || fullLiquidityPath.empty ()))
|
if (!(iPathsLeft > 0 || fullLiquidityPath.empty ()))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
auto& path = mCompletePaths[pathRank.index];
|
||||||
|
assert (!path.empty ());
|
||||||
|
if (path.empty ())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool startsWithIssuer = false;
|
||||||
|
if (! issuerIsSender)
|
||||||
|
{
|
||||||
|
if (path.front ().getAccountID() != srcIssuer)
|
||||||
|
continue;
|
||||||
|
if (isDefaultPath (path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
startsWithIssuer = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (iPathsLeft > 1 ||
|
if (iPathsLeft > 1 ||
|
||||||
(iPathsLeft > 0 && pathRank.liquidity >= remaining))
|
(iPathsLeft > 0 && pathRank.liquidity >= remaining))
|
||||||
{
|
|
||||||
// last path must fill
|
// last path must fill
|
||||||
|
{
|
||||||
--iPathsLeft;
|
--iPathsLeft;
|
||||||
remaining -= pathRank.liquidity;
|
remaining -= pathRank.liquidity;
|
||||||
bestPaths.push_back (mCompletePaths[pathRank.index]);
|
bestPaths.push_back (startsWithIssuer ? removeIssuer (path) : path);
|
||||||
}
|
}
|
||||||
else if (iPathsLeft == 0 &&
|
else if (iPathsLeft == 0 &&
|
||||||
pathRank.liquidity >= mDstAmount &&
|
pathRank.liquidity >= mDstAmount &&
|
||||||
fullLiquidityPath.empty ())
|
fullLiquidityPath.empty ())
|
||||||
{
|
{
|
||||||
// We found an extra path that can move the whole amount.
|
// We found an extra path that can move the whole amount.
|
||||||
fullLiquidityPath = mCompletePaths[pathRank.index];
|
fullLiquidityPath = (startsWithIssuer ? removeIssuer (path) : path);
|
||||||
WriteLog (lsDEBUG, Pathfinder) <<
|
WriteLog (lsDEBUG, Pathfinder) <<
|
||||||
"Found extra full path: " << fullLiquidityPath.getJson (0);
|
"Found extra full path: " << fullLiquidityPath.getJson (0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
WriteLog (lsDEBUG, Pathfinder) <<
|
WriteLog (lsDEBUG, Pathfinder) <<
|
||||||
"Skipping a non-filling path: " <<
|
"Skipping a non-filling path: " << path.getJson (0);
|
||||||
mCompletePaths[pathRank.index].getJson (0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,22 +609,18 @@ STPathSet Pathfinder::getBestPaths (int maxPaths, STPath& fullLiquidityPath)
|
|||||||
WriteLog (lsDEBUG, Pathfinder) <<
|
WriteLog (lsDEBUG, Pathfinder) <<
|
||||||
"findPaths: RESULTS: " << bestPaths.getJson (0);
|
"findPaths: RESULTS: " << bestPaths.getJson (0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
WriteLog (lsDEBUG, Pathfinder) <<
|
|
||||||
"findPaths: RESULTS: non-defaults filtered away";
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestPaths;
|
return bestPaths;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pathfinder::issueMatchesOrigin (Issue const& issue)
|
bool Pathfinder::issueMatchesOrigin (Issue const& issue)
|
||||||
{
|
{
|
||||||
return issue.currency == mSrcCurrency &&
|
bool matchingCurrency = (issue.currency == mSrcCurrency);
|
||||||
(isXRP (issue.currency) ||
|
bool matchingAccount =
|
||||||
issue.account == mSrcIssuer ||
|
isXRP (issue.currency) ||
|
||||||
issue.account == mSrcAccount);
|
(mSrcIssuer && issue.account == mSrcIssuer) ||
|
||||||
|
issue.account == mSrcAccount;
|
||||||
|
|
||||||
|
return matchingCurrency && matchingAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Pathfinder::getPathsOut (
|
int Pathfinder::getPathsOut (
|
||||||
@@ -725,11 +797,9 @@ bool Pathfinder::isNoRippleOut (STPath const& currentPath)
|
|||||||
if (!(endElement.getNodeType () & STPathElement::typeAccount))
|
if (!(endElement.getNodeType () & STPathElement::typeAccount))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// What account are we leaving?
|
// If there's only one item in the path, return true if that item specifies
|
||||||
// TODO(tom): clarify what's going on here when we only have one item in the
|
// no ripple on the output. A path with no ripple on its output can't be
|
||||||
// path.
|
// followed by a link with no ripple on its input.
|
||||||
// TODO(tom): why aren't we checking that the previous node is also an
|
|
||||||
// account?
|
|
||||||
auto const& fromAccount = (currentPath.size () == 1)
|
auto const& fromAccount = (currentPath.size () == 1)
|
||||||
? mSrcAccount
|
? mSrcAccount
|
||||||
: (currentPath.end () - 2)->getAccountID ();
|
: (currentPath.end () - 2)->getAccountID ();
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace ripple {
|
|||||||
class Pathfinder
|
class Pathfinder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
/** Construct a pathfinder with an issuer.*/
|
||||||
Pathfinder (
|
Pathfinder (
|
||||||
RippleLineCache::ref cache,
|
RippleLineCache::ref cache,
|
||||||
Account const& srcAccount,
|
Account const& srcAccount,
|
||||||
@@ -39,18 +40,35 @@ public:
|
|||||||
Account const& uSrcIssuer,
|
Account const& uSrcIssuer,
|
||||||
STAmount const& dstAmount);
|
STAmount const& dstAmount);
|
||||||
|
|
||||||
|
/** Construct a pathfinder without an issuer.*/
|
||||||
|
Pathfinder (
|
||||||
|
RippleLineCache::ref cache,
|
||||||
|
Account const& srcAccount,
|
||||||
|
Account const& dstAccount,
|
||||||
|
Currency const& uSrcCurrency,
|
||||||
|
STAmount const& dstAmount);
|
||||||
|
|
||||||
|
~Pathfinder();
|
||||||
|
|
||||||
static void initPathTable ();
|
static void initPathTable ();
|
||||||
|
|
||||||
bool findPaths (int searchLevel);
|
bool findPaths (int searchLevel);
|
||||||
|
|
||||||
void ensurePathsAreComplete (STPathSet&);
|
/** Make sure that all the input paths are included in mCompletePaths. */
|
||||||
|
void addPathsFromPreviousPathfinding (STPathSet&);
|
||||||
|
|
||||||
|
/** Compute the rankings of the paths. */
|
||||||
|
void computePathRanks (int maxPaths);
|
||||||
|
|
||||||
/* Get the best paths, up to maxPaths in number, from mCompletePaths.
|
/* Get the best paths, up to maxPaths in number, from mCompletePaths.
|
||||||
|
|
||||||
On return, if fullLiquidityPath is not empty, then it contains the best
|
On return, if fullLiquidityPath is not empty, then it contains the best
|
||||||
additional single path which can consume all the liquidity.
|
additional single path which can consume all the liquidity.
|
||||||
*/
|
*/
|
||||||
STPathSet getBestPaths (int maxPaths, STPath& fullLiquidityPath);
|
STPathSet getBestPaths (
|
||||||
|
int maxPaths,
|
||||||
|
STPath& fullLiquidityPath,
|
||||||
|
Account const& srcIssuer);
|
||||||
|
|
||||||
enum NodeType
|
enum NodeType
|
||||||
{
|
{
|
||||||
@@ -76,9 +94,17 @@ public:
|
|||||||
pt_nonXRP_to_nonXRP // Destination currency is NOT the same as source.
|
pt_nonXRP_to_nonXRP // Destination currency is NOT the same as source.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PathRank
|
||||||
|
{
|
||||||
|
std::uint64_t quality;
|
||||||
|
std::uint64_t length;
|
||||||
|
STAmount liquidity;
|
||||||
|
int index;
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/*
|
/*
|
||||||
Call graph of methoids
|
Call graph of Pathfinder methods.
|
||||||
|
|
||||||
findPaths:
|
findPaths:
|
||||||
addPathsForType:
|
addPathsForType:
|
||||||
@@ -89,12 +115,14 @@ private:
|
|||||||
isNoRippleOut:
|
isNoRippleOut:
|
||||||
isNoRipple
|
isNoRipple
|
||||||
|
|
||||||
ensurePathsAreComplete
|
addPathsFromPreviousPathfinding
|
||||||
|
|
||||||
getBestPaths:
|
computePathRanks:
|
||||||
rippleCalculate
|
rippleCalculate
|
||||||
getPathLiquidity:
|
getPathLiquidity:
|
||||||
rippleCalculate
|
rippleCalculate
|
||||||
|
|
||||||
|
getBestPaths
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
@@ -143,8 +171,11 @@ private:
|
|||||||
Account mDstAccount;
|
Account mDstAccount;
|
||||||
STAmount mDstAmount;
|
STAmount mDstAmount;
|
||||||
Currency mSrcCurrency;
|
Currency mSrcCurrency;
|
||||||
Account mSrcIssuer;
|
boost::optional<Account> mSrcIssuer;
|
||||||
STAmount mSrcAmount;
|
STAmount mSrcAmount;
|
||||||
|
/** The amount remaining from mSrcAccount after the default liquidity has
|
||||||
|
been removed. */
|
||||||
|
STAmount mRemainingAmount;
|
||||||
|
|
||||||
Ledger::pointer mLedger;
|
Ledger::pointer mLedger;
|
||||||
LoadEvent::pointer m_loadEvent;
|
LoadEvent::pointer m_loadEvent;
|
||||||
@@ -152,6 +183,7 @@ private:
|
|||||||
|
|
||||||
STPathElement mSource;
|
STPathElement mSource;
|
||||||
STPathSet mCompletePaths;
|
STPathSet mCompletePaths;
|
||||||
|
std::vector<PathRank> mPathRanks;
|
||||||
std::map<PathType, STPathSet> mPaths;
|
std::map<PathType, STPathSet> mPaths;
|
||||||
|
|
||||||
hash_map<Issue, int> mPathsOutCountMap;
|
hash_map<Issue, int> mPathsOutCountMap;
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ Json::Value doRipplePathFind (RPC::Context& context)
|
|||||||
// Fill in currencies destination will accept
|
// Fill in currencies destination will accept
|
||||||
Json::Value jvDestCur (Json::arrayValue);
|
Json::Value jvDestCur (Json::arrayValue);
|
||||||
|
|
||||||
|
// TODO(tom): this could be optimized the same way that
|
||||||
|
// PathRequest::doUpdate() is - if we don't obsolete this code first.
|
||||||
auto usDestCurrID = accountDestCurrencies (raDst, cache, true);
|
auto usDestCurrID = accountDestCurrencies (raDst, cache, true);
|
||||||
for (auto const& uCurrency: usDestCurrID)
|
for (auto const& uCurrency: usDestCurrID)
|
||||||
jvDestCur.append (to_string (uCurrency));
|
jvDestCur.append (to_string (uCurrency));
|
||||||
@@ -144,6 +146,29 @@ Json::Value doRipplePathFind (RPC::Context& context)
|
|||||||
|
|
||||||
Json::Value jvArray (Json::arrayValue);
|
Json::Value jvArray (Json::arrayValue);
|
||||||
|
|
||||||
|
int level = getConfig().PATH_SEARCH_OLD;
|
||||||
|
if ((getConfig().PATH_SEARCH_MAX > level)
|
||||||
|
&& !getApp().getFeeTrack().isLoadedLocal())
|
||||||
|
{
|
||||||
|
++level;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.params_.isMember("depth")
|
||||||
|
&& context.params_["depth"].isIntegral())
|
||||||
|
{
|
||||||
|
int rLev = context.params_["search_depth"].asInt ();
|
||||||
|
if ((rLev < level) || (context.role_ == Config::ADMIN))
|
||||||
|
level = rLev;
|
||||||
|
}
|
||||||
|
|
||||||
|
FindPaths fp (
|
||||||
|
cache,
|
||||||
|
raSrc.getAccountID(),
|
||||||
|
raDst.getAccountID(),
|
||||||
|
saDstAmount,
|
||||||
|
level,
|
||||||
|
4); // max paths
|
||||||
|
|
||||||
for (unsigned int i = 0; i != jvSrcCurrencies.size (); ++i)
|
for (unsigned int i = 0; i != jvSrcCurrencies.size (); ++i)
|
||||||
{
|
{
|
||||||
Json::Value jvSource = jvSrcCurrencies[i];
|
Json::Value jvSource = jvSrcCurrencies[i];
|
||||||
@@ -203,14 +228,8 @@ Json::Value doRipplePathFind (RPC::Context& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
STPath fullLiquidityPath;
|
STPath fullLiquidityPath;
|
||||||
auto valid = findPathsForOneIssuer(
|
auto valid = fp.findPathsForIssue (
|
||||||
cache,
|
|
||||||
raSrc.getAccountID(),
|
|
||||||
raDst.getAccountID(),
|
|
||||||
{uSrcCurrencyID, uSrcIssuerID},
|
{uSrcCurrencyID, uSrcIssuerID},
|
||||||
saDstAmount,
|
|
||||||
level,
|
|
||||||
4, // iMaxPaths
|
|
||||||
spsComputed,
|
spsComputed,
|
||||||
fullLiquidityPath);
|
fullLiquidityPath);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ static Json::Value signPayment(
|
|||||||
&& params.isMember ("build_path"))
|
&& params.isMember ("build_path"))
|
||||||
{
|
{
|
||||||
// Need a ripple path.
|
// Need a ripple path.
|
||||||
STPathSet spsPaths;
|
|
||||||
Currency uSrcCurrencyID;
|
Currency uSrcCurrencyID;
|
||||||
Account uSrcIssuerID;
|
Account uSrcIssuerID;
|
||||||
|
|
||||||
@@ -154,6 +153,7 @@ static Json::Value signPayment(
|
|||||||
return rpcError (rpcTOO_BUSY);
|
return rpcError (rpcTOO_BUSY);
|
||||||
|
|
||||||
auto cache = std::make_shared<RippleLineCache> (lSnapshot);
|
auto cache = std::make_shared<RippleLineCache> (lSnapshot);
|
||||||
|
STPathSet spsPaths;
|
||||||
STPath fullLiquidityPath;
|
STPath fullLiquidityPath;
|
||||||
auto valid = findPathsForOneIssuer (
|
auto valid = findPathsForOneIssuer (
|
||||||
cache,
|
cache,
|
||||||
|
|||||||
Reference in New Issue
Block a user