mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
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:
@@ -311,6 +311,11 @@ public:
|
||||
// second test the strand does not have the best quality (the
|
||||
// implementation has to handle this case correct and not mark the
|
||||
// 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);
|
||||
|
||||
@@ -324,7 +329,7 @@ public:
|
||||
// Notice the strand with the 800 unfunded offers has the initial
|
||||
// best quality
|
||||
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(
|
||||
env, 801, carol, XRP(1), USD(3)); // only one offer is funded
|
||||
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
|
||||
// 1. The best quality is the autobridged offers that take 2 EUR
|
||||
// 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
|
||||
// and give 3 USD.
|
||||
@@ -345,19 +353,27 @@ public:
|
||||
// A book step is allowed to consume a maxium of 1000 offers
|
||||
// at a given quality, and that limit is now reached.
|
||||
// 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
|
||||
// received 600 USD in this step. (200 funded offers consumed
|
||||
// 800 unfunded offers)
|
||||
// XRP(1) to USD(3) offers available.
|
||||
// Bob has spent 400 EUR and received 600 USD in this step.
|
||||
// 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
|
||||
// gives 500 USD.
|
||||
// Bob has 2000 EUR, and has spent 600+400=1000 EUR. He has 1000
|
||||
// left. Bob spent 500 EUR and receives 500 USD.
|
||||
// In total: Bob spent EUR(600 + 400 + 500) = EUR(1500). He started
|
||||
// with 2000 so has 500 remaining
|
||||
// Bob received USD(1200 + 600 + 500) = USD(2300).
|
||||
// Alice spent 300*4 + 199*3 + 500 = 2297 USD. She started
|
||||
// with 4000 so has 1703 USD remaining. Alice received
|
||||
// 600 + 400 + 500 = 1500 EUR
|
||||
// Bob started with 2000 EUR
|
||||
// Bob spent 500 EUR (100+400)
|
||||
// Bob has 1500 EUR left
|
||||
// In this step:
|
||||
// Bob spents 500 EUR and receives 500 USD.
|
||||
// In total:
|
||||
// Bob spent 1100 EUR (200 + 400 + 500)
|
||||
// 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.close();
|
||||
env(pay(gw, bob, EUR(2000)));
|
||||
@@ -365,15 +381,15 @@ public:
|
||||
env(offer(bob, USD(4000), EUR(4000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, USD(2300)));
|
||||
env.require(balance(bob, EUR(500)));
|
||||
env.require(balance(bob, USD(1500)));
|
||||
env.require(balance(bob, EUR(900)));
|
||||
env.require(offers(bob, 1));
|
||||
env.require(owners(bob, 3));
|
||||
|
||||
env.require(balance(alice, USD(1703)));
|
||||
env.require(balance(alice, EUR(1500)));
|
||||
env.require(balance(alice, USD(2503)));
|
||||
env.require(balance(alice, EUR(1100)));
|
||||
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(owners(alice, numAOffers + 2));
|
||||
|
||||
@@ -393,7 +409,7 @@ public:
|
||||
// initial best quality
|
||||
n_offers(env, 1, alice, EUR(1), USD(10));
|
||||
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(
|
||||
env, 801, carol, XRP(1), USD(3)); // only one offer is funded
|
||||
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
|
||||
// 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
|
||||
// and gives 3 USD.
|
||||
@@ -423,14 +439,14 @@ public:
|
||||
// 800 unfunded offers)
|
||||
// 4. The best is the non-autobridged offers that takes 499 EUR and
|
||||
// gives 499 USD.
|
||||
// Bob has 2000 EUR, and has spent 1+600+400=1001 EUR. He has
|
||||
// 999 left. Bob spent 499 EUR and receives 499 USD.
|
||||
// In total: Bob spent EUR(1 + 600 + 400 + 499) = EUR(1500). He
|
||||
// started with 2000 so has 500 remaining
|
||||
// Bob received USD(10 + 1200 + 600 + 499) = USD(2309).
|
||||
// Alice spent 10 + 300*4 + 199*3 + 499 = 2306 USD. She
|
||||
// started with 4000 so has 1704 USD remaining. Alice
|
||||
// received 600 + 400 + 500 = 1500 EUR
|
||||
// Bob has 2000 EUR, and has spent 1+200+400=601 EUR. He has
|
||||
// 1399 left. Bob spent 499 EUR and receives 499 USD.
|
||||
// In total: Bob spent EUR(1 + 200 + 400 + 499) = EUR(1100). He
|
||||
// started with 2000 so has 900 remaining
|
||||
// Bob received USD(10 + 400 + 600 + 499) = USD(1509).
|
||||
// Alice spent 10 + 100*4 + 199*3 + 499 = 1506 USD. She
|
||||
// started with 4000 so has 2494 USD remaining. Alice
|
||||
// received 200 + 400 + 500 = 1100 EUR
|
||||
env.trust(EUR(10000), bob);
|
||||
env.close();
|
||||
env(pay(gw, bob, EUR(2000)));
|
||||
@@ -438,15 +454,15 @@ public:
|
||||
env(offer(bob, USD(4000), EUR(4000)));
|
||||
env.close();
|
||||
|
||||
env.require(balance(bob, USD(2309)));
|
||||
env.require(balance(bob, EUR(500)));
|
||||
env.require(balance(bob, USD(1509)));
|
||||
env.require(balance(bob, EUR(900)));
|
||||
env.require(offers(bob, 1));
|
||||
env.require(owners(bob, 3));
|
||||
|
||||
env.require(balance(alice, USD(1694)));
|
||||
env.require(balance(alice, EUR(1500)));
|
||||
env.require(balance(alice, USD(2494)));
|
||||
env.require(balance(alice, EUR(1100)));
|
||||
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(owners(alice, numAOffers + 2));
|
||||
|
||||
@@ -506,6 +522,17 @@ public:
|
||||
// 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
|
||||
// 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(0.99), 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));
|
||||
|
||||
bool const withFlowCross = features[featureFlowCross];
|
||||
env(offer(bob, USD(8000), XRP(8000)),
|
||||
ter(withFlowCross ? TER{tecOVERSIZE} : tesSUCCESS));
|
||||
bool const withSortStrands = features[featureFlowSortStrands];
|
||||
|
||||
auto const expectedTER = [&]() -> TER {
|
||||
if (withFlowCross && !withSortStrands)
|
||||
return TER{tecOVERSIZE};
|
||||
return tesSUCCESS;
|
||||
}();
|
||||
|
||||
env(offer(bob, USD(8000), XRP(8000)), ter(expectedTER));
|
||||
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
|
||||
@@ -533,8 +575,9 @@ public:
|
||||
};
|
||||
using namespace jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testAll(sa - featureFlowCross);
|
||||
testAll(sa);
|
||||
testAll(sa - featureFlowSortStrands);
|
||||
testAll(sa - featureFlowCross - featureFlowSortStrands);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -392,13 +392,13 @@ struct Flow_test : public beast::unit_test::suite
|
||||
env(pay(gw, bob, EUR(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)));
|
||||
|
||||
// unfund offer
|
||||
env(pay(bob, gw, EUR(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)));
|
||||
|
||||
env(pay(alice, carol, USD(50)),
|
||||
@@ -414,7 +414,7 @@ struct Flow_test : public beast::unit_test::suite
|
||||
// used in the payment
|
||||
BEAST_EXPECT(!isOffer(env, bob, BTC(50), USD(50)));
|
||||
// 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
|
||||
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(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, carol, EUR(1)));
|
||||
|
||||
env(offer(bob, BTC(50), USD(50)));
|
||||
env(offer(bob, BTC(60), EUR(50)));
|
||||
env(offer(carol, BTC(1000), EUR(1)));
|
||||
env(offer(bob, EUR(50), USD(50)));
|
||||
|
||||
// unfund offer
|
||||
env(pay(bob, gw, EUR(50)));
|
||||
BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(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 const flowResult = [&] {
|
||||
@@ -499,6 +502,7 @@ struct Flow_test : public beast::unit_test::suite
|
||||
|
||||
// used in payment, but since payment failed should be untouched
|
||||
BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50)));
|
||||
BEAST_EXPECT(isOffer(env, carol, BTC(1000), EUR(1)));
|
||||
// found unfunded
|
||||
BEAST_EXPECT(!isOffer(env, bob, BTC(60), EUR(50)));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,44 @@
|
||||
namespace ripple {
|
||||
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
|
||||
*/
|
||||
inline bool
|
||||
@@ -36,15 +74,19 @@ isOffer(
|
||||
STAmount const& takerPays,
|
||||
STAmount const& takerGets)
|
||||
{
|
||||
bool exists = false;
|
||||
forEachItem(
|
||||
*env.current(), account, [&](std::shared_ptr<SLE const> const& sle) {
|
||||
if (sle->getType() == ltOFFER &&
|
||||
sle->getFieldAmount(sfTakerPays) == takerPays &&
|
||||
sle->getFieldAmount(sfTakerGets) == takerGets)
|
||||
exists = true;
|
||||
});
|
||||
return exists;
|
||||
return countOffers(env, account, takerPays, takerGets) > 0;
|
||||
}
|
||||
|
||||
/** An offer exists
|
||||
*/
|
||||
inline bool
|
||||
isOffer(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& account,
|
||||
Issue const& takerPays,
|
||||
Issue const& takerGets)
|
||||
{
|
||||
return countOffers(env, account, takerPays, takerGets) > 0;
|
||||
}
|
||||
|
||||
class Path
|
||||
|
||||
Reference in New Issue
Block a user