mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
815 lines
29 KiB
C++
815 lines
29 KiB
C++
#pragma once
|
|
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/ledger/Credit.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/IOUAmount.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/tx/paths/Flow.h>
|
|
#include <xrpl/tx/paths/detail/AmountSpec.h>
|
|
#include <xrpl/tx/paths/detail/FlatSets.h>
|
|
#include <xrpl/tx/paths/detail/FlowDebugInfo.h>
|
|
#include <xrpl/tx/paths/detail/Steps.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMContext.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMHelpers.h>
|
|
|
|
#include <boost/container/flat_set.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
#include <numeric>
|
|
|
|
namespace xrpl {
|
|
|
|
/** Result of flow() execution of a single Strand. */
|
|
template <class TInAmt, class TOutAmt>
|
|
struct StrandResult
|
|
{
|
|
bool success; ///< Strand succeeded
|
|
TInAmt in = beast::zero; ///< Currency amount in
|
|
TOutAmt out = beast::zero; ///< Currency amount out
|
|
std::optional<PaymentSandbox> sandbox; ///< Resulting Sandbox state
|
|
boost::container::flat_set<uint256> ofrsToRm; ///< Offers to remove
|
|
// Num offers consumed or partially consumed (includes expired and unfunded
|
|
// offers)
|
|
std::uint32_t ofrsUsed = 0;
|
|
// strand can be inactive if there is no more liquidity or too many offers
|
|
// have been consumed
|
|
bool inactive = false; ///< Strand should not considered as a further
|
|
///< source of liquidity (dry)
|
|
|
|
/** Strand result constructor */
|
|
StrandResult() = default;
|
|
|
|
StrandResult(
|
|
Strand const& strand,
|
|
TInAmt const& in_,
|
|
TOutAmt const& out_,
|
|
PaymentSandbox&& sandbox_,
|
|
boost::container::flat_set<uint256> ofrsToRm_,
|
|
bool inactive_)
|
|
: success(true)
|
|
, in(in_)
|
|
, out(out_)
|
|
, sandbox(std::move(sandbox_))
|
|
, ofrsToRm(std::move(ofrsToRm_))
|
|
, ofrsUsed(offersUsed(strand))
|
|
, inactive(inactive_)
|
|
{
|
|
}
|
|
|
|
StrandResult(Strand const& strand, boost::container::flat_set<uint256> ofrsToRm_)
|
|
: success(false), ofrsToRm(std::move(ofrsToRm_)), ofrsUsed(offersUsed(strand))
|
|
{
|
|
}
|
|
};
|
|
|
|
/**
|
|
Request `out` amount from a strand
|
|
|
|
@param baseView Trust lines and balances
|
|
@param strand Steps of Accounts to ripple through and offer books to use
|
|
@param maxIn Max amount of input allowed
|
|
@param out Amount of output requested from the strand
|
|
@param j Journal to write log messages to
|
|
@return Actual amount in and out from the strand, errors, offers to remove,
|
|
and payment sandbox
|
|
*/
|
|
template <class TInAmt, class TOutAmt>
|
|
StrandResult<TInAmt, TOutAmt>
|
|
flow(
|
|
PaymentSandbox const& baseView,
|
|
Strand const& strand,
|
|
std::optional<TInAmt> const& maxIn,
|
|
TOutAmt const& out,
|
|
beast::Journal j)
|
|
{
|
|
using Result = StrandResult<TInAmt, TOutAmt>;
|
|
if (strand.empty())
|
|
{
|
|
JLOG(j.warn()) << "Empty strand passed to Liquidity";
|
|
return {};
|
|
}
|
|
|
|
boost::container::flat_set<uint256> ofrsToRm;
|
|
|
|
if (isDirectXrpToXrp<TInAmt, TOutAmt>(strand))
|
|
{
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
}
|
|
|
|
try
|
|
{
|
|
std::size_t const s = strand.size();
|
|
|
|
std::size_t limitingStep = strand.size();
|
|
std::optional<PaymentSandbox> sb(&baseView);
|
|
// The "all funds" view determines if an offer becomes unfunded or is
|
|
// found unfunded
|
|
// These are the account balances before the strand executes
|
|
std::optional<PaymentSandbox> afView(&baseView);
|
|
EitherAmount limitStepOut;
|
|
{
|
|
EitherAmount stepOut(out);
|
|
for (auto i = s; i--;)
|
|
{
|
|
auto r = strand[i]->rev(*sb, *afView, ofrsToRm, stepOut);
|
|
if (strand[i]->isZero(r.second))
|
|
{
|
|
JLOG(j.trace()) << "Strand found dry in rev";
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
}
|
|
|
|
if (i == 0 && maxIn && *maxIn < get<TInAmt>(r.first))
|
|
{
|
|
// limiting - exceeded maxIn
|
|
// Throw out previous results
|
|
sb.emplace(&baseView);
|
|
limitingStep = i;
|
|
|
|
// re-execute the limiting step
|
|
r = strand[i]->fwd(*sb, *afView, ofrsToRm, EitherAmount(*maxIn));
|
|
limitStepOut = r.second;
|
|
|
|
if (strand[i]->isZero(r.second))
|
|
{
|
|
JLOG(j.trace()) << "First step found dry";
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
}
|
|
if (get<TInAmt>(r.first) != *maxIn)
|
|
{
|
|
// Something is very wrong
|
|
// throwing out the sandbox can only increase liquidity
|
|
// yet the limiting is still limiting
|
|
// LCOV_EXCL_START
|
|
JLOG(j.fatal())
|
|
<< "Re-executed limiting step failed. r.first: "
|
|
<< to_string(get<TInAmt>(r.first)) << " maxIn: " << to_string(*maxIn);
|
|
UNREACHABLE(
|
|
"xrpl::flow : first step re-executing the "
|
|
"limiting step failed");
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
}
|
|
else if (!strand[i]->equalOut(r.second, stepOut))
|
|
{
|
|
// limiting
|
|
// Throw out previous results
|
|
sb.emplace(&baseView);
|
|
afView.emplace(&baseView);
|
|
limitingStep = i;
|
|
|
|
// re-execute the limiting step
|
|
stepOut = r.second;
|
|
r = strand[i]->rev(*sb, *afView, ofrsToRm, stepOut);
|
|
limitStepOut = r.second;
|
|
|
|
if (strand[i]->isZero(r.second))
|
|
{
|
|
// A tiny input amount can cause this step to output
|
|
// zero. I.e. 10^-80 IOU into an IOU -> XRP offer.
|
|
JLOG(j.trace()) << "Limiting step found dry";
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
}
|
|
if (!strand[i]->equalOut(r.second, stepOut))
|
|
{
|
|
// Something is very wrong
|
|
// throwing out the sandbox can only increase liquidity
|
|
// yet the limiting is still limiting
|
|
// LCOV_EXCL_START
|
|
#ifndef NDEBUG
|
|
JLOG(j.fatal())
|
|
<< "Re-executed limiting step failed. r.second: " << r.second
|
|
<< " stepOut: " << stepOut;
|
|
#else
|
|
JLOG(j.fatal()) << "Re-executed limiting step failed";
|
|
#endif
|
|
UNREACHABLE(
|
|
"xrpl::flow : limiting step re-executing the "
|
|
"limiting step failed");
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
}
|
|
|
|
// prev node needs to produce what this node wants to consume
|
|
stepOut = r.first;
|
|
}
|
|
}
|
|
|
|
{
|
|
EitherAmount stepIn(limitStepOut);
|
|
for (auto i = limitingStep + 1; i < s; ++i)
|
|
{
|
|
auto const r = strand[i]->fwd(*sb, *afView, ofrsToRm, stepIn);
|
|
if (strand[i]->isZero(r.second))
|
|
{
|
|
// A tiny input amount can cause this step to output zero.
|
|
// I.e. 10^-80 IOU into an IOU -> XRP offer.
|
|
JLOG(j.trace()) << "Non-limiting step found dry";
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
}
|
|
if (!strand[i]->equalIn(r.first, stepIn))
|
|
{
|
|
// The limits should already have been found, so executing a
|
|
// strand forward from the limiting step should not find a
|
|
// new limit
|
|
// LCOV_EXCL_START
|
|
#ifndef NDEBUG
|
|
JLOG(j.fatal()) << "Re-executed forward pass failed. r.first: " << r.first
|
|
<< " stepIn: " << stepIn;
|
|
#else
|
|
JLOG(j.fatal()) << "Re-executed forward pass failed";
|
|
#endif
|
|
UNREACHABLE(
|
|
"xrpl::flow : non-limiting step re-executing the "
|
|
"forward pass failed");
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
stepIn = r.second;
|
|
}
|
|
}
|
|
|
|
auto const strandIn = *strand.front()->cachedIn();
|
|
auto const strandOut = *strand.back()->cachedOut();
|
|
|
|
#ifndef NDEBUG
|
|
{
|
|
// Check that the strand will execute as intended
|
|
// Re-executing the strand will change the cached values
|
|
PaymentSandbox checkSB(&baseView);
|
|
PaymentSandbox checkAfView(&baseView);
|
|
EitherAmount stepIn(*strand[0]->cachedIn());
|
|
for (auto i = 0; i < s; ++i)
|
|
{
|
|
bool valid;
|
|
std::tie(valid, stepIn) = strand[i]->validFwd(checkSB, checkAfView, stepIn);
|
|
if (!valid)
|
|
{
|
|
JLOG(j.warn()) << "Strand re-execute check failed. Step: " << i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
bool const inactive =
|
|
std::any_of(strand.begin(), strand.end(), [](std::unique_ptr<Step> const& step) {
|
|
return step->inactive();
|
|
});
|
|
|
|
return Result(
|
|
strand,
|
|
get<TInAmt>(strandIn),
|
|
get<TOutAmt>(strandOut),
|
|
std::move(*sb),
|
|
std::move(ofrsToRm),
|
|
inactive);
|
|
}
|
|
catch (FlowException const&)
|
|
{
|
|
return Result{strand, std::move(ofrsToRm)};
|
|
}
|
|
}
|
|
|
|
/// @cond INTERNAL
|
|
template <class TInAmt, class TOutAmt>
|
|
struct FlowResult
|
|
{
|
|
TInAmt in = beast::zero;
|
|
TOutAmt out = beast::zero;
|
|
std::optional<PaymentSandbox> sandbox;
|
|
boost::container::flat_set<uint256> removableOffers;
|
|
TER ter = temUNKNOWN;
|
|
|
|
FlowResult() = default;
|
|
|
|
FlowResult(
|
|
TInAmt const& in_,
|
|
TOutAmt const& out_,
|
|
PaymentSandbox&& sandbox_,
|
|
boost::container::flat_set<uint256> ofrsToRm)
|
|
: in(in_)
|
|
, out(out_)
|
|
, sandbox(std::move(sandbox_))
|
|
, removableOffers(std::move(ofrsToRm))
|
|
, ter(tesSUCCESS)
|
|
{
|
|
}
|
|
|
|
FlowResult(TER ter_, boost::container::flat_set<uint256> ofrsToRm)
|
|
: removableOffers(std::move(ofrsToRm)), ter(ter_)
|
|
{
|
|
}
|
|
|
|
FlowResult(
|
|
TER ter_,
|
|
TInAmt const& in_,
|
|
TOutAmt const& out_,
|
|
boost::container::flat_set<uint256> ofrsToRm)
|
|
: in(in_), out(out_), removableOffers(std::move(ofrsToRm)), ter(ter_)
|
|
{
|
|
}
|
|
};
|
|
/// @endcond
|
|
|
|
/// @cond INTERNAL
|
|
inline std::optional<Quality>
|
|
qualityUpperBound(ReadView const& v, Strand const& strand)
|
|
{
|
|
Quality q{STAmount::uRateOne};
|
|
std::optional<Quality> stepQ;
|
|
DebtDirection dir = DebtDirection::issues;
|
|
for (auto const& step : strand)
|
|
{
|
|
if (std::tie(stepQ, dir) = step->qualityUpperBound(v, dir); stepQ)
|
|
q = composed_quality(q, *stepQ);
|
|
else
|
|
return std::nullopt;
|
|
}
|
|
return q;
|
|
};
|
|
/// @endcond
|
|
|
|
/// @cond INTERNAL
|
|
/** Limit remaining out only if one strand and limitQuality is included.
|
|
* Targets one path payment with AMM where the average quality is linear
|
|
* and instant quality is quadratic function of output. Calculating quality
|
|
* function for the whole strand enables figuring out required output
|
|
* to produce requested strand's limitQuality. Reducing the output,
|
|
* increases quality of AMM steps, increasing the strand's composite
|
|
* quality as the result.
|
|
*/
|
|
template <typename TOutAmt>
|
|
inline TOutAmt
|
|
limitOut(
|
|
ReadView const& v,
|
|
Strand const& strand,
|
|
TOutAmt const& remainingOut,
|
|
Quality const& limitQuality)
|
|
{
|
|
std::optional<QualityFunction> stepQualityFunc;
|
|
std::optional<QualityFunction> qf;
|
|
DebtDirection dir = DebtDirection::issues;
|
|
for (auto const& step : strand)
|
|
{
|
|
if (std::tie(stepQualityFunc, dir) = step->getQualityFunc(v, dir); stepQualityFunc)
|
|
{
|
|
if (!qf)
|
|
qf = stepQualityFunc;
|
|
else
|
|
qf->combine(*stepQualityFunc);
|
|
}
|
|
else
|
|
return remainingOut;
|
|
}
|
|
|
|
// QualityFunction is constant
|
|
if (!qf || qf->isConst())
|
|
return remainingOut;
|
|
|
|
auto const out = [&]() {
|
|
if (auto const out = qf->outFromAvgQ(limitQuality); !out)
|
|
return remainingOut;
|
|
else if constexpr (std::is_same_v<TOutAmt, XRPAmount>)
|
|
return XRPAmount{*out};
|
|
else if constexpr (std::is_same_v<TOutAmt, IOUAmount>)
|
|
return IOUAmount{*out};
|
|
else
|
|
return STAmount{remainingOut.issue(), out->mantissa(), out->exponent()};
|
|
}();
|
|
// A tiny difference could be due to the round off
|
|
if (withinRelativeDistance(out, remainingOut, Number(1, -9)))
|
|
return remainingOut;
|
|
return std::min(out, remainingOut);
|
|
};
|
|
/// @endcond
|
|
|
|
/// @cond INTERNAL
|
|
/* Track the non-dry strands
|
|
|
|
flow will search the non-dry strands (stored in `cur_`) for the best
|
|
available liquidity If flow doesn't use all the liquidity of a strand, that
|
|
strand is added to `next_`. The strands in `next_` are searched after the
|
|
current best liquidity is used.
|
|
*/
|
|
class ActiveStrands
|
|
{
|
|
private:
|
|
// Strands to be explored for liquidity
|
|
std::vector<Strand const*> cur_;
|
|
// Strands that may be explored for liquidity on the next iteration
|
|
std::vector<Strand const*> next_;
|
|
|
|
public:
|
|
ActiveStrands(std::vector<Strand> const& strands)
|
|
{
|
|
cur_.reserve(strands.size());
|
|
next_.reserve(strands.size());
|
|
for (auto& strand : strands)
|
|
next_.push_back(&strand);
|
|
}
|
|
|
|
// Start a new iteration in the search for liquidity
|
|
// Set the current strands to the strands in `next_`
|
|
void
|
|
activateNext(ReadView const& v, std::optional<Quality> const& limitQuality)
|
|
{
|
|
// add the strands in `next_` to `cur_`, sorted by theoretical quality.
|
|
// Best quality first.
|
|
cur_.clear();
|
|
if (!next_.empty())
|
|
{
|
|
std::vector<std::pair<Quality, Strand const*>> strandQualities;
|
|
strandQualities.reserve(next_.size());
|
|
if (next_.size() > 1) // no need to sort one strand
|
|
{
|
|
for (Strand const* strand : next_)
|
|
{
|
|
if (!strand)
|
|
{
|
|
// should not happen
|
|
continue;
|
|
}
|
|
if (auto const qual = qualityUpperBound(v, *strand))
|
|
{
|
|
if (limitQuality && *qual < *limitQuality)
|
|
{
|
|
// If a strand's quality is ever over limitQuality
|
|
// it is no longer part of the candidate set. Note
|
|
// that when transfer fees are charged, and an
|
|
// account goes from redeeming to issuing then
|
|
// strand quality _can_ increase; However, this is
|
|
// an unusual corner case.
|
|
continue;
|
|
}
|
|
strandQualities.push_back({*qual, strand});
|
|
}
|
|
}
|
|
// must stable sort for deterministic order across different c++
|
|
// standard library implementations
|
|
std::stable_sort(
|
|
strandQualities.begin(),
|
|
strandQualities.end(),
|
|
[](auto const& lhs, auto const& rhs) {
|
|
// higher qualities first
|
|
return std::get<Quality>(lhs) > std::get<Quality>(rhs);
|
|
});
|
|
next_.clear();
|
|
next_.reserve(strandQualities.size());
|
|
for (auto const& sq : strandQualities)
|
|
{
|
|
next_.push_back(std::get<Strand const*>(sq));
|
|
}
|
|
}
|
|
}
|
|
std::swap(cur_, next_);
|
|
}
|
|
|
|
Strand const*
|
|
get(size_t i) const
|
|
{
|
|
if (i >= cur_.size())
|
|
{
|
|
// LCOV_EXCL_START
|
|
UNREACHABLE("xrpl::ActiveStrands::get : input out of range");
|
|
return nullptr;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
return cur_[i];
|
|
}
|
|
|
|
void
|
|
push(Strand const* s)
|
|
{
|
|
next_.push_back(s);
|
|
}
|
|
|
|
// Push the strands from index i to the end of cur_ to next_
|
|
void
|
|
pushRemainingCurToNext(size_t i)
|
|
{
|
|
if (i >= cur_.size())
|
|
return;
|
|
next_.insert(next_.end(), std::next(cur_.begin(), i), cur_.end());
|
|
}
|
|
|
|
auto
|
|
size() const
|
|
{
|
|
return cur_.size();
|
|
}
|
|
|
|
void
|
|
removeIndex(std::size_t i)
|
|
{
|
|
if (i >= next_.size())
|
|
return;
|
|
next_.erase(next_.begin() + i);
|
|
}
|
|
};
|
|
/// @endcond
|
|
|
|
/**
|
|
Request `out` amount from a collection of strands
|
|
|
|
Attempt to fulfill the payment by using liquidity from the strands in order
|
|
from least expensive to most expensive
|
|
|
|
@param baseView Trust lines and balances
|
|
@param strands Each strand contains the steps of accounts to ripple through
|
|
and offer books to use
|
|
@param outReq Amount of output requested from the strand
|
|
@param partialPayment If true allow less than the full payment
|
|
@param offerCrossing If true offer crossing, not handling a standard payment
|
|
@param limitQuality If present, the minimum quality for any strand taken
|
|
@param sendMaxST If present, the maximum STAmount to send
|
|
@param j Journal to write journal messages to
|
|
@param ammContext counts iterations with AMM offers
|
|
@param flowDebugInfo If pointer is non-null, write flow debug info here
|
|
@return Actual amount in and out from the strands, errors, and payment
|
|
sandbox
|
|
*/
|
|
template <class TInAmt, class TOutAmt>
|
|
FlowResult<TInAmt, TOutAmt>
|
|
flow(
|
|
PaymentSandbox const& baseView,
|
|
std::vector<Strand> const& strands,
|
|
TOutAmt const& outReq,
|
|
bool partialPayment,
|
|
OfferCrossing offerCrossing,
|
|
std::optional<Quality> const& limitQuality,
|
|
std::optional<STAmount> const& sendMaxST,
|
|
beast::Journal j,
|
|
AMMContext& ammContext,
|
|
path::detail::FlowDebugInfo* flowDebugInfo = nullptr)
|
|
{
|
|
// Used to track the strand that offers the best quality (output/input
|
|
// ratio)
|
|
struct BestStrand
|
|
{
|
|
TInAmt in;
|
|
TOutAmt out;
|
|
PaymentSandbox sb;
|
|
Strand const& strand;
|
|
Quality quality;
|
|
|
|
BestStrand(
|
|
TInAmt const& in_,
|
|
TOutAmt const& out_,
|
|
PaymentSandbox&& sb_,
|
|
Strand const& strand_,
|
|
Quality const& quality_)
|
|
: in(in_), out(out_), sb(std::move(sb_)), strand(strand_), quality(quality_)
|
|
{
|
|
}
|
|
};
|
|
|
|
std::size_t const maxTries = 1000;
|
|
std::size_t curTry = 0;
|
|
std::uint32_t maxOffersToConsider = 1500;
|
|
std::uint32_t offersConsidered = 0;
|
|
|
|
// There is a bug in gcc that incorrectly warns about using uninitialized
|
|
// values if `remainingIn` is initialized through a copy constructor. We can
|
|
// get similar warnings for `sendMax` if it is initialized in the most
|
|
// natural way. Using `make_optional`, allows us to work around this bug.
|
|
TInAmt const sendMaxInit = sendMaxST ? toAmount<TInAmt>(*sendMaxST) : TInAmt{beast::zero};
|
|
std::optional<TInAmt> const sendMax =
|
|
(sendMaxST && sendMaxInit >= beast::zero) ? std::make_optional(sendMaxInit) : std::nullopt;
|
|
std::optional<TInAmt> remainingIn = !!sendMax ? std::make_optional(sendMaxInit) : std::nullopt;
|
|
// std::optional<TInAmt> remainingIn{sendMax};
|
|
|
|
TOutAmt remainingOut(outReq);
|
|
|
|
PaymentSandbox sb(&baseView);
|
|
|
|
// non-dry strands
|
|
ActiveStrands activeStrands(strands);
|
|
|
|
// Keeping a running sum of the amount in the order they are processed
|
|
// will not give the best precision. Keep a collection so they may be summed
|
|
// from smallest to largest
|
|
boost::container::flat_multiset<TInAmt> savedIns;
|
|
savedIns.reserve(maxTries);
|
|
boost::container::flat_multiset<TOutAmt> savedOuts;
|
|
savedOuts.reserve(maxTries);
|
|
|
|
auto sum = [](auto const& col) {
|
|
using TResult = std::decay_t<decltype(*col.begin())>;
|
|
if (col.empty())
|
|
return TResult{beast::zero};
|
|
return std::accumulate(col.begin() + 1, col.end(), *col.begin());
|
|
};
|
|
|
|
// These offers only need to be removed if the payment is not
|
|
// successful
|
|
boost::container::flat_set<uint256> ofrsToRmOnFail;
|
|
|
|
while (remainingOut > beast::zero && (!remainingIn || *remainingIn > beast::zero))
|
|
{
|
|
++curTry;
|
|
if (curTry >= maxTries)
|
|
{
|
|
return {telFAILED_PROCESSING, std::move(ofrsToRmOnFail)};
|
|
}
|
|
|
|
activeStrands.activateNext(sb, limitQuality);
|
|
|
|
ammContext.setMultiPath(activeStrands.size() > 1);
|
|
|
|
// Limit only if one strand and limitQuality
|
|
auto const limitRemainingOut = [&]() {
|
|
if (activeStrands.size() == 1 && limitQuality)
|
|
if (auto const strand = activeStrands.get(0))
|
|
return limitOut(sb, *strand, remainingOut, *limitQuality);
|
|
return remainingOut;
|
|
}();
|
|
auto const adjustedRemOut = limitRemainingOut != remainingOut;
|
|
|
|
boost::container::flat_set<uint256> ofrsToRm;
|
|
std::optional<BestStrand> best;
|
|
if (flowDebugInfo)
|
|
flowDebugInfo->newLiquidityPass();
|
|
// Index of strand to mark as inactive (remove from the active list) if
|
|
// the liquidity is used. This is used for strands that consume too many
|
|
// offers Constructed as `false,0` to workaround a gcc warning about
|
|
// uninitialized variables
|
|
std::optional<std::size_t> markInactiveOnUse;
|
|
for (size_t strandIndex = 0, sie = activeStrands.size(); strandIndex != sie; ++strandIndex)
|
|
{
|
|
Strand const* strand = activeStrands.get(strandIndex);
|
|
if (!strand)
|
|
{
|
|
// should not happen
|
|
continue;
|
|
}
|
|
// Clear AMM liquidity used flag. The flag might still be set if
|
|
// the previous strand execution failed. It has to be reset
|
|
// since this strand might not have AMM liquidity.
|
|
ammContext.clear();
|
|
if (offerCrossing && limitQuality)
|
|
{
|
|
auto const strandQ = qualityUpperBound(sb, *strand);
|
|
if (!strandQ || *strandQ < *limitQuality)
|
|
continue;
|
|
}
|
|
auto f = flow<TInAmt, TOutAmt>(sb, *strand, remainingIn, limitRemainingOut, j);
|
|
|
|
// rm bad offers even if the strand fails
|
|
SetUnion(ofrsToRm, f.ofrsToRm);
|
|
|
|
offersConsidered += f.ofrsUsed;
|
|
|
|
if (!f.success || f.out == beast::zero)
|
|
continue;
|
|
|
|
if (flowDebugInfo)
|
|
flowDebugInfo->pushLiquiditySrc(EitherAmount(f.in), EitherAmount(f.out));
|
|
|
|
XRPL_ASSERT(
|
|
f.out <= remainingOut && f.sandbox && (!remainingIn || f.in <= *remainingIn),
|
|
"xrpl::flow : remaining constraints");
|
|
|
|
Quality const q(f.out, f.in);
|
|
|
|
JLOG(j.trace()) << "New flow iter (iter, in, out): " << curTry - 1 << " "
|
|
<< to_string(f.in) << " " << to_string(f.out);
|
|
|
|
// limitOut() finds output to generate exact requested
|
|
// limitQuality. But the actual limit quality might be slightly
|
|
// off due to the round off.
|
|
if (limitQuality && q < *limitQuality &&
|
|
(!adjustedRemOut || !withinRelativeDistance(q, *limitQuality, Number(1, -7))))
|
|
{
|
|
JLOG(j.trace()) << "Path rejected by limitQuality"
|
|
<< " limit: " << *limitQuality << " path q: " << q;
|
|
continue;
|
|
}
|
|
|
|
XRPL_ASSERT(!best, "xrpl::flow : best is unset");
|
|
if (!f.inactive)
|
|
activeStrands.push(strand);
|
|
best.emplace(f.in, f.out, std::move(*f.sandbox), *strand, q);
|
|
activeStrands.pushRemainingCurToNext(strandIndex + 1);
|
|
break;
|
|
}
|
|
|
|
bool const shouldBreak = !best || offersConsidered >= maxOffersToConsider;
|
|
|
|
if (best)
|
|
{
|
|
if (markInactiveOnUse)
|
|
{
|
|
activeStrands.removeIndex(*markInactiveOnUse);
|
|
markInactiveOnUse.reset();
|
|
}
|
|
savedIns.insert(best->in);
|
|
savedOuts.insert(best->out);
|
|
remainingOut = outReq - sum(savedOuts);
|
|
if (sendMax)
|
|
remainingIn = *sendMax - sum(savedIns);
|
|
|
|
if (flowDebugInfo)
|
|
flowDebugInfo->pushPass(
|
|
EitherAmount(best->in), EitherAmount(best->out), activeStrands.size());
|
|
|
|
JLOG(j.trace()) << "Best path: in: " << to_string(best->in)
|
|
<< " out: " << to_string(best->out)
|
|
<< " remainingOut: " << to_string(remainingOut);
|
|
|
|
best->sb.apply(sb);
|
|
ammContext.update();
|
|
}
|
|
else
|
|
{
|
|
JLOG(j.trace()) << "All strands dry.";
|
|
}
|
|
|
|
best.reset(); // view in best must be destroyed before modifying base
|
|
// view
|
|
if (!ofrsToRm.empty())
|
|
{
|
|
SetUnion(ofrsToRmOnFail, ofrsToRm);
|
|
for (auto const& o : ofrsToRm)
|
|
{
|
|
if (auto ok = sb.peek(keylet::offer(o)))
|
|
offerDelete(sb, ok, j);
|
|
}
|
|
}
|
|
|
|
if (shouldBreak)
|
|
break;
|
|
}
|
|
|
|
auto const actualOut = sum(savedOuts);
|
|
auto const actualIn = sum(savedIns);
|
|
|
|
JLOG(j.trace()) << "Total flow: in: " << to_string(actualIn)
|
|
<< " out: " << to_string(actualOut);
|
|
|
|
/* flowCross doesn't handle offer crossing with tfFillOrKill flag correctly.
|
|
* 1. If tfFillOrKill is set then the owner must receive the full
|
|
* TakerPays. We reverse pays and gets because during crossing
|
|
* we are taking, therefore the owner must deliver the full TakerPays and
|
|
* the entire TakerGets doesn't have to be spent.
|
|
* Pre-fixFillOrKill amendment code fails if the entire TakerGets
|
|
* is not spent. fixFillOrKill addresses this issue.
|
|
* 2. If tfSell is also set then the owner must spend the entire TakerGets
|
|
* even if it means obtaining more than TakerPays. Since the pays and gets
|
|
* are reversed, the owner must send the entire TakerGets.
|
|
*/
|
|
bool const fillOrKillEnabled = baseView.rules().enabled(fixFillOrKill);
|
|
|
|
if (actualOut != outReq)
|
|
{
|
|
if (actualOut > outReq)
|
|
{
|
|
// Rounding in the payment engine is causing this assert to
|
|
// sometimes fire with "dust" amounts. This is causing issues when
|
|
// running debug builds of rippled. While this issue still needs to
|
|
// be resolved, the assert is causing more harm than good at this
|
|
// point.
|
|
// UNREACHABLE("xrpl::flow : rounding error");
|
|
|
|
return {tefEXCEPTION, std::move(ofrsToRmOnFail)};
|
|
}
|
|
if (!partialPayment)
|
|
{
|
|
// If we're offerCrossing a !partialPayment, then we're
|
|
// handling tfFillOrKill.
|
|
// Pre-fixFillOrKill amendment:
|
|
// That case is handled below; not here.
|
|
// fixFillOrKill amendment:
|
|
// That case is handled here if tfSell is also not set; i.e,
|
|
// case 1.
|
|
if (!offerCrossing || (fillOrKillEnabled && offerCrossing != OfferCrossing::sell))
|
|
return {tecPATH_PARTIAL, actualIn, actualOut, std::move(ofrsToRmOnFail)};
|
|
}
|
|
else if (actualOut == beast::zero)
|
|
{
|
|
return {tecPATH_DRY, std::move(ofrsToRmOnFail)};
|
|
}
|
|
}
|
|
if (offerCrossing &&
|
|
(!partialPayment && (!fillOrKillEnabled || offerCrossing == OfferCrossing::sell)))
|
|
{
|
|
// If we're offer crossing and partialPayment is *not* true, then
|
|
// we're handling a FillOrKill offer. In this case remainingIn must
|
|
// be zero (all funds must be consumed) or else we kill the offer.
|
|
// Pre-fixFillOrKill amendment:
|
|
// Handles both cases 1. and 2.
|
|
// fixFillOrKill amendment:
|
|
// Handles 2. 1. is handled above and falls through for tfSell.
|
|
XRPL_ASSERT(remainingIn, "xrpl::flow : nonzero remainingIn");
|
|
if (remainingIn && *remainingIn != beast::zero)
|
|
return {tecPATH_PARTIAL, actualIn, actualOut, std::move(ofrsToRmOnFail)};
|
|
}
|
|
|
|
return {actualIn, actualOut, std::move(sb), std::move(ofrsToRmOnFail)};
|
|
}
|
|
|
|
} // namespace xrpl
|