Optimize payment path exploration in flow:

* Use theoretical quality to order the strands
* Do not use strands below the user specified quality limit
* Stop exploring strands (at the current quality iteration) once any strand is non-dry
This commit is contained in:
seelabs
2020-06-25 08:13:49 -07:00
committed by manojsdoshi
parent 0a1fb4e6ca
commit fe129e8e4f
8 changed files with 318 additions and 93 deletions

View File

@@ -52,6 +52,14 @@ protected:
bool const ownerPaysTransferFee_; bool const ownerPaysTransferFee_;
// Mark as inactive (dry) if too many offers are consumed // Mark as inactive (dry) if too many offers are consumed
bool inactive_ = false; bool inactive_ = false;
/** Number of offers consumed or partially consumed the last time
the step ran, including expired and unfunded offers.
N.B. This this not the total number offers consumed by this step for the
entire payment, it is only the number the last time it ran. Offers may
be partially consumed multiple times during a payment.
*/
std::uint32_t offersUsed_ = 0;
beast::Journal const j_; beast::Journal const j_;
struct Cache struct Cache
@@ -125,6 +133,9 @@ public:
qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) qualityUpperBound(ReadView const& v, DebtDirection prevStepDir)
const override; const override;
std::uint32_t
offersUsed() const override;
std::pair<TIn, TOut> std::pair<TIn, TOut>
revImp( revImp(
PaymentSandbox& sb, PaymentSandbox& sb,
@@ -480,6 +491,13 @@ BookStep<TIn, TOut, TDerived>::qualityUpperBound(
return {q, dir}; return {q, dir};
} }
template <class TIn, class TOut, class TDerived>
std::uint32_t
BookStep<TIn, TOut, TDerived>::offersUsed() const
{
return offersUsed_;
}
// Adjust the offer amount and step amount subject to the given input limit // Adjust the offer amount and step amount subject to the given input limit
template <class TIn, class TOut> template <class TIn, class TOut>
static void static void
@@ -779,6 +797,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
auto const r = forEachOffer(sb, afView, prevStepDebtDir, eachOffer); auto const r = forEachOffer(sb, afView, prevStepDebtDir, eachOffer);
boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r)); boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r));
std::uint32_t const offersConsumed = std::get<1>(r); std::uint32_t const offersConsumed = std::get<1>(r);
offersUsed_ = offersConsumed;
SetUnion(ofrsToRm, toRm); SetUnion(ofrsToRm, toRm);
if (offersConsumed >= maxOffersToConsume_) if (offersConsumed >= maxOffersToConsume_)
@@ -948,6 +967,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
auto const r = forEachOffer(sb, afView, prevStepDebtDir, eachOffer); auto const r = forEachOffer(sb, afView, prevStepDebtDir, eachOffer);
boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r)); boost::container::flat_set<uint256> toRm = std::move(std::get<0>(r));
std::uint32_t const offersConsumed = std::get<1>(r); std::uint32_t const offersConsumed = std::get<1>(r);
offersUsed_ = offersConsumed;
SetUnion(ofrsToRm, toRm); SetUnion(ofrsToRm, toRm);
if (offersConsumed >= maxOffersToConsume_) if (offersConsumed >= maxOffersToConsume_)

View File

@@ -188,6 +188,19 @@ public:
virtual std::pair<boost::optional<Quality>, DebtDirection> virtual std::pair<boost::optional<Quality>, DebtDirection>
qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const = 0; qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const = 0;
/** Return the number of offers consumed or partially consumed the last time
the step ran, including expired and unfunded offers.
N.B. This this not the total number offers consumed by this step for the
entire payment, it is only the number the last time it ran. Offers may
be partially consumed multiple times during a payment.
*/
virtual std::uint32_t
offersUsed() const
{
return 0;
}
/** /**
If this step is a BookStep, return the book. If this step is a BookStep, return the book.
*/ */
@@ -281,6 +294,18 @@ private:
/// @cond INTERNAL /// @cond INTERNAL
using Strand = std::vector<std::unique_ptr<Step>>; using Strand = std::vector<std::unique_ptr<Step>>;
inline std::uint32_t
offersUsed(Strand const& strand)
{
std::uint32_t r = 0;
for (auto const& step : strand)
{
if (step)
r += step->offersUsed();
}
return r;
}
/// @endcond /// @endcond
/// @cond INTERNAL /// @cond INTERNAL

View File

@@ -29,6 +29,7 @@
#include <ripple/basics/IOUAmount.h> #include <ripple/basics/IOUAmount.h>
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/basics/XRPAmount.h> #include <ripple/basics/XRPAmount.h>
#include <ripple/protocol/Feature.h>
#include <boost/container/flat_set.hpp> #include <boost/container/flat_set.hpp>
@@ -48,6 +49,9 @@ struct StrandResult
TOutAmt out = beast::zero; ///< Currency amount out TOutAmt out = beast::zero; ///< Currency amount out
boost::optional<PaymentSandbox> sandbox; ///< Resulting Sandbox state boost::optional<PaymentSandbox> sandbox; ///< Resulting Sandbox state
boost::container::flat_set<uint256> ofrsToRm; ///< Offers to remove 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 // strand can be inactive if there is no more liquidity or too many offers
// have been consumed // have been consumed
bool inactive = false; ///< Strand should not considered as a further bool inactive = false; ///< Strand should not considered as a further
@@ -57,6 +61,7 @@ struct StrandResult
StrandResult() = default; StrandResult() = default;
StrandResult( StrandResult(
Strand const& strand,
TInAmt const& in_, TInAmt const& in_,
TOutAmt const& out_, TOutAmt const& out_,
PaymentSandbox&& sandbox_, PaymentSandbox&& sandbox_,
@@ -67,12 +72,17 @@ struct StrandResult
, out(out_) , out(out_)
, sandbox(std::move(sandbox_)) , sandbox(std::move(sandbox_))
, ofrsToRm(std::move(ofrsToRm_)) , ofrsToRm(std::move(ofrsToRm_))
, ofrsUsed(offersUsed(strand))
, inactive(inactive_) , inactive(inactive_)
{ {
} }
explicit StrandResult(boost::container::flat_set<uint256> ofrsToRm_) StrandResult(
: success(false), ofrsToRm(std::move(ofrsToRm_)) Strand const& strand,
boost::container::flat_set<uint256> ofrsToRm_)
: success(false)
, ofrsToRm(std::move(ofrsToRm_))
, ofrsUsed(offersUsed(strand))
{ {
} }
}; };
@@ -108,7 +118,7 @@ flow(
if (isDirectXrpToXrp<TInAmt, TOutAmt>(strand)) if (isDirectXrpToXrp<TInAmt, TOutAmt>(strand))
{ {
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
try try
@@ -130,7 +140,7 @@ flow(
if (strand[i]->isZero(r.second)) if (strand[i]->isZero(r.second))
{ {
JLOG(j.trace()) << "Strand found dry in rev"; JLOG(j.trace()) << "Strand found dry in rev";
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
if (i == 0 && maxIn && *maxIn < get<TInAmt>(r.first)) if (i == 0 && maxIn && *maxIn < get<TInAmt>(r.first))
@@ -148,7 +158,7 @@ flow(
if (strand[i]->isZero(r.second)) if (strand[i]->isZero(r.second))
{ {
JLOG(j.trace()) << "First step found dry"; JLOG(j.trace()) << "First step found dry";
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
if (get<TInAmt>(r.first) != *maxIn) if (get<TInAmt>(r.first) != *maxIn)
{ {
@@ -160,7 +170,7 @@ flow(
<< to_string(get<TInAmt>(r.first)) << to_string(get<TInAmt>(r.first))
<< " maxIn: " << to_string(*maxIn); << " maxIn: " << to_string(*maxIn);
assert(0); assert(0);
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
} }
else if (!strand[i]->equalOut(r.second, stepOut)) else if (!strand[i]->equalOut(r.second, stepOut))
@@ -181,7 +191,7 @@ flow(
// A tiny input amount can cause this step to output // A tiny input amount can cause this step to output
// zero. I.e. 10^-80 IOU into an IOU -> XRP offer. // zero. I.e. 10^-80 IOU into an IOU -> XRP offer.
JLOG(j.trace()) << "Limiting step found dry"; JLOG(j.trace()) << "Limiting step found dry";
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
if (!strand[i]->equalOut(r.second, stepOut)) if (!strand[i]->equalOut(r.second, stepOut))
{ {
@@ -196,7 +206,7 @@ flow(
JLOG(j.fatal()) << "Re-executed limiting step failed"; JLOG(j.fatal()) << "Re-executed limiting step failed";
#endif #endif
assert(0); assert(0);
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
} }
@@ -215,7 +225,7 @@ flow(
// A tiny input amount can cause this step to output zero. // A tiny input amount can cause this step to output zero.
// I.e. 10^-80 IOU into an IOU -> XRP offer. // I.e. 10^-80 IOU into an IOU -> XRP offer.
JLOG(j.trace()) << "Non-limiting step found dry"; JLOG(j.trace()) << "Non-limiting step found dry";
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
if (!strand[i]->equalIn(r.first, stepIn)) if (!strand[i]->equalIn(r.first, stepIn))
{ {
@@ -230,7 +240,7 @@ flow(
JLOG(j.fatal()) << "Re-executed forward pass failed"; JLOG(j.fatal()) << "Re-executed forward pass failed";
#endif #endif
assert(0); assert(0);
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
stepIn = r.second; stepIn = r.second;
} }
@@ -267,6 +277,7 @@ flow(
[](std::unique_ptr<Step> const& step) { return step->inactive(); }); [](std::unique_ptr<Step> const& step) { return step->inactive(); });
return Result( return Result(
strand,
get<TInAmt>(strandIn), get<TInAmt>(strandIn),
get<TOutAmt>(strandOut), get<TOutAmt>(strandOut),
std::move(*sb), std::move(*sb),
@@ -275,7 +286,7 @@ flow(
} }
catch (FlowException const&) catch (FlowException const&)
{ {
return Result{std::move(ofrsToRm)}; return Result{strand, std::move(ofrsToRm)};
} }
} }
@@ -366,41 +377,85 @@ public:
// Start a new iteration in the search for liquidity // Start a new iteration in the search for liquidity
// Set the current strands to the strands in `next_` // Set the current strands to the strands in `next_`
void void
activateNext() activateNext(
ReadView const& v,
boost::optional<Quality> const& limitQuality)
{ {
// Swap, don't move, so we keep the reserve in next_ // add the strands in `next_` to `cur_`, sorted by theoretical quality.
// Best quality first.
cur_.clear(); cur_.clear();
if (v.rules().enabled(featureFlowSortStrands) && !next_.empty())
{
std::vector<std::pair<Quality, Strand const*>> strandQuals;
strandQuals.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;
}
strandQuals.push_back({*qual, strand});
}
}
// must stable sort for deterministic order across different c++
// standard library implementations
std::stable_sort(
strandQuals.begin(),
strandQuals.end(),
[](auto const& lhs, auto const& rhs) {
// higher qualities first
return std::get<Quality>(lhs) > std::get<Quality>(rhs);
});
next_.clear();
next_.reserve(strandQuals.size());
for (auto const& sq : strandQuals)
{
next_.push_back(std::get<Strand const*>(sq));
}
}
}
std::swap(cur_, next_); std::swap(cur_, next_);
} }
Strand const*
get(size_t i) const
{
if (i >= cur_.size())
{
assert(0);
return nullptr;
}
return cur_[i];
}
void void
push(Strand const* s) push(Strand const* s)
{ {
next_.push_back(s); next_.push_back(s);
} }
auto // Push the strands from index i to the end of cur_ to next_
begin() void
pushRemainingCurToNext(size_t i)
{ {
return cur_.begin(); if (i >= cur_.size())
} return;
next_.insert(next_.end(), std::next(cur_.begin(), i), cur_.end());
auto
end()
{
return cur_.end();
}
auto
begin() const
{
return cur_.begin();
}
auto
end() const
{
return cur_.end();
} }
auto auto
@@ -422,7 +477,7 @@ public:
/** /**
Request `out` amount from a collection of strands Request `out` amount from a collection of strands
Attempt to fullfill the payment by using liquidity from the strands in order Attempt to fulfill the payment by using liquidity from the strands in order
from least expensive to most expensive from least expensive to most expensive
@param baseView Trust lines and balances @param baseView Trust lines and balances
@@ -478,6 +533,8 @@ flow(
std::size_t const maxTries = 1000; std::size_t const maxTries = 1000;
std::size_t curTry = 0; 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 // There is a bug in gcc that incorrectly warns about using uninitialized
// values if `remainingIn` is initialized through a copy constructor. We can // values if `remainingIn` is initialized through a copy constructor. We can
@@ -526,7 +583,7 @@ flow(
return {telFAILED_PROCESSING, std::move(ofrsToRmOnFail)}; return {telFAILED_PROCESSING, std::move(ofrsToRmOnFail)};
} }
activeStrands.activateNext(); activeStrands.activateNext(sb, limitQuality);
boost::container::flat_set<uint256> ofrsToRm; boost::container::flat_set<uint256> ofrsToRm;
boost::optional<BestStrand> best; boost::optional<BestStrand> best;
@@ -537,8 +594,16 @@ flow(
// offers Constructed as `false,0` to workaround a gcc warning about // offers Constructed as `false,0` to workaround a gcc warning about
// uninitialized variables // uninitialized variables
boost::optional<std::size_t> markInactiveOnUse{false, 0}; boost::optional<std::size_t> markInactiveOnUse{false, 0};
for (auto strand : activeStrands) for (size_t strandIndex = 0, sie = activeStrands.size();
strandIndex != sie;
++strandIndex)
{ {
Strand const* strand = activeStrands.get(strandIndex);
if (!strand)
{
// should not happen
continue;
}
if (offerCrossing && limitQuality) if (offerCrossing && limitQuality)
{ {
auto const strandQ = qualityUpperBound(sb, *strand); auto const strandQ = qualityUpperBound(sb, *strand);
@@ -551,6 +616,8 @@ flow(
// rm bad offers even if the strand fails // rm bad offers even if the strand fails
SetUnion(ofrsToRm, f.ofrsToRm); SetUnion(ofrsToRm, f.ofrsToRm);
offersConsidered += f.ofrsUsed;
if (!f.success || f.out == beast::zero) if (!f.success || f.out == beast::zero)
continue; continue;
@@ -576,26 +643,46 @@ flow(
continue; continue;
} }
if (baseView.rules().enabled(featureFlowSortStrands))
{
assert(!best);
if (!f.inactive)
activeStrands.push(strand);
best.emplace(f.in, f.out, std::move(*f.sandbox), *strand, q);
activeStrands.pushRemainingCurToNext(strandIndex + 1);
break;
}
activeStrands.push(strand); activeStrands.push(strand);
if (!best || best->quality < q || if (!best || best->quality < q ||
(best->quality == q && best->out < f.out)) (best->quality == q && best->out < f.out))
{ {
// If this strand is inactive (because it consumed too many // If this strand is inactive (because it consumed too many
// offers) and ends up having the best quality, remove it from // offers) and ends up having the best quality, remove it
// the activeStrands. If it doesn't end up having the best // from the activeStrands. If it doesn't end up having the
// quality, keep it active. // best quality, keep it active.
if (f.inactive) if (f.inactive)
{
// This should be `nextSize`, not `size`. This issue is
// fixed in featureFlowSortStrands.
markInactiveOnUse = activeStrands.size() - 1; markInactiveOnUse = activeStrands.size() - 1;
}
else else
{
markInactiveOnUse.reset(); markInactiveOnUse.reset();
}
best.emplace(f.in, f.out, std::move(*f.sandbox), *strand, q); best.emplace(f.in, f.out, std::move(*f.sandbox), *strand, q);
} }
} }
bool const shouldBreak = !best; bool const shouldBreak = [&] {
if (baseView.rules().enabled(featureFlowSortStrands))
return !best || offersConsidered >= maxOffersToConsider;
return !best;
}();
if (best) if (best)
{ {

View File

@@ -113,7 +113,8 @@ class FeatureCollections
"HardenedValidations", "HardenedValidations",
"fixAmendmentMajorityCalc", // Fix Amendment majority calculation "fixAmendmentMajorityCalc", // Fix Amendment majority calculation
"NegativeUNL", "NegativeUNL",
"TicketBatch"}; "TicketBatch",
"FlowSortStrands"};
std::vector<uint256> features; std::vector<uint256> features;
boost::container::flat_map<uint256, std::size_t> featureToIndex; boost::container::flat_map<uint256, std::size_t> featureToIndex;
@@ -371,6 +372,7 @@ extern uint256 const featureHardenedValidations;
extern uint256 const fixAmendmentMajorityCalc; extern uint256 const fixAmendmentMajorityCalc;
extern uint256 const featureNegativeUNL; extern uint256 const featureNegativeUNL;
extern uint256 const featureTicketBatch; extern uint256 const featureTicketBatch;
extern uint256 const featureFlowSortStrands;
} // namespace ripple } // namespace ripple

View File

@@ -133,6 +133,7 @@ detail::supportedAmendments()
"fixAmendmentMajorityCalc", "fixAmendmentMajorityCalc",
//"NegativeUNL", // Commented out to prevent automatic enablement //"NegativeUNL", // Commented out to prevent automatic enablement
"TicketBatch", "TicketBatch",
"FlowSortStrands",
}; };
return supported; return supported;
} }
@@ -186,7 +187,8 @@ uint256 const
featureHardenedValidations = *getRegisteredFeature("HardenedValidations"), featureHardenedValidations = *getRegisteredFeature("HardenedValidations"),
fixAmendmentMajorityCalc = *getRegisteredFeature("fixAmendmentMajorityCalc"), fixAmendmentMajorityCalc = *getRegisteredFeature("fixAmendmentMajorityCalc"),
featureNegativeUNL = *getRegisteredFeature("NegativeUNL"), featureNegativeUNL = *getRegisteredFeature("NegativeUNL"),
featureTicketBatch = *getRegisteredFeature("TicketBatch"); featureTicketBatch = *getRegisteredFeature("TicketBatch"),
featureFlowSortStrands = *getRegisteredFeature("FlowSortStrands");
// The following amendments have been active for at least two years. Their // The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated. // pre-amendment code has been removed and the identifiers are deprecated.

View File

@@ -311,6 +311,11 @@ public:
// second test the strand does not have the best quality (the // second test the strand does not have the best quality (the
// implementation has to handle this case correct and not mark the // implementation has to handle this case correct and not mark the
// strand dry until the liquidity is actually used) // strand dry until the liquidity is actually used)
// The implementation allows any single step to consume at most 1000
// offers. With the `FlowSortStrands` feature enabled, if the total
// number of offers consumed by all the steps combined exceeds 1500, the
// payment stops.
{ {
Env env(*this, features); Env env(*this, features);
@@ -324,7 +329,7 @@ public:
// Notice the strand with the 800 unfunded offers has the initial // Notice the strand with the 800 unfunded offers has the initial
// best quality // best quality
n_offers(env, 2000, alice, EUR(2), XRP(1)); n_offers(env, 2000, alice, EUR(2), XRP(1));
n_offers(env, 300, alice, XRP(1), USD(4)); n_offers(env, 100, alice, XRP(1), USD(4));
n_offers( n_offers(
env, 801, carol, XRP(1), USD(3)); // only one offer is funded env, 801, carol, XRP(1), USD(3)); // only one offer is funded
n_offers(env, 1000, alice, XRP(1), USD(3)); n_offers(env, 1000, alice, XRP(1), USD(3));
@@ -334,7 +339,10 @@ public:
// Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000 EUR // Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000 EUR
// 1. The best quality is the autobridged offers that take 2 EUR // 1. The best quality is the autobridged offers that take 2 EUR
// and give 4 USD. // and give 4 USD.
// Bob spends 600 EUR and receives 1200 USD. // Bob spends 200 EUR and receives 400 USD.
// 100 EUR->XRP offers consumed.
// 100 XRP->USD offers consumed.
// 200 total offers consumed.
// //
// 2. The best quality is the autobridged offers that take 2 EUR // 2. The best quality is the autobridged offers that take 2 EUR
// and give 3 USD. // and give 3 USD.
@@ -345,19 +353,27 @@ public:
// A book step is allowed to consume a maxium of 1000 offers // A book step is allowed to consume a maxium of 1000 offers
// at a given quality, and that limit is now reached. // at a given quality, and that limit is now reached.
// d. Now the strand is dry, even though there are still funded // d. Now the strand is dry, even though there are still funded
// XRP(1) to USD(3) offers available. Bob has spent 400 EUR and // XRP(1) to USD(3) offers available.
// received 600 USD in this step. (200 funded offers consumed // Bob has spent 400 EUR and received 600 USD in this step.
// 800 unfunded offers) // 200 EUR->XRP offers consumed
// 800 unfunded XRP->USD offers consumed
// 200 funded XRP->USD offers consumed (1 carol, 199 alice)
// 1400 total offers consumed so far (100 left before the
// limit)
// 3. The best is the non-autobridged offers that takes 500 EUR and // 3. The best is the non-autobridged offers that takes 500 EUR and
// gives 500 USD. // gives 500 USD.
// Bob has 2000 EUR, and has spent 600+400=1000 EUR. He has 1000 // Bob started with 2000 EUR
// left. Bob spent 500 EUR and receives 500 USD. // Bob spent 500 EUR (100+400)
// In total: Bob spent EUR(600 + 400 + 500) = EUR(1500). He started // Bob has 1500 EUR left
// with 2000 so has 500 remaining // In this step:
// Bob received USD(1200 + 600 + 500) = USD(2300). // Bob spents 500 EUR and receives 500 USD.
// Alice spent 300*4 + 199*3 + 500 = 2297 USD. She started // In total:
// with 4000 so has 1703 USD remaining. Alice received // Bob spent 1100 EUR (200 + 400 + 500)
// 600 + 400 + 500 = 1500 EUR // Bob has 900 EUR remaining (2000 - 1100)
// Bob received 1500 USD (400 + 600 + 500)
// Alice spent 1497 USD (100*4 + 199*3 + 500)
// Alice has 2503 remaining (4000 - 1497)
// Alice received 1100 EUR (200 + 400 + 500)
env.trust(EUR(10000), bob); env.trust(EUR(10000), bob);
env.close(); env.close();
env(pay(gw, bob, EUR(2000))); env(pay(gw, bob, EUR(2000)));
@@ -365,15 +381,15 @@ public:
env(offer(bob, USD(4000), EUR(4000))); env(offer(bob, USD(4000), EUR(4000)));
env.close(); env.close();
env.require(balance(bob, USD(2300))); env.require(balance(bob, USD(1500)));
env.require(balance(bob, EUR(500))); env.require(balance(bob, EUR(900)));
env.require(offers(bob, 1)); env.require(offers(bob, 1));
env.require(owners(bob, 3)); env.require(owners(bob, 3));
env.require(balance(alice, USD(1703))); env.require(balance(alice, USD(2503)));
env.require(balance(alice, EUR(1500))); env.require(balance(alice, EUR(1100)));
auto const numAOffers = auto const numAOffers =
2000 + 300 + 1000 + 1 - (2 * 300 + 2 * 199 + 1 + 1); 2000 + 100 + 1000 + 1 - (2 * 100 + 2 * 199 + 1 + 1);
env.require(offers(alice, numAOffers)); env.require(offers(alice, numAOffers));
env.require(owners(alice, numAOffers + 2)); env.require(owners(alice, numAOffers + 2));
@@ -393,7 +409,7 @@ public:
// initial best quality // initial best quality
n_offers(env, 1, alice, EUR(1), USD(10)); n_offers(env, 1, alice, EUR(1), USD(10));
n_offers(env, 2000, alice, EUR(2), XRP(1)); n_offers(env, 2000, alice, EUR(2), XRP(1));
n_offers(env, 300, alice, XRP(1), USD(4)); n_offers(env, 100, alice, XRP(1), USD(4));
n_offers( n_offers(
env, 801, carol, XRP(1), USD(3)); // only one offer is funded env, 801, carol, XRP(1), USD(3)); // only one offer is funded
n_offers(env, 1000, alice, XRP(1), USD(3)); n_offers(env, 1000, alice, XRP(1), USD(3));
@@ -407,7 +423,7 @@ public:
// //
// 2. The best quality is the autobridged offers that takes 2 EUR // 2. The best quality is the autobridged offers that takes 2 EUR
// and gives 4 USD. // and gives 4 USD.
// Bob spends 600 EUR and receives 1200 USD. // Bob spends 200 EUR and receives 400 USD.
// //
// 3. The best quality is the autobridged offers that takes 2 EUR // 3. The best quality is the autobridged offers that takes 2 EUR
// and gives 3 USD. // and gives 3 USD.
@@ -423,14 +439,14 @@ public:
// 800 unfunded offers) // 800 unfunded offers)
// 4. The best is the non-autobridged offers that takes 499 EUR and // 4. The best is the non-autobridged offers that takes 499 EUR and
// gives 499 USD. // gives 499 USD.
// Bob has 2000 EUR, and has spent 1+600+400=1001 EUR. He has // Bob has 2000 EUR, and has spent 1+200+400=601 EUR. He has
// 999 left. Bob spent 499 EUR and receives 499 USD. // 1399 left. Bob spent 499 EUR and receives 499 USD.
// In total: Bob spent EUR(1 + 600 + 400 + 499) = EUR(1500). He // In total: Bob spent EUR(1 + 200 + 400 + 499) = EUR(1100). He
// started with 2000 so has 500 remaining // started with 2000 so has 900 remaining
// Bob received USD(10 + 1200 + 600 + 499) = USD(2309). // Bob received USD(10 + 400 + 600 + 499) = USD(1509).
// Alice spent 10 + 300*4 + 199*3 + 499 = 2306 USD. She // Alice spent 10 + 100*4 + 199*3 + 499 = 1506 USD. She
// started with 4000 so has 1704 USD remaining. Alice // started with 4000 so has 2494 USD remaining. Alice
// received 600 + 400 + 500 = 1500 EUR // received 200 + 400 + 500 = 1100 EUR
env.trust(EUR(10000), bob); env.trust(EUR(10000), bob);
env.close(); env.close();
env(pay(gw, bob, EUR(2000))); env(pay(gw, bob, EUR(2000)));
@@ -438,15 +454,15 @@ public:
env(offer(bob, USD(4000), EUR(4000))); env(offer(bob, USD(4000), EUR(4000)));
env.close(); env.close();
env.require(balance(bob, USD(2309))); env.require(balance(bob, USD(1509)));
env.require(balance(bob, EUR(500))); env.require(balance(bob, EUR(900)));
env.require(offers(bob, 1)); env.require(offers(bob, 1));
env.require(owners(bob, 3)); env.require(owners(bob, 3));
env.require(balance(alice, USD(1694))); env.require(balance(alice, USD(2494)));
env.require(balance(alice, EUR(1500))); env.require(balance(alice, EUR(1100)));
auto const numAOffers = auto const numAOffers =
1 + 2000 + 300 + 1000 + 1 - (1 + 2 * 300 + 2 * 199 + 1 + 1); 1 + 2000 + 100 + 1000 + 1 - (1 + 2 * 100 + 2 * 199 + 1 + 1);
env.require(offers(alice, numAOffers)); env.require(offers(alice, numAOffers));
env.require(owners(alice, numAOffers + 2)); env.require(owners(alice, numAOffers + 2));
@@ -506,6 +522,17 @@ public:
// up a book with many offers. At each quality keep the number of offers // up a book with many offers. At each quality keep the number of offers
// below the limit. However, if all the offers are consumed it would // below the limit. However, if all the offers are consumed it would
// create a tecOVERSIZE error. // create a tecOVERSIZE error.
// The featureFlowSortStrands introduces a way of tracking the total
// number of consumed offers; with this feature the transaction no
// longer fails with a tecOVERSIZE error.
// The implementation allows any single step to consume at most 1000
// offers. With the `FlowSortStrands` feature enabled, if the total
// number of offers consumed by all the steps combined exceeds 1500, the
// payment stops. Since the first set of offers consumes 998 offers, the
// second set will consume 998, which is not over the limit and the
// payment stops. So 2*998, or 1996 is the expected value when
// `FlowSortStrands` is enabled.
n_offers(env, 998, alice, XRP(1.00), USD(1)); n_offers(env, 998, alice, XRP(1.00), USD(1));
n_offers(env, 998, alice, XRP(0.99), USD(1)); n_offers(env, 998, alice, XRP(0.99), USD(1));
n_offers(env, 998, alice, XRP(0.98), USD(1)); n_offers(env, 998, alice, XRP(0.98), USD(1));
@@ -514,11 +541,26 @@ public:
n_offers(env, 998, alice, XRP(0.95), USD(1)); n_offers(env, 998, alice, XRP(0.95), USD(1));
bool const withFlowCross = features[featureFlowCross]; bool const withFlowCross = features[featureFlowCross];
env(offer(bob, USD(8000), XRP(8000)), bool const withSortStrands = features[featureFlowSortStrands];
ter(withFlowCross ? TER{tecOVERSIZE} : tesSUCCESS));
auto const expectedTER = [&]() -> TER {
if (withFlowCross && !withSortStrands)
return TER{tecOVERSIZE};
return tesSUCCESS;
}();
env(offer(bob, USD(8000), XRP(8000)), ter(expectedTER));
env.close(); env.close();
env.require(balance(bob, USD(withFlowCross ? 0 : 850))); auto const expectedUSD = [&] {
if (!withFlowCross)
return USD(850);
if (!withSortStrands)
return USD(0);
return USD(1996);
}();
env.require(balance(bob, expectedUSD));
} }
void void
@@ -533,8 +575,9 @@ public:
}; };
using namespace jtx; using namespace jtx;
auto const sa = supported_amendments(); auto const sa = supported_amendments();
testAll(sa - featureFlowCross);
testAll(sa); testAll(sa);
testAll(sa - featureFlowSortStrands);
testAll(sa - featureFlowCross - featureFlowSortStrands);
} }
}; };

View File

@@ -392,13 +392,13 @@ struct Flow_test : public beast::unit_test::suite
env(pay(gw, bob, EUR(50))); env(pay(gw, bob, EUR(50)));
env(offer(bob, BTC(50), USD(50))); env(offer(bob, BTC(50), USD(50)));
env(offer(bob, BTC(60), EUR(50))); env(offer(bob, BTC(40), EUR(50)));
env(offer(bob, EUR(50), USD(50))); env(offer(bob, EUR(50), USD(50)));
// unfund offer // unfund offer
env(pay(bob, gw, EUR(50))); env(pay(bob, gw, EUR(50)));
BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50))); BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50)));
BEAST_EXPECT(isOffer(env, bob, BTC(60), EUR(50))); BEAST_EXPECT(isOffer(env, bob, BTC(40), EUR(50)));
BEAST_EXPECT(isOffer(env, bob, EUR(50), USD(50))); BEAST_EXPECT(isOffer(env, bob, EUR(50), USD(50)));
env(pay(alice, carol, USD(50)), env(pay(alice, carol, USD(50)),
@@ -414,7 +414,7 @@ struct Flow_test : public beast::unit_test::suite
// used in the payment // used in the payment
BEAST_EXPECT(!isOffer(env, bob, BTC(50), USD(50))); BEAST_EXPECT(!isOffer(env, bob, BTC(50), USD(50)));
// found unfunded // found unfunded
BEAST_EXPECT(!isOffer(env, bob, BTC(60), EUR(50))); BEAST_EXPECT(!isOffer(env, bob, BTC(40), EUR(50)));
// unfunded, but should not yet be found unfunded // unfunded, but should not yet be found unfunded
BEAST_EXPECT(isOffer(env, bob, EUR(50), USD(50))); BEAST_EXPECT(isOffer(env, bob, EUR(50), USD(50)));
} }
@@ -435,17 +435,20 @@ struct Flow_test : public beast::unit_test::suite
env.trust(EUR(1000), alice, bob, carol); env.trust(EUR(1000), alice, bob, carol);
env(pay(gw, alice, BTC(60))); env(pay(gw, alice, BTC(60)));
env(pay(gw, bob, USD(50))); env(pay(gw, bob, USD(60)));
env(pay(gw, bob, EUR(50))); env(pay(gw, bob, EUR(50)));
env(pay(gw, carol, EUR(1)));
env(offer(bob, BTC(50), USD(50))); env(offer(bob, BTC(50), USD(50)));
env(offer(bob, BTC(60), EUR(50))); env(offer(bob, BTC(60), EUR(50)));
env(offer(carol, BTC(1000), EUR(1)));
env(offer(bob, EUR(50), USD(50))); env(offer(bob, EUR(50), USD(50)));
// unfund offer // unfund offer
env(pay(bob, gw, EUR(50))); env(pay(bob, gw, EUR(50)));
BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50))); BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50)));
BEAST_EXPECT(isOffer(env, bob, BTC(60), EUR(50))); BEAST_EXPECT(isOffer(env, bob, BTC(60), EUR(50)));
BEAST_EXPECT(isOffer(env, carol, BTC(1000), EUR(1)));
auto flowJournal = env.app().logs().journal("Flow"); auto flowJournal = env.app().logs().journal("Flow");
auto const flowResult = [&] { auto const flowResult = [&] {
@@ -499,6 +502,7 @@ struct Flow_test : public beast::unit_test::suite
// used in payment, but since payment failed should be untouched // used in payment, but since payment failed should be untouched
BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50))); BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50)));
BEAST_EXPECT(isOffer(env, carol, BTC(1000), EUR(1)));
// found unfunded // found unfunded
BEAST_EXPECT(!isOffer(env, bob, BTC(60), EUR(50))); BEAST_EXPECT(!isOffer(env, bob, BTC(60), EUR(50)));
} }

View File

@@ -27,6 +27,44 @@
namespace ripple { namespace ripple {
namespace test { namespace test {
/** Count offer
*/
inline std::size_t
countOffers(
jtx::Env& env,
jtx::Account const& account,
Issue const& takerPays,
Issue const& takerGets)
{
size_t count = 0;
forEachItem(
*env.current(), account, [&](std::shared_ptr<SLE const> const& sle) {
if (sle->getType() == ltOFFER &&
sle->getFieldAmount(sfTakerPays).issue() == takerPays &&
sle->getFieldAmount(sfTakerGets).issue() == takerGets)
++count;
});
return count;
}
inline std::size_t
countOffers(
jtx::Env& env,
jtx::Account const& account,
STAmount const& takerPays,
STAmount const& takerGets)
{
size_t count = 0;
forEachItem(
*env.current(), account, [&](std::shared_ptr<SLE const> const& sle) {
if (sle->getType() == ltOFFER &&
sle->getFieldAmount(sfTakerPays) == takerPays &&
sle->getFieldAmount(sfTakerGets) == takerGets)
++count;
});
return count;
}
/** An offer exists /** An offer exists
*/ */
inline bool inline bool
@@ -36,15 +74,19 @@ isOffer(
STAmount const& takerPays, STAmount const& takerPays,
STAmount const& takerGets) STAmount const& takerGets)
{ {
bool exists = false; return countOffers(env, account, takerPays, takerGets) > 0;
forEachItem( }
*env.current(), account, [&](std::shared_ptr<SLE const> const& sle) {
if (sle->getType() == ltOFFER && /** An offer exists
sle->getFieldAmount(sfTakerPays) == takerPays && */
sle->getFieldAmount(sfTakerGets) == takerGets) inline bool
exists = true; isOffer(
}); jtx::Env& env,
return exists; jtx::Account const& account,
Issue const& takerPays,
Issue const& takerGets)
{
return countOffers(env, account, takerPays, takerGets) > 0;
} }
class Path class Path