diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index ac9c161d9..7f046233c 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -42,13 +42,15 @@ template class BookStep : public StepImp> { protected: - static constexpr uint32_t maxOffersToConsume_ = 2000; + uint32_t const maxOffersToConsume_; Book book_; AccountID strandSrc_; AccountID strandDst_; // Charge transfer fees when the prev step redeems Step const* const prevStep_ = nullptr; bool const ownerPaysTransferFee_; + // Mark as inactive (dry) if too many offers are consumed + bool inactive_ = false; beast::Journal j_; struct Cache @@ -64,11 +66,20 @@ protected: boost::optional cache_; + static + uint32_t getMaxOffersToConsume(StrandContext const& ctx) + { + if (ctx.view.rules().enabled(fix1515)) + return 1000; + return 2000; + } + public: BookStep (StrandContext const& ctx, Issue const& in, Issue const& out) - : book_ (in, out) + : maxOffersToConsume_ (getMaxOffersToConsume(ctx)) + , book_ (in, out) , strandSrc_ (ctx.strandSrc) , strandDst_ (ctx.strandDst) , prevStep_ (ctx.prevStep) @@ -136,6 +147,10 @@ public: // Check for errors frozen constraints. TER check(StrandContext const& ctx) const; + bool inactive() const override { + return inactive_; + } + protected: std::string logStringImpl (char const* name) const { @@ -719,9 +734,17 @@ BookStep::revImp ( if (offersConsumed >= maxOffersToConsume_) { - // Too many iterations, mark this strand as dry - cache_.emplace (beast::zero, beast::zero); - return {beast::zero, beast::zero}; + // Too many iterations, mark this strand as inactive + if (!afView.rules().enabled(fix1515)) + { + // Don't use the liquidity + cache_.emplace(beast::zero, beast::zero); + return {beast::zero, beast::zero}; + } + + // Use the liquidity, but use this to mark the strand as inactive so + // it's not used further + inactive_ = true; } } @@ -873,9 +896,17 @@ BookStep::fwdImp ( if (offersConsumed >= maxOffersToConsume_) { - // Too many iterations, mark this strand as dry - cache_.emplace (beast::zero, beast::zero); - return {beast::zero, beast::zero}; + // Too many iterations, mark this strand as inactive (dry) + if (!afView.rules().enabled(fix1515)) + { + // Don't use the liquidity + cache_.emplace(beast::zero, beast::zero); + return {beast::zero, beast::zero}; + } + + // Use the liquidity, but use this to mark the strand as inactive so + // it's not used further + inactive_ = true; } } diff --git a/src/ripple/app/paths/impl/Steps.h b/src/ripple/app/paths/impl/Steps.h index edfeda4c8..cda57cebf 100644 --- a/src/ripple/app/paths/impl/Steps.h +++ b/src/ripple/app/paths/impl/Steps.h @@ -189,7 +189,17 @@ public: */ virtual bool - dry (EitherAmount const& out) const = 0; + isZero (EitherAmount const& out) const = 0; + + /** + Return true if the step should be considered inactive. + A strand that has additional liquidity may be marked inactive if a step + has consumed too many offers. + */ + virtual + bool inactive() const{ + return false; + } /** Return true if Out of lhs == Out of rhs. @@ -404,7 +414,7 @@ struct StepImp : public Step } bool - dry (EitherAmount const& out) const override + isZero (EitherAmount const& out) const override { return get(out) == beast::zero; } diff --git a/src/ripple/app/paths/impl/StrandFlow.h b/src/ripple/app/paths/impl/StrandFlow.h index f9ad099cc..7301646d1 100644 --- a/src/ripple/app/paths/impl/StrandFlow.h +++ b/src/ripple/app/paths/impl/StrandFlow.h @@ -47,6 +47,8 @@ struct StrandResult TOutAmt out = beast::zero; ///< Currency amount out boost::optional sandbox; ///< Resulting Sandbox state boost::container::flat_set ofrsToRm; ///< Offers to remove + // 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; @@ -54,12 +56,14 @@ struct StrandResult StrandResult (TInAmt const& in_, TOutAmt const& out_, PaymentSandbox&& sandbox_, - boost::container::flat_set ofrsToRm_) + boost::container::flat_set ofrsToRm_, + bool inactive_) : ter (tesSUCCESS) , in (in_) , out (out_) , sandbox (std::move (sandbox_)) , ofrsToRm (std::move (ofrsToRm_)) + , inactive(inactive_) { } @@ -121,7 +125,7 @@ flow ( for (auto i = s; i--;) { auto r = strand[i]->rev (*sb, *afView, ofrsToRm, stepOut); - if (strand[i]->dry (r.second)) + if (strand[i]->isZero (r.second)) { JLOG (j.trace()) << "Strand found dry in rev"; return {tecPATH_DRY, std::move (ofrsToRm)}; @@ -139,7 +143,7 @@ flow ( *sb, *afView, ofrsToRm, EitherAmount (*maxIn)); limitStepOut = r.second; - if (strand[i]->dry (r.second) || + if (strand[i]->isZero (r.second) || get (r.first) != *maxIn) { // Something is very wrong @@ -163,7 +167,7 @@ flow ( r = strand[i]->rev (*sb, *afView, ofrsToRm, stepOut); limitStepOut = r.second; - if (strand[i]->dry (r.second) || + if (strand[i]->isZero (r.second) || !strand[i]->equalOut (r.second, stepOut)) { // Something is very wrong @@ -185,7 +189,7 @@ flow ( for (auto i = limitingStep + 1; i < s; ++i) { auto const r = strand[i]->fwd (*sb, *afView, ofrsToRm, stepIn); - if (strand[i]->dry (r.second) || + if (strand[i]->isZero (r.second) || !strand[i]->equalIn (r.first, stepIn)) { // The limits should already have been found, so executing a strand forward @@ -223,8 +227,17 @@ flow ( } #endif - return Result (get (strandIn), get (strandOut), - std::move (*sb), std::move (ofrsToRm)); + bool const inactive = std::any_of( + strand.begin(), + strand.end(), + [](std::unique_ptr const& step) { return step->inactive(); }); + + return Result( + get(strandIn), + get(strandOut), + std::move(*sb), + std::move(ofrsToRm), + inactive); } catch (FlowException const& e) { @@ -340,6 +353,14 @@ public: { return cur_.size (); } + + void + removeIndex(std::size_t i) + { + if (i >= next_.size()) + return; + next_.erase(next_.begin() + i); + } }; /// @endcond @@ -469,6 +490,10 @@ flow (PaymentSandbox const& baseView, boost::container::flat_set ofrsToRm; boost::optional 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 + boost::optional markInactiveOnUse{false, 0}; for (auto strand : activeStrands) { if (offerCrossing && limitQuality) @@ -514,13 +539,30 @@ flow (PaymentSandbox const& baseView, if (!best || best->quality < q || (best->quality == q && best->out < f.out)) - best.emplace (f.in, f.out, std::move (*f.sandbox), *strand, q); + { + // If this strand is inactive (because it consumed too many + // offers) and ends up having the best quality, remove it from + // the activeStrands. If it doesn't end up having the best + // quality, keep it active. + + if (f.inactive) + markInactiveOnUse = activeStrands.size() - 1; + else + markInactiveOnUse.reset(); + + best.emplace(f.in, f.out, std::move(*f.sandbox), *strand, q); + } } - bool const shouldBreak = !bool(best); + bool const shouldBreak = !best; if (best) { + if (markInactiveOnUse) + { + activeStrands.removeIndex(*markInactiveOnUse); + markInactiveOnUse.reset(); + } savedIns.insert (best->in); savedOuts.insert (best->out); remainingOut = outReq - sum (savedOuts); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 43e3f941f..a66127582 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -79,7 +79,8 @@ class FeatureCollections "fix1571", "fix1543", "fix1623", - "DepositPreauth" + "DepositPreauth", + "fix1515" }; std::vector features; @@ -365,6 +366,7 @@ extern uint256 const fix1571; extern uint256 const fix1543; extern uint256 const fix1623; extern uint256 const featureDepositPreauth; +extern uint256 const fix1515; } // ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index b212ec3c5..dd5216f97 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -111,7 +111,9 @@ detail::supportedAmendments () { "7117E2EC2DBF119CA55181D69819F1999ECEE1A0225A7FD2B9ED47940968479C fix1571" }, { "CA7C02118BA27599528543DFE77BA6838D1B0F43B447D4D7F53523CE6A0E9AC2 fix1543" }, { "58BE9B5968C4DA7C59BA900961828B113E5490699B21877DEF9A31E9D0FE5D5F fix1623" }, - { "3CBC5C4E630A1B82380295CDA84B32B49DD066602E74E39B85EF64137FA65194 DepositPreauth"} + { "3CBC5C4E630A1B82380295CDA84B32B49DD066602E74E39B85EF64137FA65194 DepositPreauth"}, + // Use liquidity from strands that consume max offers, but mark as dry + { "5D08145F0A4983F23AFFFF514E83FAD355C5ABFBB6CAB76FB5BC8519FF5F33BE fix1515"} }; return supported; } @@ -165,5 +167,6 @@ uint256 const fix1571 = *getRegisteredFeature("fix1571"); uint256 const fix1543 = *getRegisteredFeature("fix1543"); uint256 const fix1623 = *getRegisteredFeature("fix1623"); uint256 const featureDepositPreauth = *getRegisteredFeature("DepositPreauth"); +uint256 const fix1515 = *getRegisteredFeature("fix1515"); } // ripple diff --git a/src/test/app/CrossingLimits_test.cpp b/src/test/app/CrossingLimits_test.cpp index d7af52321..7fb9e711d 100644 --- a/src/test/app/CrossingLimits_test.cpp +++ b/src/test/app/CrossingLimits_test.cpp @@ -52,6 +52,11 @@ public: using namespace jtx; Env env(*this, features); + auto const closeTime = + fix1449Time() + + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + auto const gw = Account("gateway"); auto const USD = gw["USD"]; @@ -64,7 +69,7 @@ public: n_offers (env, 1, "dan", XRP(1), USD(1)); // Alice offers to buy 1000 XRP for 1000 USD. She takes Bob's first - // offer, and removes 999 more as unfunded and hits the step limit. + // offer, removes 999 more as unfunded, then hits the step limit. env(offer("alice", USD(1000), XRP(1000))); env.require (balance("alice", USD(1))); env.require (owners("alice", 2)); @@ -91,24 +96,38 @@ public: using namespace jtx; Env env(*this, features); + auto const closeTime = + fix1449Time() + + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + auto const gw = Account("gateway"); auto const USD = gw["USD"]; + // The number of allowed offers to cross is different between + // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. + // Accommodate that difference in the test. + int const maxConsumed = features[featureFlowCross] ? 1000 : 850; + env.fund(XRP(100000000), gw, "alice", "bob", "carol"); - env.trust(USD(1000), "bob"); - env(pay(gw, "bob", USD(1000))); - n_offers (env, 1000, "bob", XRP(1), USD(1)); + int const bobsOfferCount = maxConsumed + 150; + env.trust(USD(bobsOfferCount), "bob"); + env(pay(gw, "bob", USD(bobsOfferCount))); + env.close(); + n_offers (env, bobsOfferCount, "bob", XRP(1), USD(1)); - // Alice offers to buy 1000 XRP for 1000 USD. She takes the first - // 850 offers, hitting the crossing limit. - env(offer("alice", USD(1000), XRP(1000))); - env.require (balance("alice", USD(850))); + // Alice offers to buy Bob's offers. However she hits the offer + // crossing limit, so she can't buy them all at once. + env(offer("alice", USD(bobsOfferCount), XRP(bobsOfferCount))); + env.close(); + env.require (balance("alice", USD(maxConsumed))); env.require (balance("bob", USD(150))); - env.require (owners ("bob", 151)); + env.require (owners ("bob", 150 + 1)); - // Carol offers to buy 1000 XRP for 1000 USD. She takes the remaining - // 150 offers without hitting a limit. + // Carol offers to buy 1000 XRP for 1000 USD. She takes Bob's + // remaining 150 offers without hitting a limit. env(offer("carol", USD(1000), XRP(1000))); + env.close(); env.require (balance("carol", USD(150))); env.require (balance("bob", USD(0))); env.require (owners ("bob", 1)); @@ -121,21 +140,41 @@ public: using namespace jtx; Env env(*this, features); + auto const closeTime = + fix1449Time() + + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + auto const gw = Account("gateway"); auto const USD = gw["USD"]; env.fund(XRP(100000000), gw, "alice", "bob", "carol", "dan", "evita"); + // The number of offers allowed to cross is different between + // Taker and FlowCross. Taker allows 850 and FlowCross allows 1000. + // Accommodate that difference in the test. + bool const isFlowCross {features[featureFlowCross]}; + int const maxConsumed = isFlowCross ? 1000 : 850; + + int const evitasOfferCount {maxConsumed + 49}; env.trust(USD(1000), "alice"); env(pay(gw, "alice", USD(1000))); env.trust(USD(1000), "carol"); env(pay(gw, "carol", USD(1))); - env.trust(USD(1000), "evita"); - env(pay(gw, "evita", USD(1000))); + env.trust(USD(evitasOfferCount + 1), "evita"); + env(pay(gw, "evita", USD(evitasOfferCount + 1))); + // Taker and FlowCross have another difference we must accommodate. + // Taker allows a total of 1000 unfunded offers to be consumed + // beyond the 850 offers it can take. FlowCross draws no such + // distinction; its limit is 1000 funded or unfunded. + // + // Give carol an extra 150 (unfunded) offers when we're using Taker + // to accommodate that difference. + int const carolsOfferCount {isFlowCross ? 700 : 850}; n_offers (env, 400, "alice", XRP(1), USD(1)); - n_offers (env, 700, "carol", XRP(1), USD(1)); - n_offers (env, 999, "evita", XRP(1), USD(1)); + n_offers (env, carolsOfferCount, "carol", XRP(1), USD(1)); + n_offers (env, evitasOfferCount, "evita", XRP(1), USD(1)); // Bob offers to buy 1000 XRP for 1000 USD. He takes all 400 USD from // Alice's offers, 1 USD from Carol's and then removes 599 of Carol's @@ -145,15 +184,16 @@ public: env.require (balance("alice", USD(600))); env.require (owners("alice", 1)); env.require (balance("carol", USD(0))); - env.require (owners("carol", 101)); - env.require (balance("evita", USD(1000))); - env.require (owners("evita", 1000)); + env.require (owners("carol", carolsOfferCount - 599)); + env.require (balance("evita", USD(evitasOfferCount + 1))); + env.require (owners("evita", evitasOfferCount + 1)); - // Dan offers to buy 900 XRP for 900 USD. He removes all 100 of Carol's - // offers as unfunded, then takes 850 USD from Evita's, hitting the - // crossing limit. - env(offer("dan", USD(900), XRP(900))); - env.require (balance("dan", USD(850))); + // Dan offers to buy maxConsumed + 50 XRP USD. He removes all of + // Carol's remaining offers as unfunded, then takes + // (maxConsumed - 100) USD from Evita's, hitting the crossing limit. + env(offer("dan", USD(maxConsumed + 50), XRP(maxConsumed + 50))); + env.require (balance("dan", USD(maxConsumed - 100))); + env.require (owners("dan", 2)); env.require (balance("alice", USD(600))); env.require (owners("alice", 1)); env.require (balance("carol", USD(0))); @@ -162,12 +202,17 @@ public: env.require (owners("evita", 150)); } - void testAutoBridgedLimits (FeatureBitset features) + void testAutoBridgedLimitsTaker (FeatureBitset features) { - testcase ("Auto Bridged Limits"); + testcase ("Auto Bridged Limits Taker"); using namespace jtx; Env env(*this, features); + auto const closeTime = + fix1449Time() + + 100 * env.closed()->info().closeTimeResolution; + env.close (closeTime); + auto const gw = Account("gateway"); auto const USD = gw["USD"]; auto const EUR = gw["EUR"]; @@ -256,6 +301,246 @@ public: env.require (owners("evita", 152)); } + void + testAutoBridgedLimitsFlowCross(FeatureBitset features) + { + testcase("Auto Bridged Limits FlowCross"); + + // If any book step in a payment strand consumes 1000 offers, the + // liquidity from the offers is used, but that strand will be marked as + // dry for the remainder of the transaction. + + using namespace jtx; + + auto const gw = Account("gateway"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + auto const USD = gw["USD"]; + auto const EUR = gw["EUR"]; + + // There are two almost identical tests. There is a strand with a large + // number of unfunded offers that will cause the strand to be marked dry + // even though there will still be liquidity available on that strand. + // In the first test, the strand has the best initial quality. In the + // 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) + { + Env env(*this, features); + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close(closeTime); + + env.fund(XRP(100000000), gw, alice, bob, carol); + + env.trust(USD(4000), alice); + env(pay(gw, alice, USD(4000))); + env.trust(USD(1000), carol); + env(pay(gw, carol, USD(3))); + + // 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, 801, carol, XRP(1), USD(3)); // only one offer is funded + n_offers(env, 1000, alice, XRP(1), USD(3)); + + n_offers(env, 1, alice, EUR(500), USD(500)); + + // 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. + // + // 2. The best quality is the autobridged offers that take 2 EUR + // and give 3 USD. + // a. One of Carol's offers is taken. This leaves her other + // offers unfunded. + // b. Carol's remaining 800 offers are consumed as unfunded. + // c. 199 of alice's XRP(1) to USD(3) offers are consumed. + // 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) + // 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 + env.trust(EUR(10000), bob); + env.close(); + env(pay(gw, bob, EUR(2000))); + env.close(); + env(offer(bob, USD(4000), EUR(4000))); + env.close(); + + env.require(balance(bob, USD(2300))); + env.require(balance(bob, EUR(500))); + env.require(offers(bob, 1)); + env.require(owners(bob, 3)); + + env.require(balance(alice, USD(1703))); + env.require(balance(alice, EUR(1500))); + auto const numAOffers = + 2000 + 300 + 1000 + 1 - (2 * 300 + 2 * 199 + 1 + 1); + env.require(offers(alice, numAOffers)); + env.require(owners(alice, numAOffers + 2)); + + env.require(offers(carol, 0)); + } + { + Env env(*this, features); + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close(closeTime); + + env.fund(XRP(100000000), gw, alice, bob, carol); + + env.trust(USD(4000), alice); + env(pay(gw, alice, USD(4000))); + env.trust(USD(1000), carol); + env(pay(gw, carol, USD(3))); + + // Notice the strand with the 800 unfunded offers does not have the + // 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, 801, carol, XRP(1), USD(3)); // only one offer is funded + n_offers(env, 1000, alice, XRP(1), USD(3)); + + n_offers(env, 1, alice, EUR(499), USD(499)); + + // Bob offers to buy 2000 USD for 2000 EUR; He starts with 2000 EUR + // 1. The best quality is the offer that takes 1 EUR and gives 10 USD + // Bob spends 1 EUR and receives 10 USD. + // + // 2. The best quality is the autobridged offers that takes 2 EUR + // and gives 4 USD. + // Bob spends 600 EUR and receives 1200 USD. + // + // 3. The best quality is the autobridged offers that takes 2 EUR + // and gives 3 USD. + // a. One of Carol's offers is taken. This leaves her other + // offers unfunded. + // b. Carol's remaining 800 offers are consumed as unfunded. + // c. 199 of alice's XRP(1) to USD(3) offers are consumed. + // 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) + // 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 + env.trust(EUR(10000), bob); + env.close(); + env(pay(gw, bob, EUR(2000))); + env.close(); + env(offer(bob, USD(4000), EUR(4000))); + env.close(); + + env.require(balance(bob, USD(2309))); + env.require(balance(bob, EUR(500))); + env.require(offers(bob, 1)); + env.require(owners(bob, 3)); + + env.require(balance(alice, USD(1694))); + env.require(balance(alice, EUR(1500))); + auto const numAOffers = + 1 + 2000 + 300 + 1000 + 1 - (1 + 2 * 300 + 2 * 199 + 1 + 1); + env.require(offers(alice, numAOffers)); + env.require(owners(alice, numAOffers + 2)); + + env.require(offers(carol, 0)); + } + } + + void testAutoBridgedLimits (FeatureBitset features) + { + // Taker and FlowCross are too different in the way they handle + // autobridging to make one test suit both approaches. + // + // o Taker alternates between books, completing one full increment + // before returning to make another pass. + // + // o FlowCross extracts as much as possible in one book at one Quality + // before proceeding to the other book. This reduces the number of + // times we change books. + // + // So the tests for the two forms of autobridging are separate. + if (features[featureFlowCross]) + testAutoBridgedLimitsFlowCross (features); + else + testAutoBridgedLimitsTaker (features); + } + + void + testOfferOverflow (FeatureBitset features) + { + testcase("Offer Overflow"); + + using namespace jtx; + + auto const gw = Account("gateway"); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + auto const USD = gw["USD"]; + + Env env(*this, features); + auto const closeTime = + fix1449Time() + 100 * env.closed()->info().closeTimeResolution; + env.close(closeTime); + + env.fund(XRP(100000000), gw, alice, bob); + + env.trust(USD(8000), alice); + env.trust(USD(8000), bob); + env.close(); + + env(pay(gw, alice, USD(8000))); + env.close(); + + // The new flow cross handles consuming excessive offers differently than the old + // offer crossing code. In the old code, the total number of consumed offers is tracked, and + // the crossings will stop after this limit is hit. In the new code, the number of offers is + // tracked per offerbook and per quality. This test shows how they can differ. Set 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. + 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)); + n_offers(env, 998, alice, XRP(0.97), USD(1)); + n_offers(env, 998, alice, XRP(0.96), USD(1)); + 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)); + env.close(); + + env.require(balance(bob, USD(withFlowCross ? 0 : 850))); + } + void run() override { @@ -264,13 +549,14 @@ public: testCrossingLimit(features); testStepAndCrossingLimit(features); testAutoBridgedLimits(features); + testOfferOverflow(features); }; using namespace jtx; auto const sa = supported_amendments(); testAll(sa - featureFlow - fix1373 - featureFlowCross); testAll(sa - fix1373 - featureFlowCross); testAll(sa - featureFlowCross); -// testAll(sa);// Does not pass with FlowCross enabled. + testAll(sa ); } };